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 -

- - + +

-[![Build](https://github.com/n64brew/libdragon/actions/workflows/build-toolchain-library-and-roms.yml/badge.svg?branch=trunk)](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 +

+ + Build + +

> [!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 + + + + + + + + + + + + + + + + + + + +
+ +

🎨  Graphics & 3D

+ +RDPQ • Sprites • Text & Fonts • TrueType • OpenGL 1.1 • Magma 3D • +glTF Models + +
+ +

🔊  Audio & Music

+ +RSP Mixer • Streaming • VADPCM • ULC • Opus • XM modules • YM modules • +MIDI + SF2 + +
+ +

🎬  Video

+ +MPEG-1 • H.264 • YUV • +Subtitles • +Seeking • Render-to-texture + +
+ +

🎮  Input & Peripherals

+ +Joypads • GameCube controllers • Mouse • Controller Pak • Rumble Pak • Transfer Pak • Bio Sensor • RTC + +
+ +

💾  Storage & Filesystems

+ +DragonFS • C/POSIX API • +SD cards • EEPROM • +SRAM • +FlashRAM • +iQue NAND/BBFS + +
+ +

🚀  Runtime & Boot

+ +64-bit ABI • +C11 Runtime • iQue support • +Open-source IPL3 • +Region free • +Coroutines • DSO/Overlays + +
+ +

⚙️  Low-level Hardware

+ +RSP / RSPQ • RDP / RDPQ • Custom microcode • PI DMA queue • Interrupts • Timers • Exceptions + +
+ +

🛠️  Toolchain & Asset Pipeline

+ +GCC 16 • Docker • PNG • TTF/OTF • glTF • Audio conversion • Video conversion • Compression + +
+ +

🐛  Debugging & Profiling

+ +Crash screen • Symbolized backtraces • Assertions • USB logging • +CPU profiler • RDP validation • RSP crash diagnostics + +
## 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 + * @author Giovanni Bajo * @brief RDP Command queue * @ingroup rdpq */ @@ -144,9 +145,11 @@ #ifndef __LIBDRAGON_RDPQ_H #define __LIBDRAGON_RDPQ_H + #include #include #include +#include "preview.h" #include "graphics.h" #include "n64sys.h" #include "rdpq_macros.h" @@ -164,6 +167,7 @@ enum { RDPQ_CMD_NOOP = 0x00, RDPQ_CMD_SET_LOOKUP_ADDRESS = 0x01, RDPQ_CMD_FILL_RECTANGLE_EX = 0x02, + RDPQ_CMD_CLEAR_ZBUFFER = 0x03, RDPQ_CMD_RESET_RENDER_MODE = 0x04, RDPQ_CMD_SET_COMBINE_MODE_2PASS = 0x05, RDPQ_CMD_PUSH_RENDER_MODE = 0x06, @@ -185,6 +189,7 @@ enum { RDPQ_CMD_SET_FILL_COLOR_32 = 0x16, RDPQ_CMD_SET_BLENDING_MODE = 0x18, RDPQ_CMD_SET_FOG_MODE = 0x19, + RDPQ_CMD_WAIT_IDLE = 0x1A, RDPQ_CMD_SET_COMBINE_MODE_1PASS = 0x1B, RDPQ_CMD_AUTOTMEM_SET_ADDR = 0x1C, RDPQ_CMD_AUTOTMEM_SET_TILE = 0x1D, @@ -381,6 +386,39 @@ uint32_t rdpq_config_enable(uint32_t cfg_enable_bits); */ uint32_t rdpq_config_disable(uint32_t cfg_disable_bits); +/** + * @brief Low level function to set the components of the chroma key + * @preview + */ +LIBDRAGON_PREVIEW_API +inline void rdpq_set_chromakey_parms(color_t color, + int edge_r, int edge_g, int edge_b, + int width_r, int width_g, int width_b) +{ + float fsr = 1.0f / edge_r; + float fsg = 1.0f / edge_g; + float fsb = 1.0f / edge_b; + uint8_t sr = fsr * 255.0f; + uint8_t sg = fsg * 255.0f; + uint8_t sb = fsb * 255.0f; + float fwr = width_r * fsr; + float fwg = width_g * fsg; + float fwb = width_b * fsb; + uint16_t wr = fwr * 255.0f; + uint16_t wg = fwg * 255.0f; + uint16_t wb = fwb * 255.0f; + + extern void __rdpq_write8_syncchange(uint32_t cmd_id, uint32_t arg0, uint32_t arg1, uint32_t autosync); + __rdpq_write8_syncchange(RDPQ_CMD_SET_KEY_R, + 0, + _carg(wr, 0xFFF, 16) | _carg(color.r, 0xFF, 8) | _carg(sr, 0xFF, 0), + AUTOSYNC_PIPE); + __rdpq_write8_syncchange(RDPQ_CMD_SET_KEY_GB, + _carg(wg, 0xFFF, 12) | _carg(wb, 0xFFF, 0), + _carg(color.g, 0xFF, 24) | _carg(sg, 0xFF, 16) | _carg(color.b, 0xFF, 8) | _carg(sb, 0xFF, 0), + AUTOSYNC_PIPE); +} + /** * @brief Low level functions to set the matrix coefficients for texture format conversion */ @@ -591,9 +629,10 @@ inline void rdpq_load_tile_fx(rdpq_tile_t tile, uint16_t s0, uint16_t t0, uint16 inline void rdpq_load_tlut_raw(rdpq_tile_t tile, int color_idx, int num_colors) { extern void __rdpq_write8_syncchangeuse(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t); + assertf(num_colors <= 256, "invalid palette length %d: must be smaller than 256", num_colors); __rdpq_write8_syncchangeuse(RDPQ_CMD_LOAD_TLUT, _carg(color_idx, 0xFF, 14), - _carg(tile, 0x7, 24) | _carg(color_idx+num_colors-1, 0xFF, 14), + _carg(tile, 0x7, 24) | _carg(color_idx+num_colors-1, 0x1FF, 14), AUTOSYNC_TMEM(0), AUTOSYNC_TILE(tile)); } @@ -978,7 +1017,7 @@ inline void rdpq_set_prim_color(color_t color) inline void rdpq_set_detail_factor(float value) { // NOTE: this does not require a pipe sync - int8_t conv = (1.0 - value) * 31; + int8_t conv = (1.0f - value) * 31; extern void __rdpq_fixup_write8_syncchange(uint32_t, uint32_t, uint32_t, uint32_t); __rdpq_fixup_write8_syncchange(RDPQ_CMD_SET_PRIM_COLOR_COMPONENT, ((conv & 0x1F) << 8) | (2<<16), 0, 0); } @@ -1170,7 +1209,7 @@ inline void rdpq_set_color_image_raw(uint8_t index, uint32_t offset, tex_format_ extern void __rdpq_set_color_image(uint32_t, uint32_t, uint32_t, uint32_t); __rdpq_set_color_image( _carg(format, 0x1F, 19) | _carg(TEX_FORMAT_BYTES2PIX(format, stride)-1, 0x3FF, 0) | _carg(height-1, 0x1FF, 10), - _carg(index, 0xF, 28) | (offset & 0xFFFFFF) | _carg((height-1)>>9, 0x1, 31), + _carg(index, 0xF, 26) | (offset & 0x1FFFFFF) | _carg((height-1)>>9, 0x1, 31), _carg(0, 0xFFF, 12) | _carg(0, 0xFFF, 0), // for set_scissor _carg(width*4, 0xFFF, 12) | _carg(height*4, 0xFFF, 0)); // for set_scissor } @@ -1200,7 +1239,7 @@ inline void rdpq_set_z_image_raw(uint8_t index, uint32_t offset) extern void __rdpq_fixup_write8_syncchange(uint32_t, uint32_t, uint32_t, uint32_t); __rdpq_fixup_write8_syncchange(RDPQ_CMD_SET_Z_IMAGE, 0, - _carg(index, 0xF, 28) | (offset & 0xFFFFFF), + _carg(index, 0xF, 26) | (offset & 0x1FFFFFF), AUTOSYNC_PIPE); } @@ -1236,7 +1275,7 @@ inline void rdpq_set_texture_image_raw(uint8_t index, uint32_t offset, tex_forma // to help the validator to a better job. The RDP hardware ignores those bits. __rdpq_fixup_write8_syncchange(RDPQ_CMD_SET_TEXTURE_IMAGE, _carg(format, 0x1F, 19) | _carg(width-1, 0x3FF, 0) | _carg(height-1, 0x1FF, 10), - _carg(index, 0xF, 28) | (offset & 0xFFFFFF) | _carg((height-1)>>9, 0x1, 31), + _carg(index, 0xF, 26) | (offset & 0x1FFFFFF) | _carg((height-1)>>9, 0x1, 31), AUTOSYNC_PIPE); } @@ -1261,7 +1300,7 @@ inline void rdpq_load_block_linear(int32_t offset, void *buffer, uint16_t size) rdpq_set_texture_image_raw(0, PhysicalAddr(buffer), FMT_RGBA16, 8, size / 8); rdpq_set_tile(RDPQ_TILE_INTERNAL, FMT_RGBA16, offset, 0, NULL); uint32_t num_texels = size / 2; - rdpq_load_block(RDPQ_TILE_INTERNAL, 0, 0, num_texels, 16); + rdpq_load_block_fx(RDPQ_TILE_INTERNAL, 0, 0, num_texels, 0); } /** @@ -1487,6 +1526,19 @@ inline void rdpq_set_combiner_raw(uint64_t comb) { AUTOSYNC_PIPE); } +/** + * @brief Read the current combiner register. + * @preview + * + * This function executes a full sync (#rspq_wait) and then extracts the + * current raw combiner from the RSP state. This should be used only + * for debugging purposes. + * + * @return THe current value of the combiner register. + */ +LIBDRAGON_PREVIEW_API +uint64_t rdpq_get_combiner_raw(void); + /** * @brief Add a fence to synchronize RSP with RDP commands. * @@ -1529,6 +1581,40 @@ void rdpq_fence(void); */ void rdpq_exec(void *buffer, int size); +/** + * @brief Enqueue a callback that will be called after the RSP and the RDP have + * @preview + * finished processing all commands enqueued until now. + * + * This function is similar to #rspq_call_deferred, but it also guarantees + * that the callback is called after the RDP has finished processing all + * commands enqueued until now. + * + * For example: + * + * @code{.c} + * // Draw a green rectangle + * rdpq_mode_set_fill(RGBA(0,255,0,0)); + * rdpq_fill_rectangle(10, 10, 100, 100); + * + * // Enqueue a callback. The callback is guaranteed to be called + * // after the RSP has finished prepared the RDP command list for the + * // filled rectangle. It is possible that the RDP would still + * // be processing the rectangle when the callback is called. + * rspq_call_deferred(my_callback1, NULL); + * + * // Enqueue a callback. The callback is guaranteed to be called + * // after the rectangle has been fully drawn to the target buffer, so + * // that for instance the callback could readback the green pixels. + * rdpq_call_deferred(my_callback2, NULL); + * @endcode + * + * @param func Callback function to call + * @param arg Argument to pass to the callback function + */ +LIBDRAGON_PREVIEW_API +void rdpq_call_deferred(void (*func)(void *), void *arg); + /** * @brief Enqueue a RSP command that also generates RDP commands. * @@ -1577,8 +1663,44 @@ void rdpq_exec(void *buffer, int size); rspq_write(ovl_id, cmd_id, ##__VA_ARGS__); \ }) +/** + * @brief Begin enqueuing a RSP command that also generates RDP commands. + * + * This macro is similar to #rspq_write_begin, but like #rdpq_write (versus + * #rspq_write), it also declares that the RSP command is going to generate + * RDP commands as part of its execution, so that space for them can be + * reserved in the RDP static buffer when this is called within a block. + * + * Use this instead of #rdpq_write whenever the command is too complex or + * variable-sized to be built with a single #rdpq_write call; finish the + * command with (zero or more) calls to #rspq_write_arg followed by a call + * to #rspq_write_end. + * + * @param num_rdp_commands Maximum number of RDP 8-byte commands that will be + * generated by the RSP command. Use -1 if the number + * is unbounded and potentially high. + * @param ovl_id ID of the overlay for the command (see #rspq_write_begin) + * @param cmd_id ID of the command (see #rspq_write_begin) + * @param size Size of the command, in 32-bit words (see #rspq_write_begin) + * @return A write cursor, that must be passed to #rspq_write_arg + * and #rspq_write_end + * + * @see #rdpq_write + * @see #rspq_write_begin + * @hideinitializer + */ +#define rdpq_write_begin(num_rdp_commands, ovl_id, cmd_id, size) ({ \ + int __num_rdp_commands = (num_rdp_commands); \ + if (!__builtin_constant_p(__num_rdp_commands) || __num_rdp_commands != 0) { \ + if (__builtin_expect(rspq_block != NULL, 0)) { \ + __rdpq_block_reserve(__num_rdp_commands); \ + } \ + } \ + rspq_write_begin(ovl_id, cmd_id, size); \ +}) + /// @cond -// Declarations used by rdpq_write, not part of the public API. +// Declarations used by rdpq_write and rdpq_write_begin, not part of the public API. typedef struct rspq_block_s rspq_block_t; extern rspq_block_t *rspq_block; extern void __rdpq_block_reserve(int); \ diff --git a/include/rdpq_attach.h b/include/rdpq_attach.h index 1453712f4..fe1337536 100644 --- a/include/rdpq_attach.h +++ b/include/rdpq_attach.h @@ -1,6 +1,6 @@ /** * @file rdpq_attach.h - * @author Dennis Heinze + * @author Giovanni Bajo * @brief RDP Command queue: surface attachment API * @ingroup rdpq * @@ -18,6 +18,7 @@ #ifndef LIBDRAGON_RDPQ_ATTACH_H #define LIBDRAGON_RDPQ_ATTACH_H +#include "preview.h" #include "rspq.h" #ifdef __cplusplus @@ -60,7 +61,7 @@ void rdpq_attach(const surface_t *surf_color, const surface_t *surf_z); * * This function is similar to #rdpq_attach, but it also clears the surface * to full black (color 0) immediately after attaching. If a z-buffer is - * specified, it is also cleared (to 0xFFFC). + * specified, it is also cleared (to #ZBUF_MAX). * * This function is just a shortcut for calling #rdpq_attach, #rdpq_clear and * #rdpq_clear_z. @@ -196,6 +197,18 @@ inline void rdpq_detach_wait(void) */ void rdpq_detach_cb(void (*cb)(void*), void *arg); +/** + * @brief Get the surface that is currently attached to the RDP + * @preview + * + * @return A pointer to the surface that is currently attached to the RDP, + * or NULL if none is attached. + * + * @see #rdpq_attach + */ +LIBDRAGON_PREVIEW_API +const surface_t* rdpq_get_attached(void); + #ifdef __cplusplus } #endif diff --git a/include/rdpq_constants.h b/include/rdpq_constants.h index 8a98d4ec9..5c88eaddd 100644 --- a/include/rdpq_constants.h +++ b/include/rdpq_constants.h @@ -30,6 +30,9 @@ /** @brief Asserted if the TMEM is full during an auto-TMEM operation */ #define RDPQ_ASSERT_AUTOTMEM_UNPAIRED 0xC008 +/** @brief RDPQCmd_ClearZBuffer temporary buffer is too small */ +#define RDPQ_ASSERT_ZCLEAR_INVALID_BUFFER 0xC009 + /** @brief Maximum RDPQ command size */ #define RDPQ_MAX_COMMAND_SIZE 44 #define RDPQ_BLOCK_MIN_SIZE 64 ///< RDPQ block minimum size (in 32-bit words) diff --git a/include/rdpq_debug.h b/include/rdpq_debug.h index de7418276..ee105b5cd 100644 --- a/include/rdpq_debug.h +++ b/include/rdpq_debug.h @@ -1,6 +1,6 @@ /** * @file rdpq_debug.h - * @author Dennis Heinze + * @author Giovanni Bajo * @brief RDP Command queue: debugging helpers * @ingroup rdpq */ diff --git a/include/rdpq_font.h b/include/rdpq_font.h index 5637f9510..f4b97a727 100644 --- a/include/rdpq_font.h +++ b/include/rdpq_font.h @@ -38,7 +38,7 @@ typedef struct { * format, as created by the mkfont tool. * * @param fn Filename to load (including filesystem prefix, eg: "rom:/arial.font64") - * @return rdpq_font_t* Loaded font + * @return Loaded font */ rdpq_font_t* rdpq_font_load(const char *fn); @@ -54,7 +54,7 @@ rdpq_font_t* rdpq_font_load(const char *fn); * * @param buf Buffer to load * @param sz Size of the buffer - * @return rdpq_font_t* Loaded font + * @return Loaded font */ rdpq_font_t* rdpq_font_load_buf(void *buf, int sz); @@ -87,7 +87,7 @@ typedef enum { * * @param font Builtin font to load * - * @return rdpq_font_t* Loaded font + * @return Loaded font */ inline rdpq_font_t* rdpq_font_load_builtin(rdpq_font_builtin_t font) { switch (font) { @@ -158,7 +158,7 @@ void rdpq_font_style(rdpq_font_t *font, uint8_t style_id, const rdpq_fontstyle_t * @param chars Array of chars to render * @param x0 X coordinate of the start of the paragraph (baseline first char) * @param y0 Y coordinate of the start of the paragraph (baseline first char) - * @return int Number of chars rendered + * @return Number of chars rendered */ int rdpq_font_render_paragraph(const rdpq_font_t *fnt, const rdpq_paragraph_char_t *chars, float x0, float y0); @@ -206,7 +206,7 @@ bool rdpq_font_get_glyph_ranges(const rdpq_font_t *fnt, int idx, uint32_t *start * * @param fnt Font to query * @param codepoint Unicode codepoint of the glyph - * @return int Internal index of the glyph, or -1 if the glyph does not exist + * @return Internal index of the glyph, or -1 if the glyph does not exist */ int rdpq_font_get_glyph_index(const rdpq_font_t *fnt, uint32_t codepoint); @@ -220,7 +220,7 @@ int rdpq_font_get_glyph_index(const rdpq_font_t *fnt, uint32_t codepoint); * @param fnt Font to query * @param codepoint Unicode codepoint of the glyph * @param metrics Pointer to a structure to store the metrics - * @return bool true if the glyph was found, false otherwise + * @return true if the glyph was found, false otherwise */ bool rdpq_font_get_glyph_metrics(const rdpq_font_t *fnt, uint32_t codepoint, rdpq_font_gmetrics_t *metrics); diff --git a/include/rdpq_macros.h b/include/rdpq_macros.h index ef7e3e528..65a48ccd8 100644 --- a/include/rdpq_macros.h +++ b/include/rdpq_macros.h @@ -1,6 +1,7 @@ /** * @file rdpq_macros.h * @author Dennis Heinze + * @author Giovanni Bajo * @brief RDP command macros * @ingroup rdpq * @@ -140,7 +141,8 @@ typedef uint32_t rdpq_blender_t; #define _RDPQ_COMB2B_RGB_MUL_ENV cast64(5) #define _RDPQ_COMB2B_RGB_MUL_KEYSCALE cast64(6) #define _RDPQ_COMB2B_RGB_MUL_COMBINED_ALPHA cast64(7) -#define _RDPQ_COMB2B_RGB_MUL_TEX1_ALPHA cast64(8) // TEX0_ALPHA not available in 2nd cycle (pipelined) +#define _RDPQ_COMB2B_RGB_MUL_TEX1_ALPHA cast64(8) +#define _RDPQ_COMB2B_RGB_MUL_TEX0_ALPHA cast64(9) // TEX0_ALPHA is buggy in 2nd cycle: it refers to *next* pixel in the scanline #define _RDPQ_COMB2B_RGB_MUL_PRIM_ALPHA cast64(10) #define _RDPQ_COMB2B_RGB_MUL_SHADE_ALPHA cast64(11) #define _RDPQ_COMB2B_RGB_MUL_ENV_ALPHA cast64(12) @@ -272,6 +274,38 @@ typedef uint32_t rdpq_blender_t; */ #define RDPQ_COMBINER_2PASS (cast64(1)<<63) +///@cond +#define __RDPQ_PP_CAT(a,b) __RDPQ_PP_CAT_I(a,b) +#define __RDPQ_PP_CAT_I(a,b) a##b +#define __RDPQ_PP_PROBE() ~,1 +#define __RDPQ_PP_CHECK(...) __RDPQ_PP_CHECK_N(__VA_ARGS__,0) +#define __RDPQ_PP_CHECK_N(x,n,...) n +#define __RDPQ_PP_IS_TEX1(x) __RDPQ_PP_CHECK(__RDPQ_PP_CAT(__RDPQ_PP_TEX1_PROBE_,x)) +#define __RDPQ_PP_TEX1_PROBE_TEX1 __RDPQ_PP_PROBE() + +#define __RDPQ_PP_TUPLE_ELEM_0(a,b,c,d) a +#define __RDPQ_PP_TUPLE_ELEM_1(a,b,c,d) b +#define __RDPQ_PP_TUPLE_ELEM_2(a,b,c,d) c +#define __RDPQ_PP_TUPLE_ELEM_3(a,b,c,d) d + +#define __RDPQ_PP_OR(a,b) __RDPQ_PP_CAT(__RDPQ_PP_OR_,a)(b) +#define __RDPQ_PP_OR_0(b) b +#define __RDPQ_PP_OR_1(b) 1 + +#define __RDPQ_PP_HAS_TEX1_TUPLE(t) \ + __RDPQ_PP_OR(__RDPQ_PP_IS_TEX1(__RDPQ_PP_TUPLE_ELEM_0 t), \ + __RDPQ_PP_OR(__RDPQ_PP_IS_TEX1(__RDPQ_PP_TUPLE_ELEM_1 t), \ + __RDPQ_PP_OR(__RDPQ_PP_IS_TEX1(__RDPQ_PP_TUPLE_ELEM_2 t), \ + __RDPQ_PP_IS_TEX1(__RDPQ_PP_TUPLE_ELEM_3 t)))) + +#define __RDPQ_PP_HAS_TEX1(rgb, alpha) \ + __RDPQ_PP_OR(__RDPQ_PP_HAS_TEX1_TUPLE(rgb), __RDPQ_PP_HAS_TEX1_TUPLE(alpha)) + +#define __RDPQ_PP_IIF(c) __RDPQ_PP_CAT(__RDPQ_PP_IIF_,c) +#define __RDPQ_PP_IIF_0(t,f) f +#define __RDPQ_PP_IIF_1(t,f) t +///@endcond + /** * @brief Build a 1-pass combiner formula * @@ -401,7 +435,7 @@ typedef uint32_t rdpq_blender_t; * * // Set a combiner to sample TEX0 as-is in RGB channels, and put a fixed value * // as alpha channel, coming from the ENV register. - * rdpq_mode_combiner(RDPQ_COMBINER1((ZERO, ZERO, ZERO, TEX0), (ZERO, ZERO, ZERO, ENV))); + * rdpq_mode_combiner(RDPQ_COMBINER1((0, 0, 0, TEX0), (0, 0, 0, ENV))); * * // Set the fixed value in the ENV register. RGB components are ignored as the slot * // ENV is not used in the RGB combiner formula, so we just put zero there. @@ -430,7 +464,11 @@ typedef uint32_t rdpq_blender_t; * @hideinitializer */ #define RDPQ_COMBINER1(rgb, alpha) \ - castcc(__rdpq_1cyc_comb_rgb rgb | __rdpq_1cyc_comb_alpha alpha) + __RDPQ_PP_IIF(__RDPQ_PP_HAS_TEX1(rgb, alpha))( \ + RDPQ_COMBINER2(rgb, alpha, (0,0,0,COMBINED), (0,0,0,COMBINED)), \ + castcc(__rdpq_1cyc_comb_rgb rgb | __rdpq_1cyc_comb_alpha alpha) \ + ) + /** * @brief Build a 2-pass combiner formula @@ -516,6 +554,9 @@ typedef uint32_t rdpq_blender_t; ///@{ #define SOMX_NUMLODS_MASK ((cast64(7))<<59) ///< Rdpq extension: number of LODs #define SOMX_NUMLODS_SHIFT 59 ///< Rdpq extension: number of LODs shift +#define SOMX_FOG ((cast64(1))<<58) ///< RDPQ special state: fogging is enabled +#define SOMX_ALPHACOMPARE ((cast64(1))<<57) ///< RDPQ special state: alpha compare is enabled (logical state; see mode API) +#define SOMX_ALPHACOMPARE_SHIFT 57 ///< Shift of #SOMX_ALPHACOMPARE in the 64-bit SOM word #define SOM_ATOMIC_PRIM ((cast64(1))<<55) ///< Atomic: serialize command execution @@ -566,10 +607,12 @@ typedef uint32_t rdpq_blender_t; #define SOM_ALPHADITHER_MASK ((cast64(3))<<36) ///< Alpha Dithering mask #define SOM_ALPHADITHER_SHIFT 36 ///< Alpha Dithering mask shift -#define SOMX_FOG ((cast64(1))<<32) ///< RDPQ special state: fogging is enabled -#define SOMX_UPDATE_FREEZE ((cast64(1))<<33) ///< RDPQ special state: render mode update is frozen (see #rdpq_mode_begin) -#define SOMX_AA_REDUCED ((cast64(1))<<34) ///< RDPQ special state: reduced antialiasing is enabled -#define SOMX_LOD_INTERPOLATE ((cast64(1))<<35) ///< RDPQ special state: mimap interpolation (aka trilinear) requested +#define SOMX_LOD_INTERPOLATE ((cast64(1))<<32) ///< RDPQ special state: mimap interpolation (aka trilinear) requested +#define SOMX_LOD_INTERPOLATE_SHQ ((cast64(1))<<33) ///< RDPQ special state: mimap interpolation for SHC texture format +#define SOMX_LOD_INTERP_MASK ((cast64(3))<<32) ///< RDPQ special state: mask for LOD interpolation formulas +#define SOMX_LOD_INTERP_SHIFT 32 ///< RDPQ special state: shift for LOD interpolation formulas +#define SOMX_AA_REDUCED ((cast64(1))<<34) ///< RDPQ special state: reduced antialiasing is enabled +#define SOMX_UPDATE_FREEZE ((cast64(1))<<35) ///< RDPQ special state: render mode update is frozen (see #rdpq_mode_begin) #define SOM_BLEND0_MASK (cast64(0xCCCC0000) | SOM_BLENDING | SOM_READ_ENABLE | SOMX_BLEND_2PASS) ///< Blender: mask of settings related to pass 0 #define SOM_BLEND1_MASK (cast64(0x33330000) | SOM_BLENDING | SOM_READ_ENABLE | SOMX_BLEND_2PASS) ///< Blender: mask of settings related to pass 1 diff --git a/include/rdpq_mat.h b/include/rdpq_mat.h new file mode 100644 index 000000000..8c48d1677 --- /dev/null +++ b/include/rdpq_mat.h @@ -0,0 +1,302 @@ +/** + * @file rdpq_mat.h + * @author Giovanni Bajo + * @brief RDP Command queue: material system + * @ingroup rdpq + * + * The RDP material system provides a high-level API for managing complex rendering + * state by organizing related RDP settings into reusable "materials". A material + * encapsulates textures, combiners, blending modes, and other RDP state that + * define how primitives should be rendered, allowing for efficient switching + * between different visual appearances. + * + * Materials can contain textures, color combiner formulas, blending modes, + * antialiasing settings, fog parameters, Z-buffer configuration, and various + * render mode overrides. They also support an extensible system for storing + * custom key-value pairs, making them suitable for advanced rendering pipelines + * and integration with external tools. + * + * The material system is designed for performance, with efficient binary formats + * for storage and fast lookup mechanisms. Materials are typically created using + * the mkmaterial tool and stored in material database (.mdb) files, which can contain + * multiple materials with fast name-based lookup. + * + * ## Basic Usage + * + * The typical workflow involves: + * 1. Configure the texture database path with #rdpq_mat_set_texture_path. This + * was created via mkmaterial and should normally be stored in the ROM (though + * any valid filesystem can be used, including SD, etc.). + * 2. Open a material database using #rdpq_matdb_open + * 3. Load specific materials with #rdpq_matdb_load, which will also load its + * textures into RDRAM. This should be done when eg a mesh is loaded and + * the material is needed for rendering. + * 4. Apply materials before rendering with #rdpq_mat_draw_begin + * 5. Render your primitives (triangles, sprites, etc.) + * 6. Clean up the material state with #rdpq_mat_draw_end + * 7. Free materials when done with #rdpq_mat_free + * 8. Close the material database with #rdpq_matdb_close when not needed anymore. + * + * ## Source material files + * + * Materials (.mat files) are INI files that describe all the properties of one + * or multiple materials. They can be processed by the mkmaterial tool to + * generate the binary material database (.mdb) files used at runtime. + * An alternative representation in json format (.jmat files) is also supported, + * which is useful when generating materials via a tool (eg: exporting from a 3D editor). + * + * ## Material Database Files + * + * Material databases (.mdb files) are binary files that efficiently store + * multiple materials with metadata for fast loading and lookup. You can use + * a single material database for a whole scene, or just one per model, + * depending on how your asset pipeline is structured. + * + * ## Texture Management + * + * Materials reference textures by hash, allowing the same texture to be shared + * across multiple materials efficiently. Textures are loaded on-demand and + * cached automatically. Configure the texture search path using + * #rdpq_mat_set_texture_path. + * + * ## Extension System + * + * Materials support custom extensions through a key-value system that can + * store integers, floats, strings, and booleans. This allows materials to + * carry application-specific data or parameters for custom shaders and + * rendering techniques. + */ +#ifndef LIBDRAGON_RDPQ_MAT_H +#define LIBDRAGON_RDPQ_MAT_H + +#include +#include +#include "preview.h" +#include "string_hash.h" + +#ifdef __cplusplus +extern "C" { +#endif + +///@cond +typedef struct rdpq_matdb_s rdpq_matdb_t; +typedef void* rdpq_mat_t; + +bool __rdpq_mat_ext_get_bool(rdpq_mat_t *mat, uint32_t ext_key, bool *value); +bool __rdpq_mat_ext_get_int(rdpq_mat_t *mat, uint32_t ext_key, uint32_t *value); +bool __rdpq_mat_ext_get_string(rdpq_mat_t *mat, uint32_t ext_key, char **value); +bool __rdpq_mat_ext_get_float(rdpq_mat_t *mat, uint32_t ext_key, float *value); +///@endcond + +/** + * @brief Open a material DB file and load it into memory + * @preview + * + * This function loads a Material DB file. By default, textures are not + * loaded into RDRAM until requested by the user. This is done to save + * memory when the material database is large. + * + * @param filename Path to the material database file + * @return Pointer to the material database + */ +LIBDRAGON_PREVIEW_API +rdpq_matdb_t* rdpq_matdb_open(const char *filename); + +/** @brief Close a material database + * @preview + */ +LIBDRAGON_PREVIEW_API +void rdpq_matdb_close(rdpq_matdb_t* mdb); + +/** + * @brief Load a material from the material database + * @preview + * + * This function retrieves a material from the material database by its name. + * If the material does not exist, it returns NULL. + * + * Textures associated with the material are also loaded into RDRAM. + * + * Make sure to call #rdpq_mat_free when done with the material, + * + * @param mdb Material database + * @param mat_name Name of the material to retrieve. If NULL, + * the first material in the database is returned. + * @return Pointer to the material, or NULL if not found + */ +LIBDRAGON_PREVIEW_API +rdpq_mat_t* rdpq_matdb_load(rdpq_matdb_t* mdb, const char *mat_name); + +/** + * @brief Load a single material from a raw buffer + * @preview + * + * This function is useful to load a material from its raw binary representation, + * stored outside of a material database. The raw representation is created + * by mkmaterial when using the --raw-material option, and can be used to + * embed the material as part of a larger bundle (eg: a mesh file). + * + * If you use a material database (.mdb) you don't need this function; use + * #rdpq_matdb_load instead. + * + * @param buf Pointer to the raw material buffer + * @param size Size of the buffer + * @return Loaded material + */ +LIBDRAGON_PREVIEW_API +rdpq_mat_t* rdpq_mat_load_buf(void *buf, int size); + +/** + * @brief Activate a material by configuring the RDP state and uploading textures to TMEM + * @preview + * + * This function configures the RDP to prepare drawing primitives (triangles or + * rectangles) with the material specified by @p mat. It sets the RDP state + * to the one specified in the material, and uploads textures to TMEM. + * + * After drawing the primitives with this material, you must call #rdpq_mat_draw_end + * to restore the previous RDP state. In fact, the rdpq material system allows + * for render mode overrides; for instance, a single material might need Z writes + * to be disabled, but then any further material would need to simply revert + * back to the previous state configured for the scene. + * + * @param mat Material to activate + * + * @see #rdpq_mat_draw_end + */ +LIBDRAGON_PREVIEW_API +void rdpq_mat_draw_begin(rdpq_mat_t *mat); + +/** + * @brief Deactivate a material after drawing with it + * @preview + * + * This function ends the use of the specified material and restores the previous + * RDP state. + * + * @param mat Material to deactivate + */ +LIBDRAGON_PREVIEW_API +void rdpq_mat_draw_end(rdpq_mat_t *mat); + +/** + * @brief Get a boolean extension value from the material + * + * This function retrieves a boolean extension value from the + * material, specified by its name. If the name does not exist, it returns false. + * + * Note that the name is case-sensitive, and must be specified without the "ext." + * prefix. So for instance, if the material file contains an extension key called + * "ext.mygame.has_alpha", you should call this function with "mygame.has_alpha". + * + * @param mat Material to query + * @param name Name of the boolean value to retrieve + * @param value Pointer to store the result + * @return true if the name exists, false otherwise + * + * @hideinitializer + */ +#define rdpq_mat_ext_get_bool(mat, name, value) \ + __rdpq_mat_ext_get_bool(mat, string_hash(name) & 0xFFFF, value) + +/** + * @brief Get an integer extension value from the material + * + * This function retrieves an integer extension value from the + * material, specified by its name. If the name does not exist, it returns false. + * + * Note that the name is case-sensitive, and must be specified without the "ext." + * prefix. So for instance, if the material file contains an extension key called + * "ext.mygame.has_alpha", you should call this function with "mygame.has_alpha". + * + * @param mat Material to query + * @param name Name of the boolean value to retrieve + * @param value Pointer to store the result + * @return true if the name exists, false otherwise + * + * @hideinitializer + */ +#define rdpq_mat_ext_get_int(mat, name, value) \ + __rdpq_mat_ext_get_int(mat, string_hash(name) & 0xFFFF, value) + +/** + * @brief Get a string extension value from the material + * + * This function retrieves a string extension value from the + * material, specified by its name. If the name does not exist, it returns false. + * The string is NULL terminated. + * + * Note that the name is case-sensitive, and must be specified without the "ext." + * prefix. So for instance, if the material file contains an extension key called + * "ext.mygame.has_alpha", you should call this function with "mygame.has_alpha". + * + * @param mat Material to query + * @param name Name of the boolean value to retrieve + * @param value Pointer to store the result + * @return true if the name exists, false otherwise + * + * @hideinitializer + */ +#define rdpq_mat_ext_get_string(mat, name, value) \ + __rdpq_mat_ext_get_string(mat, string_hash(name) & 0xFFFF, value) + +/** + * @brief Get a float extension value from the material + * + * This function retrieves a float extension value from the + * material, specified by its name. If the name does not exist, it returns false. + * + * Note that the name is case-sensitive, and must be specified without the "ext." + * prefix. So for instance, if the material file contains an extension key called + * "ext.mygame.has_alpha", you should call this function with "mygame.has_alpha". + * + * @param mat Material to query + * @param name Name of the float value to retrieve + * @param value Pointer to store the result + * @return true if the name exists, false otherwise + * + * @hideinitializer + */ +#define rdpq_mat_ext_get_float(mat, name, value) \ + __rdpq_mat_ext_get_float(mat, string_hash(name) & 0xFFFF, value) + + +/** + * @brief Free a material loaded with #rdpq_matdb_load or #rdpq_mat_load_buf + * @preview + * + * This function releases the textures associated with the material. Notice + * that the material itself is not freed: in fact, it is either part of the + * material database (that will be freed via #rdpq_matdb_close), or + * it is a standalone material loaded with #rdpq_mat_load_buf and the + * caller will be responsible for freeing the memory. + * + * @param mat Material to free + */ +LIBDRAGON_PREVIEW_API +void rdpq_mat_free(rdpq_mat_t *mat); + + +/** + * @brief Set the path in which the texture DB is stored + * @preview + * + * The texture database is a directory in an accessible filesystem + * (normally, in ROM) where all the textures using the rdpq material + * system are stored. The database is created by mkmaterial while + * processing materials. + * + * This function informs the rdpq_mat library where the database + * can be accessed. + * + * @param path Path to the texture database directory, + * including filesystem prefix (eg: "rom:/textures") + */ +LIBDRAGON_PREVIEW_API +void rdpq_mat_set_texture_path(const char *path); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/rdpq_mode.h b/include/rdpq_mode.h index f45ea0485..4c48b5aa8 100644 --- a/include/rdpq_mode.h +++ b/include/rdpq_mode.h @@ -1,5 +1,6 @@ /** * @file rdpq_mode.h + * @author Giovanni Bajo * @author Dennis Heinze * @brief RDP Command queue: mode setting * @ingroup rdpq @@ -92,6 +93,7 @@ #ifndef LIBDRAGON_RDPQ_MODE_H #define LIBDRAGON_RDPQ_MODE_H +#include "preview.h" #include "rdpq.h" #include @@ -251,6 +253,7 @@ typedef enum rdpq_mipmap_s { MIPMAP_INTERPOLATE = (SOM_TEXTURE_LOD | SOMX_LOD_INTERPOLATE) >> 32, ///< Interpolate between the two nearest mipmap levels (also known as "trilinear") MIPMAP_INTERPOLATE_SHARPEN = (SOM_TEXTURE_LOD | SOMX_LOD_INTERPOLATE | SOM_TEXTURE_SHARPEN) >> 32, ///< Interpolate between the two nearest mipmap levels (also known as "trilinear") with sharpening enabled MIPMAP_INTERPOLATE_DETAIL = (SOM_TEXTURE_LOD | SOMX_LOD_INTERPOLATE | SOM_TEXTURE_DETAIL) >> 32, ///< Interpolate between the two nearest mipmap levels (also known as "trilinear") with detail texture enabled + MIPMAP_INTERPOLATE_SHQ = (SOMX_LOD_INTERPOLATE_SHQ) >> 32, ///< Special mipmap mode that must be used for SHC textures } rdpq_mipmap_t; /** @@ -262,6 +265,17 @@ typedef enum rdpq_antialias_s { AA_REDUCED = 2, ///< Reduced antialiasing } rdpq_antialias_t; +/** + * @brief Types of Z-buffering modes supported by RDP + * + * See #rdpq_mode_zmode for more information. + */ +typedef enum rdpq_zmode_s { + ZMODE_STANDARD = 0, ///< Standard Z-buffer mode + ZMODE_INTERPENETRATING = 1, ///< Z-buffer mode for interpenetrating surfaces + ZMODE_DECAL = 3, ///< Z-buffer mode for decal surfaces +} rdpq_zmode_t; + /** * @name Render modes @@ -374,7 +388,7 @@ void rdpq_set_mode_yuv(bool bilinear); * #FILTERS_RESAMPLE_ANTIALIAS_DEDITHER to #display_init. * * On the other hand, if you want to make sure that no antialias is performed, - * disable antialias with `rdpq_mode_antialias(false)` (which is the default + * disable antialias with `rdpq_mode_antialias(AA_NONE)` (which is the default * for #rdpq_set_mode_standard), and that will make sure that the VI will not * do anything to the image, even if #display_init was called with * #FILTERS_RESAMPLE_ANTIALIAS or #FILTERS_RESAMPLE_ANTIALIAS_DEDITHER. @@ -551,8 +565,8 @@ inline void rdpq_mode_combiner(rdpq_combiner_t comb) { * use #RDPQ_BLENDER_MULTIPLY_CONST. * * #RDPQ_BLENDER_ADDITIVE is mostly broken on RDP, as it doesn't handle correctly - * overflowing values. Basically, values up to 1.5 are correctly clamped to 1, - * but values above 1.5 are wrapped back to 0, which makes the mode almost useless. + * overflowing values. Basically, values that overflow 1.0 are not clamped but + * wrap back to 0, which makes the mode almost useless. * * It is possible to also create custom formulas. The blender unit * allows for up to two passes. Use #RDPQ_BLENDER to create a one-pass @@ -696,12 +710,12 @@ inline void rdpq_mode_dithering(rdpq_dither_t dither) { */ inline void rdpq_mode_alphacompare(int threshold) { if (threshold == 0) { - __rdpq_mode_change_som(SOM_ALPHACOMPARE_MASK, 0); + __rdpq_mode_change_som(SOM_ALPHACOMPARE_MASK | SOMX_ALPHACOMPARE, 0); } else if (threshold > 0) { - __rdpq_mode_change_som(SOM_ALPHACOMPARE_MASK, SOM_ALPHACOMPARE_THRESHOLD); + __rdpq_mode_change_som(SOM_ALPHACOMPARE_MASK | SOMX_ALPHACOMPARE, SOM_ALPHACOMPARE_THRESHOLD | SOMX_ALPHACOMPARE); rdpq_set_blend_color(RGBA32(0,0,0,(uint8_t)threshold)); } else { - __rdpq_mode_change_som(SOM_ALPHACOMPARE_MASK, SOM_ALPHACOMPARE_NOISE); + __rdpq_mode_change_som(SOM_ALPHACOMPARE_MASK | SOMX_ALPHACOMPARE, SOM_ALPHACOMPARE_NOISE | SOMX_ALPHACOMPARE); } } @@ -752,6 +766,31 @@ inline void rdpq_mode_zoverride(bool enable, float z, int16_t deltaz) { ); } +/** + * @brief Configure the Z buffering mode + * @preview + * + * This function allows to tune the internal Z buffer formula to obtain several + * different effects. In addition to the standard operating mode (#ZMODE_STANDARD), + * there are two special modes that can be activated: + * + * * #ZMODE_DECAL: this mode can be used to draw polygons that are coplanar with + * already drawn polygons, normally called "decals". NOTE: this will never + * be bulletproof. If you still get some Z-fighting flickering in this mode, + * try to subdivide the background polygons so that they share vertices + * exactly with the decal. + * * #ZMODE_INTERPENETRATING: this mode can be used to reduce z-fighting when + * two objects intersect each other, and anti-aliasing is enabled. A common + * case can be objects like trees positioned slightly under the terrain. + * + * @param mode Z-buffering mode to use + * + * @see #rdpq_zmode_t + */ +LIBDRAGON_PREVIEW_API +inline void rdpq_mode_zmode(rdpq_zmode_t mode) { + __rdpq_mode_change_som(SOM_ZMODE_MASK, (uint64_t)mode << SOM_ZMODE_SHIFT); +} /** * @brief Activate palette lookup during drawing @@ -819,7 +858,7 @@ inline void rdpq_mode_mipmap(rdpq_mipmap_t mode, int num_levels) { num_levels = 0; if (num_levels) num_levels -= 1; - __rdpq_mode_change_som(SOM_TEXTURE_LOD | SOMX_LOD_INTERPOLATE | SOMX_NUMLODS_MASK | SOM_TEXTURE_SHARPEN | SOM_TEXTURE_DETAIL, + __rdpq_mode_change_som(SOM_TEXTURE_LOD | SOMX_LOD_INTERP_MASK | SOMX_NUMLODS_MASK | SOM_TEXTURE_SHARPEN | SOM_TEXTURE_DETAIL, ((uint64_t)mode << 32) | ((uint64_t)num_levels << SOMX_NUMLODS_SHIFT)); }; diff --git a/include/rdpq_rect.h b/include/rdpq_rect.h index 956f8ed4f..7c678199c 100644 --- a/include/rdpq_rect.h +++ b/include/rdpq_rect.h @@ -252,6 +252,22 @@ inline void __rdpq_texture_rectangle_flip_raw_fx(rdpq_tile_t tile, uint16_t x0, __rdpq_fill_rectangle_fx((x0)*4, (y0)*4, (x1)*4, (y1)*4); \ }) +/** + * @brief Draw a pixel (RDP command: FILL_RECTANGLE) + * + * This function is a convenience macro to draw a single pixel on the screen, + * by using the fill rectangle command. + * + * Drawing a single pixel is a relatively inefficient operation; sometimes you + * need this so the function is provided, but do not expect to be able to draw + * thousands of single pixels without performance issues. + * + * See #rdpq_fill_rectangle for more information on how to configure the + * color of the pixel. + */ +#define rdpq_draw_pixel(x, y) \ + rdpq_fill_rectangle((x), (y), (x)+1, (y)+1) + /** * @brief Draw a textured rectangle (RDP command: TEXTURE_RECTANGLE) * diff --git a/include/rdpq_tex.h b/include/rdpq_tex.h index 538b7f9de..6e0959f60 100644 --- a/include/rdpq_tex.h +++ b/include/rdpq_tex.h @@ -8,6 +8,7 @@ #ifndef LIBDRAGON_RDPQ_TEX_H #define LIBDRAGON_RDPQ_TEX_H +#include "preview.h" #include "rdpq.h" #include @@ -39,7 +40,7 @@ extern "C" { */ typedef struct rdpq_texparms_s { int tmem_addr; ///< TMEM address where to load the texture (default: 0) - int palette; ///< Palette number where TLUT is stored (used only for CI4 textures) + int palette; ///< Palette number where TLUT is stored (used only for CI4 textures, or Ci8 textures with less than 256 colors) struct { float translate; ///< Translation of the texture (in pixels) @@ -107,6 +108,10 @@ int tex_loader_calc_max_height(tex_loader_t *tload, int s0, int s1); * use #rdpq_tex_upload_sub, or alternatively create a sub-surface using * #surface_make_sub and pass it to #rdpq_tex_upload. See #rdpq_tex_upload_sub * for an example of both techniques. + * + * @note This function might corrupt the next tile descriptor (tile+1) to + * speed up the upload. Loading tile descriptors in increasing order + * will avoid issues. * * @param tile Tile descriptor that will be initialized with this texture * @param tex Surface containing the texture to load @@ -168,6 +173,9 @@ int rdpq_tex_upload(rdpq_tile_t tile, const surface_t *tex, const rdpq_texparms_ * be 8-byte aligned (like all RDP textures), so it can only be used if the * rectangle that needs to be loaded respects such constraint as well. * + * @note This function might corrupt the next tile descriptor (tile+1) to + * speed up the upload. Loading tile descriptors in increasing order + * will avoid issues. * * @param tile Tile descriptor that will be initialized with this texture * @param tex Surface containing the texture to load @@ -176,13 +184,27 @@ int rdpq_tex_upload(rdpq_tile_t tile, const surface_t *tex, const rdpq_texparms_ * @param t0 Top-left Y coordinate of the rectangle to load * @param s1 Bottom-right *exclusive* X coordinate of the rectangle * @param t1 Bottom-right *exclusive* Y coordinate of the rectangle - * @return int Number of bytes used in TMEM for this texture + * @return Number of bytes used in TMEM for this texture * * @see #rdpq_tex_upload * @see #surface_make_sub */ int rdpq_tex_upload_sub(rdpq_tile_t tile, const surface_t *tex, const rdpq_texparms_t *parms, int s0, int t0, int s1, int t1); +/** + * @brief Check if a surface can be fully uploaded to TMEM + * @preview + * + * This helper verifies whether a full-surface upload via #rdpq_tex_upload + * would fit in TMEM for the surface format. + * + * @param tex Surface to check + * @return true The surface fits in TMEM + * @return false The surface does not fit in TMEM + */ +LIBDRAGON_PREVIEW_API +bool rdpq_tex_can_upload(const surface_t *tex); + /** * @brief Load one or more palettes into TMEM * @@ -228,7 +250,7 @@ void rdpq_tex_upload_tlut(uint16_t *tlut, int color_idx, int num_colors); * @param t0 Top-left Y coordinate of the rectangle to reuse * @param s1 Bottom-right *exclusive* X coordinate of the rectangle * @param t1 Bottom-right *exclusive* Y coordinate of the rectangle - * @return int Number of bytes used in TMEM for this texture (always 0) + * @return Number of bytes used in TMEM for this texture (always 0) */ int rdpq_tex_reuse_sub(rdpq_tile_t tile, const rdpq_texparms_t *parms, int s0, int t0, int s1, int t1); @@ -247,7 +269,7 @@ int rdpq_tex_reuse_sub(rdpq_tile_t tile, const rdpq_texparms_t *parms, int s0, i * * @param tile Tile descriptor that will be initialized with reused texture * @param parms All optional parameters on how to sample reused texture. Refer to #rdpq_texparms_t for more information. - * @return int Number of bytes used in TMEM for this texture (always 0) + * @return Number of bytes used in TMEM for this texture (always 0) */ int rdpq_tex_reuse(rdpq_tile_t tile, const rdpq_texparms_t *parms); @@ -315,6 +337,9 @@ typedef struct rdpq_blitparms_s { float scale_y; ///< Vertical scale factor to apply to the surface. This scaling is applied along the Y axis before rotation. If 0, no scaling is performed (the same as 1.0f). If negative, vertical flipping is applied float theta; ///< Counter-clockwise rotation angle in radians + LIBDRAGON_PREVIEW_SYM + bool allow_xform; ///< True if blit should be affected by transforms applied by rdpq_xform @preview + // FIXME: replace this with CPU tracking of filtering mode? bool filtering; ///< True if texture filtering is enabled (activates workaround for filtering artifacts when splitting textures in chunks) } rdpq_blitparms_t; diff --git a/include/rdpq_text.h b/include/rdpq_text.h index d473b3bc7..990c67753 100644 --- a/include/rdpq_text.h +++ b/include/rdpq_text.h @@ -273,7 +273,7 @@ void rdpq_text_unregister_font(uint8_t font_id); * @brief Get a registered font by its ID. * * @param font_id Font ID - * @return const rdpq_font_t* Registered font or NULL + * @return Registered font or NULL */ const rdpq_font_t *rdpq_text_get_font(uint8_t font_id); @@ -322,7 +322,7 @@ const rdpq_font_t *rdpq_text_get_font(uint8_t font_id); * @param utf8_text Text to render, in UTF-8 encoding. Does not need to be * NULL terminated. * @param nbytes Number of bytes in the text to render - * @return int Number of bytes printed + * @return Number of bytes printed * * @see #rdpq_text_printf * @see #rdpq_text_print @@ -344,7 +344,7 @@ rdpq_textmetrics_t rdpq_text_printn(const rdpq_textparms_t *parms, uint8_t font_ * @param x0 X coordinate where to start rendering the text * @param y0 Y coordinate where to start rendering the text * @param utf8_fmt Format string, in UTF-8 encoding - * @return int Number of bytes printed + * @return Number of bytes printed */ __attribute__((format(printf, 5, 6))) rdpq_textmetrics_t rdpq_text_printf(const rdpq_textparms_t *parms, uint8_t font_id, float x0, float y0, @@ -366,7 +366,7 @@ rdpq_textmetrics_t rdpq_text_printf(const rdpq_textparms_t *parms, uint8_t font_ * @param y0 Y coordinate where to start rendering the text * @param utf8_fmt Format string, in UTF-8 encoding * @param va Argument list - * @return rdpq_textmetrics_t Metrics of the text + * @return Metrics of the text */ rdpq_textmetrics_t rdpq_text_vprintf(const rdpq_textparms_t *parms, uint8_t font_id, float x0, float y0, const char *utf8_fmt, va_list va); @@ -384,7 +384,7 @@ rdpq_textmetrics_t rdpq_text_vprintf(const rdpq_textparms_t *parms, uint8_t font * @param x0 X coordinate where to start rendering the text * @param y0 Y coordinate where to start rendering the text * @param utf8_text Text to render, in UTF-8 encoding, NULL terminated. - * @return int Number of bytes printed + * @return Number of bytes printed */ inline rdpq_textmetrics_t rdpq_text_print(const rdpq_textparms_t *parms, uint8_t font_id, float x0, float y0, const char *utf8_text) diff --git a/include/rdpq_xform.h b/include/rdpq_xform.h new file mode 100644 index 000000000..ee5dab1ec --- /dev/null +++ b/include/rdpq_xform.h @@ -0,0 +1,189 @@ +/** + * @file rdpq_xform.h + * @brief RDP Command queue: 2D transformation API. + * @ingroup rdpq + * + * This file contains functions for 2D transformations and transformed primitives. + */ + +#ifndef LIBDRAGON_RDPQ_XFORM_H +#define LIBDRAGON_RDPQ_XFORM_H + +#include +#include "preview.h" +#include "fgeom2d.h" +#include "rdpq_tri.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Replace current transformation matrix with another 3x3 matrix. + * @preview + * + * @param mtx Pointer to 3x3 matrix to load + */ +LIBDRAGON_PREVIEW_API +void rdpq_xform_mtx_load(fm_mat3_t *mtx); +/** + * @brief Multiply current transformation matrix by another 3x3 matrix. + * @preview + * + * @param mtx Pointer to 3x3 matrix to multiply by + */ +LIBDRAGON_PREVIEW_API +void rdpq_xform_mtx_mult(fm_mat3_t *mtx); + +/** + * @brief Replace current transformation matrix with the identity matrix. + * @preview + * + * This clears any transformations that may have been applied. + */ +LIBDRAGON_PREVIEW_API +void rdpq_xform_identity(void); +/** + * @brief Multiplies current transformation matrix by rotation matrix. + * @preview + * + * @param theta Counterclockwise angle to rotate by in radians + */ +LIBDRAGON_PREVIEW_API +void rdpq_xform_rotate(float theta); +/** + * @brief Multiplies current transformation matrix by translation matrix. + * @preview + * + * @param x X component of translation vector + * @param y Y component of translation vector + */ +LIBDRAGON_PREVIEW_API +void rdpq_xform_translate(float x, float y); +/** + * @brief Multiplies current transformation matrix by scaling matrix. + * @preview + * + * @param x Scaling factor along X axis + * @param y Scaling factor along Y axis + */ +LIBDRAGON_PREVIEW_API +void rdpq_xform_scale(float x, float y); +/** + * @brief Push current matrix to matrix stack. + * @preview + */ +LIBDRAGON_PREVIEW_API +void rdpq_xform_push(void); +/** + * @brief Pop from matrix stack. + * @preview + * + * This replaces the current matrix with the matrix one below it on the stack. + */ +LIBDRAGON_PREVIEW_API +void rdpq_xform_pop(void); + +/** + * @brief Replace current transformation matrix with a combined scale, rotation and translation matrix + * @preview + * + * It is equivalent to loading an identity matrix followed by multiplying + * by a scale, a rotation, and a translation matrix in that order. + * + * @param x X component of translation vector + * @param y Y component of translation vector + * @param theta Counterclockwise angle to rotate by in radians + * @param scale_x Scaling factor along X axis + * @param scale_y Scaling factor along Y axis + */ +LIBDRAGON_PREVIEW_API +void rdpq_xform_load_srt(float x, float y, float theta, float scale_x, float scale_y); +/** + * @brief Multiply current transformation matrix by a combined scale, rotation and translation matrix + * @preview + * + * It is equivalent to multiplying by a scale, a rotation, and a translation matrix in that order. + * + * @param x X component of translation vector + * @param y Y component of translation vector + * @param theta Counterclockwise angle to rotate by in radians + * @param scale_x Scaling factor along X axis + * @param scale_y Scaling factor along Y axis + */ +LIBDRAGON_PREVIEW_API +void rdpq_xform_mult_srt(float x, float y, float theta, float scale_x, float scale_y); + +/** + * @brief Replace current transformation matrix with a combined rotation, scale and translation matrix + * @preview + * + * It is equivalent to loading an identity matrix followed by multiplying + * by a rotation, a scale, and a translation matrix in that order. + * + * @param x X component of translation vector + * @param y Y component of translation vector + * @param theta Counterclockwise angle to rotate by in radians + * @param scale_x Scaling factor along X axis + * @param scale_y Scaling factor along Y axis + */ +LIBDRAGON_PREVIEW_API +void rdpq_xform_load_rst(float x, float y, float theta, float scale_x, float scale_y); +/** + * @brief Multiply current transformation matrix by a combined rotation, scale and translation matrix + * @preview + * + * It is equivalent to multiplying by a rotation, a scale, and a translation matrix in that order. + * + * @param x X component of translation vector + * @param y Y component of translation vector + * @param theta Counterclockwise angle to rotate by in radians + * @param scale_x Scaling factor along X axis + * @param scale_y Scaling factor along Y axis + */ +LIBDRAGON_PREVIEW_API +void rdpq_xform_mult_rst(float x, float y, float theta, float scale_x, float scale_y); + +/** + * @brief Draw a filled rectangle with the current transformation applied. + * @preview + * + * This function works as a replacement for #rdpq_fill_rectangle in contexts + * where a transformation should be applied. +*/ +LIBDRAGON_PREVIEW_API +void rdpq_xform_fill_rectangle(float x0, float y0, float x1, float y1); + +/** + * @brief Draw a filled rectangle with the current transformation applied. + * @preview + * + * This function works as a replacement for #rdpq_texture_rectangle in contexts + * where a transformation should be applied. +*/ +LIBDRAGON_PREVIEW_API +void rdpq_xform_texture_rectangle(rdpq_tile_t tile, float x0, float y0, float x1, float y1, float s, float t); +/** + * @brief Draw a textured rectangle with the current transformation applied and scaling applied. + * @preview + * + * This function works as a replacement for #rdpq_texture_rectangle_scaled in contexts + * where a transformation should be applied. +*/ +LIBDRAGON_PREVIEW_API +void rdpq_xform_texture_rectangle_scaled(rdpq_tile_t tile, float x0, float y0, float x1, float y1, float s0, float t0, float s1, float t1); +/** + * @brief Draw a triangle with the current transformation applied. + * @preview + * + * This function works as a replacement for #rdpq_triangle in contexts + * where a transformation should be applied. +*/ +LIBDRAGON_PREVIEW_API +void rdpq_xform_triangle(const rdpq_trifmt_t *fmt, const float *v1, const float *v2, const float *v3); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/rsp.h b/include/rsp.h index 407559cd0..10696cb60 100644 --- a/include/rsp.h +++ b/include/rsp.h @@ -285,6 +285,16 @@ typedef struct { uint8_t *data; ///< Pointer to the data segment void *data_end; ///< Pointer past the end of the data segment + /** + * @brief Pointer to the meta segment. + * + * This is a special data segment which may contain arbitrary user-defined data. + * All sections with the prefix ".meta" are automatically placed here by the linker script. + * The contents are never loaded into DMEM or IMEM. + */ + uint8_t *meta; + void *meta_end; ///< Pointer past the end of the meta segment + const char *name; ///< Name of the ucode uint32_t start_pc; ///< Initial RSP PC @@ -347,13 +357,17 @@ typedef struct { #define DEFINE_RSP_UCODE(ucode_name, ...) \ extern uint8_t ucode_name ## _text_start[]; \ extern uint8_t ucode_name ## _data_start[]; \ + extern uint8_t ucode_name ## _meta_start[]; \ extern uint8_t ucode_name ## _text_end[0]; \ extern uint8_t ucode_name ## _data_end[0]; \ + extern uint8_t ucode_name ## _meta_end[0]; \ rsp_ucode_t ucode_name = (rsp_ucode_t){ \ .code = ucode_name ## _text_start, \ - .data = ucode_name ## _data_start, \ .code_end = ucode_name ## _text_end, \ + .data = ucode_name ## _data_start, \ .data_end = ucode_name ## _data_end, \ + .meta = ucode_name ## _meta_start, \ + .meta_end = ucode_name ## _meta_end, \ .name = #ucode_name, .start_pc = 0, \ .crash_handler = 0, .assert_handler = 0, \ __VA_ARGS__ \ diff --git a/include/rsp.inc b/include/rsp.inc index 3625f58ff..043e28b7a 100644 --- a/include/rsp.inc +++ b/include/rsp.inc @@ -11,6 +11,7 @@ # MIPS general register definitions #include +#include "emux.inc" ###############################################################################¶ # libdragon MIPS conventions for ucode. @@ -1159,6 +1160,8 @@ makeMxc2Op mfc2, 0x0 # "vsrl8 , , " to perform a logical right shift from 8 to 15 bits # "vsra , , " to perform an arithmetic right shift up to 7 bits # "vsra8 , , " to perform a arithmetic right shift from 8 to 15 bits +# "vdsrl , , , " to perform a double-precision right shift up to 8 bits +# "vdsrl8 , , , " to perform a double-precision right shift from 8 to 15 bits # .macro vsll_data .align 4 @@ -1226,6 +1229,37 @@ makeMxc2Op mfc2, 0x0 .endif .endm + .macro vdsrl8 vdstreg, vloreg, vmdreg, qty + .if (\qty == 9) + vmudl \vdstreg, \vloreg, \vshiftreg, e(0) + vmadn \vdstreg, \vmdreg, \vshiftreg, e(0) + .elseif (\qty == 10) + vmudl \vdstreg, \vloreg, \vshiftreg, e(1) + vmadn \vdstreg, \vmdreg, \vshiftreg, e(1) + .elseif (\qty == 11) + vmudl \vdstreg, \vloreg, \vshiftreg, e(2) + vmadn \vdstreg, \vmdreg, \vshiftreg, e(2) + .elseif (\qty == 12) + vmudl \vdstreg, \vloreg, \vshiftreg, e(3) + vmadn \vdstreg, \vmdreg, \vshiftreg, e(3) + .elseif (\qty == 13) + vmudl \vdstreg, \vloreg, \vshiftreg, e(4) + vmadn \vdstreg, \vmdreg, \vshiftreg, e(4) + .elseif (\qty == 14) + vmudl \vdstreg, \vloreg, \vshiftreg, e(5) + vmadn \vdstreg, \vmdreg, \vshiftreg, e(5) + .elseif (\qty == 15) + vmudl \vdstreg, \vloreg, \vshiftreg, e(6) + vmadn \vdstreg, \vmdreg, \vshiftreg, e(6) + .elseif (\qty >= 2 && \qty <= 8) + .error "Use vdsrl for quantities in range 2-8" + .elseif (\qty == 1) + .error "vdsrl8 does not support quantity 1" + .else + .error "Invalid quantity in vdsrl8" + .endif + .endm + .macro vsra8 vdstreg, vsrcreg, qty .if (\qty == 9) vmudm \vdstreg, \vsrcreg, \vshiftreg, e(0) @@ -1318,6 +1352,37 @@ makeMxc2Op mfc2, 0x0 .endif .endm + .macro vdsrl vdstreg, vloreg, vmdreg, qty + .if (\qty == 2) + vmudl \vdstreg, \vloreg, \vshiftreg, e(1) + vmadn \vdstreg, \vmdreg, \vshiftreg, e(1) + .elseif (\qty == 3) + vmudl \vdstreg, \vloreg, \vshiftreg, e(2) + vmadn \vdstreg, \vmdreg, \vshiftreg, e(2) + .elseif (\qty == 4) + vmudl \vdstreg, \vloreg, \vshiftreg, e(3) + vmadn \vdstreg, \vmdreg, \vshiftreg, e(3) + .elseif (\qty == 5) + vmudl \vdstreg, \vloreg, \vshiftreg, e(4) + vmadn \vdstreg, \vmdreg, \vshiftreg, e(4) + .elseif (\qty == 6) + vmudl \vdstreg, \vloreg, \vshiftreg, e(5) + vmadn \vdstreg, \vmdreg, \vshiftreg, e(5) + .elseif (\qty == 7) + vmudl \vdstreg, \vloreg, \vshiftreg, e(6) + vmadn \vdstreg, \vmdreg, \vshiftreg, e(6) + .elseif (\qty == 8) + vmudl \vdstreg, \vloreg, \vshiftreg, e(7) + vmadn \vdstreg, \vmdreg, \vshiftreg, e(7) + .elseif (\qty == 1) + .error "vdsrl does not support quantity 1" + .elseif (\qty >= 9 && \qty <= 15) + .error "Use vdsrl8 for quantities in range 9-15" + .else + .error "Invalid quantity in vdsrl" + .endif + .endm + .macro vsra vdstreg, vsrcreg, qty .if (\qty == 1) vmudm \vdstreg, \vsrcreg, \vshiftreg, e(0) diff --git a/include/rsp_highpri.inc b/include/rsp_highpri.inc new file mode 100644 index 000000000..0568fdfcc --- /dev/null +++ b/include/rsp_highpri.inc @@ -0,0 +1,34 @@ +######################################################## +# Include this file wherever you prefer in your text segment +# +# Authors: Giovanni Bajo +######################################################## + + ############################################################ + # RSPQ_CheckHighpri + # + # Polling function. Check whether the highpri mode has been + # requested by the CPU, and if so start executing it right away. + # + # This is called by the main loop automatically between each + # command, but can be also polled by any overlay function + # that takes a long time and want to yield. In this case, + # the same command will be executed again when the highpri queue + # is finished, so make sure there is state to continue rather + # than restart the execution. + # + # ARGS: + # t0: size of the current command + ############################################################ + + .func RSPQ_CheckHighpri +RSPQ_CheckHighpri: + # We need to enter high-pri mode if highpri was requested and it is not + # already running (to avoid infinite loops). + mfc0 t2, COP0_SP_STATUS + andi t2, SP_STATUS_SIG_HIGHPRI_REQUESTED | SP_STATUS_SIG_HIGHPRI_RUNNING + beq t2, SP_STATUS_SIG_HIGHPRI_REQUESTED, JrRa + nop + j rspq_switch_highpri + sub rspq_dmem_buf_ptr, t0 + .endfunc diff --git a/include/rsp_magma.inc b/include/rsp_magma.inc new file mode 100644 index 000000000..c6923bb45 --- /dev/null +++ b/include/rsp_magma.inc @@ -0,0 +1,458 @@ +################################################## +# RSP_MAGMA.INC - Include file for magma vertex shaders +# +# Authors: Dennis Heinze +################################################## +# +# HOW TO WRITE A VERTEX SHADER: +# +# 1. Put `#include ` at the top of your file. +# 2. Define the list of uniforms: +# * All uniforms must be placed between MgBeginShaderUniforms and MgEndShaderUniforms. +# * Begin each uniform declaration with MgBeginUniform, specifying a label and the binding number. +# * Place data directives within the uniform. +# * End a uniform declaration with MgEndUniform. +# * Only uniform declarations are permitted between MgBeginShaderUniforms and MgEndShaderUniforms. +# * Alignment of uniforms is handled automatically. +# 3. Define the list of vertex attributes: +# * All attributes must be placed between MgBeginVertexInput and MgEndVertexInput. +# * Begin each attribute declaration with MgBeginVertexAttribute, specifying the input number and whether it is optional. +# * Specify the list of attribute loaders with MgVertexAttributeLoaders. +# * Optionally specify a list of patches with MgBeginVertexAttributePatch and MgEndVertexAttributePatch. +# * End an attribute declaration with MgEndVertexAttribute. +# * MgVertexAttributeLoaders and MgBeginVertexAttributePatch may only be specified once per attribute. +# * Between MgBeginVertexAttributePatch and MgEndVertexAttributePatch must be exactly one instruction. +# * Only attribute declarations are permitted between MgBeginVertexInput and MgEndVertexInput. +# 4. Write the shader code: +# * All shader code must be put between MgBeginShader and MgEndShader. +# * No other code outside of those directives is permitted. +# * Shader code may perform any arbitrary computations, but is expected to load the specified input vertices, +# process them, and write the processed vertices into the vertex cache at the specified location. +# * See MgCmd_LoadVertices below for how the input arguments are laid out. +# * The shader code itself is not called in a loop. It is invoked once per vertex load command. +# The vertex processing loop must be written manually. +# * When all vertices have been processed, jump back to RSPQ as you would in any command. +# * You may define custom functions, but they must be placed between MgBeginShader and MgEndShader as well. +# * The scalar register in attribute loaders is assumed to point to the start of a vertex. The offset +# will then be automatically patched so that the labeled instruction loads from the correct offset. +# * Patches will simply replace the labeled instruction with the instruction specified between MgBeginVertexAttributePatch and MgEndVertexAttributePatch. +# +# Put together, here is an example of a vertex shader file: +# +# #include +# MgBeginShaderUniforms +# MgBeginUniform UNIFORMLABEL0, 0 +# ... +# MgEndUniform +# MgBeginUniform UNIFORMLABEL1, 1 +# ... +# MgEndUniform +# ... +# MgEndShaderUniforms +# +# MgBeginVertexInput +# MgBeginVertexAttribute 0, 0 +# MgVertexAttributeLoaders LOADER0, LOADER1 +# MgEndVertexAttribute +# MgBeginVertexAttribute 1, 1 +# MgVertexAttributeLoaders LOADER2, LOADER3 +# MgBeginVertexAttributePatch PATCH0 +# vnop +# MgEndVertexAttributePatch +# MgEndVertexAttribute +# ... +# MgEndVertexInput +# +# MgBeginShader +# ... +# LOADER0: ldv $v01.e0, 0,s0 +# LOADER1: ldv $v01.e4, 0,s1 +# ... +# PATCH0: vmulf $v03, $v01, $v20 +# ... +# MgEndShader +# +################################################## + +#include +#include + +#define TRIANGLE_SIZE 0xB0 + +.macro MgBeginShaderUniforms + .section .meta.mg_uniforms +_MG_META_UNIFORMS: + .equ _mg_uniform_count, 0 + .data + + RSPQ_BeginSavedState + + .align 3 + MG_VERTEX_CACHE: .dcb.b MG_VERTEX_CACHE_COUNT * MG_VTX_SIZE + MG_VERTEX_CACHE_END: .dcb.b MG_VTX_SIZE # Padding for overflow in case of odd-numbered load count + + .align 4 +MG_STATE: + _MgState 1 + + .align 3 +_MG_VTX_SHADER_UNIFORMS: +.endm + +.macro MgEndShaderUniforms + .align 3 +_MG_VTX_SHADER_UNIFORMS_END: + RSPQ_EndSavedState +.endm + +.equ _mg_defining_uniform, 0 + +.macro MgBeginUniform name binding + .ifne _mg_defining_uniform + .error "MgBeginUniform must not be preceded by another MgBeginUniform" + .exitm + .endif + .equ _mg_defining_uniform, 1 + + .equ _mg_uniform_count, _mg_uniform_count+1 + + .section .meta.mg_uniforms + .long \binding + .long \name - _MG_VTX_SHADER_UNIFORMS + .data + .align 3 +\name: +.endm + +.macro MgEndUniform + .ifeq _mg_defining_uniform + .error "MgEndUniform must be preceded by MgBeginUniform" + .exitm + .endif + .equ _mg_defining_uniform, 0 + + .section .meta.mg_uniforms + .long _MG_UNIFORM_END\@ - _MG_VTX_SHADER_UNIFORMS + .data + .align 3 +_MG_UNIFORM_END\@ : +.endm + +# Defines the uniform region using the maximum allowable size, but filled with zeroes. +# This is needed to make space for uniforms once in the initial overlay. +# Whenever shaders are loaded, they will be reading/writing from that area automatically. +.macro _MgDefineUniformRegion + MgBeginShaderUniforms + .fill MG_MAX_UNIFORMS_SIZE + MgEndShaderUniforms +.endm + +.macro MgBeginVertexInput + .section .meta.mg_attributes +_MG_META_ATTRIBUTES: + .equ _mg_attribute_count, 0 +.endm + +.macro MgEndVertexInput +.endm + +.macro MgBeginVertexAttribute input is_optional=0 + .equ _mg_attribute_count, _mg_attribute_count + 1 + + .equ _mg_attribute_loader_count, 0 + .equ _mg_attribute_patches_count, 0 + + .subsection 0 + .long \input + .long \is_optional + .long _MG_ATTRIBUTE_LOADERS\input - _MG_META_ATTRIBUTES + .long _MG_ATTRIBUTE_PATCHES\input - _MG_META_ATTRIBUTES + + .subsection 1 +_MG_ATTRIBUTE_LOADERS\input: + + .subsection 2 +_MG_ATTRIBUTE_PATCHES\input: +.endm + +.macro MgEndVertexAttribute + .subsection 0 + .long _mg_attribute_loader_count + .long _mg_attribute_patches_count +.endm + +.macro _MgAttrLoader first rest:vararg + .ifnb \first + .equ _mg_attribute_loader_count, _mg_attribute_loader_count + 1 + .long \first - _MG_OVERLAY + _MgAttrLoader \rest + .endif +.endm + +.macro MgVertexAttributeLoaders loaders:vararg + .subsection 1 + _MgAttrLoader \loaders +.endm + +.macro MgBeginVertexAttributePatch address + .subsection 2 + .equ _mg_attribute_patches_count, _mg_attribute_patches_count + 1 + .long \address - _MG_OVERLAY +.endm + +.macro MgEndVertexAttributePatch +.endm + +.macro MgBeginShader + .section .meta +_MG_META_START: + .long _mg_uniform_count + .long _MG_META_UNIFORMS - _MG_META_START + .long _mg_attribute_count + .long _MG_META_ATTRIBUTES - _MG_META_START + _MgBeginShader +.endm + +.macro _MgBeginShader + .section .text.mg_overlay + .align 3 +_MG_OVERLAY: + +_MG_CLIPPING: + # Jump to the actual bootstrapper. + # This piece of code is kept constant, so that _MG_VTX_SHADER is always at the same address. + j Mg_Clipping + nop + +_MG_VTX_SHADER: +.endm + +.macro MgEndShader + # Place the clipping code bootstrapper in each shader so it can get loaded when required. +Mg_Clipping: + # Bootstrap the clipping code. + lw s0, %lo(MG_CLIPPING_TEXT) + lw t0, %lo(MG_CLIPPING_TEXT_SIZE) + jal_and_j Mg_LoadOverlay, _MG_CLIPPING +.endm + +# Implementation of the shader code bootstrapper +.macro _MgBootstrapShader + jal_and_j Mg_LoadShader, _MG_VTX_SHADER +.endm + + + .data + + RSPQ_BeginOverlayHeader + RSPQ_DefineCommand MgCmd_SetByte, 8 + RSPQ_DefineCommand MgCmd_SetShort, 8 + RSPQ_DefineCommand MgCmd_SetWord, 8 + RSPQ_DefineCommand MgCmd_SetQuad, 20 + RSPQ_DefineCommand MgCmd_SetShader, 8 + RSPQ_DefineCommand MgCmd_LoadVertices, 8 + RSPQ_DefineCommand MgCmd_DrawIndices, 8 + RSPQ_DefineCommand MgCmd_DrawEnd, 4 + RSPQ_DefineCommand MgCmd_LoadUniform, 8 + RSPQ_DefineCommand MgCmd_LoadUniform, MG_INLINE_UNIFORM_HEADER + 8 + RSPQ_DefineCommand MgCmd_LoadUniform, MG_INLINE_UNIFORM_HEADER + 16 + RSPQ_DefineCommand MgCmd_LoadUniform, MG_INLINE_UNIFORM_HEADER + 32 + RSPQ_DefineCommand MgCmd_LoadUniform, MG_INLINE_UNIFORM_HEADER + 64 + RSPQ_DefineCommand MgCmd_LoadUniform, MG_INLINE_UNIFORM_HEADER + 128 + RSPQ_DefineCommand MgCmd_LoadUniform, 248 # TODO: get rid of magic number + RSPQ_EndOverlayHeader + + .align 4 + .ascii "Dragon RSP Magma" + .ascii "Code by snacchus" + + .align 4 +MG_NORMAL_MASK: .half 0xF800, 0x07E0, 0x001F, 0x7FFF, 1, 1<<5, 1<<11, 0 +MG_CLIP_CACHE_OFFSETS: .half 2,4,6,8,10,12,14,16,18 + +MG_CLIP_PLANES: + .half 1, 0, 0, 1 + .half 0, 1, 0, 1 + .half 0, 0, 1, 1 + .half 1, 0, 0, -1 + .half 0, 1, 0, -1 + .half 0, 0, 1, -1 + +MG_CLIPPING_TEXT: .word 0 +MG_CLIPPING_TEXT_SIZE: .word 0 + + .bss + .align 4 +MG_SCRATCH_MEM: +MG_CLIP_CACHE_USED: .dcb.w 16 + +MG_CLIP_CACHE: .dcb.b MG_VTX_SIZE * MG_CLIP_CACHE_SIZE +MG_CLIP_CACHE_END: + +MG_CLIP_LIST0: .dcb.w MG_CLIP_CACHE_SIZE +MG_CLIP_LIST1: .dcb.w MG_CLIP_CACHE_SIZE + + .align 3 +MG_OUTPUT_BUFFER: .dcb.b TRIANGLE_SIZE * 2 + .text + + ############################################################# + # MgCmd_SetByte + ############################################################# + .func MgCmd_SetByte +MgCmd_SetByte: + jr ra + sb a1, %lo(MG_STATE)(a0) + .endfunc + + ############################################################# + # MgCmd_SetShort + ############################################################# + .func MgCmd_SetShort +MgCmd_SetShort: + jr ra + sh a1, %lo(MG_STATE)(a0) + .endfunc + + ############################################################# + # MgCmd_SetWord + ############################################################# + .func MgCmd_SetWord +MgCmd_SetWord: + jr ra + sw a1, %lo(MG_STATE)(a0) + .endfunc + + ############################################################# + # MgCmd_SetQuad + ############################################################# + .func MgCmd_SetQuad +MgCmd_SetQuad: + addi s0, rspq_dmem_buf_ptr, %lo(RSPQ_DMEM_BUFFER) + lqv $v01, -0x10,s0 + lrv $v01, 0x00,s0 + addi a0, %lo(MG_STATE) + jr ra + sqv $v01, 0x00,a0 + .endfunc + + ############################################################# + # MgCmd_LoadUniform + # ARGS: + # a0: RDRAM pointer + # a1: Bit 31..16: Size + # Bit 15..0: Offset into _MG_VTX_SHADER_UNIFORMS + ############################################################# + .func MgCmd_LoadUniform +MgCmd_LoadUniform: + move s0, a0 + addi s4, a1, %lo(_MG_VTX_SHADER_UNIFORMS) + j DMAIn + srl t0, a1, 16 + + .endfunc + + ############################################################# + # MgCmd_SetShader + # ARGS: + # a0: RDRAM pointer to shader ucode text + # a1: shader text size + ############################################################# + .func MgCmd_SetShader +MgCmd_SetShader: + # TODO: Adjust size of saved data in case the shader uses less DMEM for uniforms. + # In theory this could be done by overwriting the state size in the overlay header. + # However, this could be problematic because of the profiler buffer that is automatically generated at the end of the state. + # There are two potential fixes: + # 1) Disable this optimization when profiling is enabled. + # 2) Move the profiler buffer to the beginning of the saved state. + # Provided it doesn't break anything, the second option would be preferred. + sw a0, %lo(MG_SHADER_TEXT) + sw a1, %lo(MG_SHADER_TEXT_SIZE) + # fallthrough + .endfunc + + .func Mg_LoadShader +Mg_LoadShader: + lw s0, %lo(MG_SHADER_TEXT) + lw t0, %lo(MG_SHADER_TEXT_SIZE) + # fallthrough + .endfunc + + .func Mg_LoadOverlay +Mg_LoadOverlay: + j DMAIn + li s4, %lo(_MG_OVERLAY) + .endfunc + + ############################################################# + # MgCmd_LoadVertices + # ARGS: + # a0: Bit 31..24: Command id + # Bit 23..0: Vertex buffer offset + # a1: Bit 31..16: Cache offset + # Bit 15..0: Vertex count + ############################################################# + .func MgCmd_LoadVertices +MgCmd_LoadVertices: + # We can't fall through here because RDPQ_Triangle_Send_Async etc. is always placed after the .text segment + # and we therefore need to put the vertex shader into it's own segment to force it being placed at the end. + # That means we can't register the command with the shader's address directly because it is not in the .text segment. + j _MG_VTX_SHADER + nop + .endfunc + + ############################################################# + # MgCmd_DrawIndices + # + # ARGS: + # a0: Bit 31..24: Command id + # Bit 15..0: Index 0 + # a1: Bit 31..16: Index 1 + # Bit 15..0: Index 2 + ############################################################# + .func MgCmd_DrawIndices +MgCmd_DrawIndices: + #define vtx1 a0 + #define vtx2 a2 + #define vtx3 a1 + srl a2, a1, 16 + addi s1, rspq_dmem_buf_ptr, %lo(RSPQ_DMEM_BUFFER) - 6 + move s2, s1 + lbu v0, %lo(MG_CULL_MODE) + lbu t7, %lo(MG_OUTPUT_OFFSET) + li sp, %lo(RSPQ_Loop) + lhu a3, %lo(MG_TRI_CMD) + jal RDPQ_Triangle_Send_Async + addi s3, t7, %lo(MG_OUTPUT_BUFFER) + + xor t7, TRIANGLE_SIZE + j RSPQ_Loop + sb t7, %lo(MG_OUTPUT_OFFSET) + #undef vtx1 + #undef vtx2 + #undef vtx3 + .endfunc + +RDPQ_Triangle_Cull: + jr sp + nop + + ############################################################# + # MgCmd_DrawEnd + ############################################################# + .func MgCmd_DrawEnd +MgCmd_DrawEnd: + j RDPQ_Triangle_Send_End + sb zero, %lo(MG_OUTPUT_OFFSET) + .endfunc + +#define RDPQ_TRIANGLE_VTX1 a0 +#define RDPQ_TRIANGLE_VTX2 a2 +#define RDPQ_TRIANGLE_VTX3 a1 +#define RDPQ_TRIANGLE_TRICMD a3 +#define RDPQ_TRIANGLE_VTX1_DMEM 0,s1 +#define RDPQ_TRIANGLE_VTX2_DMEM 2,s2 +#define RDPQ_TRIANGLE_VTX3_DMEM 4,s2 +#define RDPQ_Triangle_Clip _MG_CLIPPING +#include "rsp_rdpq.inc" diff --git a/include/rsp_magma_state.inc b/include/rsp_magma_state.inc new file mode 100644 index 000000000..de96241c5 --- /dev/null +++ b/include/rsp_magma_state.inc @@ -0,0 +1,29 @@ +################################################## +# RSP_MAGMA_STATE.INC +# +# Authors: Dennis Heinze +################################################## + +#include "magma_constants.h" + +.macro _MgState initialize + MG_VIEWPORT: + MG_VIEWPORT_SCALE: .half 0,0,0,0 + MG_VIEWPORT_OFFSET: .half 0,0,0,0 + .ifeq \initialize + MG_CLIP_FACTORS: .half 0,0,0,0 + .else + MG_CLIP_FACTORS: .half 1,1,MG_DEFAULT_GUARD_BAND,MG_DEFAULT_GUARD_BAND + .endif + MG_VERTEX_SIZE: .half 0,0,0,0 + MG_SHADER_TEXT: .word 0 + MG_SHADER_TEXT_SIZE: .word 0 + MG_VERTEX_BUFFER: .word 0 + .ifeq \initialize + MG_TRI_CMD: .half 0 + .else + MG_TRI_CMD: .half 0x800 + .endif + MG_CULL_MODE: .byte 0 + MG_OUTPUT_OFFSET: .byte 0 +.endm \ No newline at end of file diff --git a/include/rsp_queue.inc b/include/rsp_queue.inc index 88a3f943a..ef99fd788 100644 --- a/include/rsp_queue.inc +++ b/include/rsp_queue.inc @@ -93,12 +93,21 @@ _RSPQ_OVERLAY_HEADER: .short _RSPQ_SAVED_STATE_END - _RSPQ_SAVED_STATE_START - 1 # 4: state RDRAM address (filled in by C code) .long 0 - # 8: text RDRAM address (filled in by C code) - .long 0 - # 10: text size (filled in by C code) + # 8: command base (filled in by C code) + .short 0 + # 10: overlay ID (multipled by 4, filled in by C code) + .short 0 + # 12: Load segment table. First entry is text segment + .long 0 # RDRAM address + .short 0 # Size in bytes (filled by C code) + .short _ovl_text_start - _text_start + 0x1000 + .long 0 # Space for a second segment (shared state, if any) .short 0 - # 14: command base (filled in by C code) .short 0 + #if RSPQ_PROFILE + # 28: Profiling slot address + .short _RSPQ_OVL_PROFILESLOT - _data_start + #endif .align 1 _RSPQ_OVERLAY_COMMAND_TABLE: .endm @@ -108,9 +117,12 @@ _RSPQ_OVERLAY_COMMAND_TABLE: #define RSPQ_OH_STATESTART 0 #define RSPQ_OH_STATESIZE 2 #define RSPQ_OH_STATEADDR 4 -#define RSPQ_OH_TEXTADDR 8 -#define RSPQ_OH_TEXTSIZE 10 -#define RSPQ_OH_CMDBASE 14 +#define RSPQ_OH_CMDBASE 8 +#define RSPQ_OH_OVLID 10 +#define RSPQ_OH_LOADSEGTABLE 12 +#if RSPQ_PROFILE +#define RSPQ_OH_PROFILESLOT 28 +#endif ######################################################## # RSPQ_BeginSavedState @@ -137,6 +149,11 @@ _RSPQ_SAVED_STATE_START: ######################################################## .macro RSPQ_EndSavedState .align 3 +#if RSPQ_PROFILE && !RSPQ_PROFILE_MANUAL_SLOT +_RSPQ_OVL_PROFILESLOT: + # Profiling data for this overlay + .long 0, 0 +#endif _RSPQ_SAVED_STATE_END: .endm @@ -298,6 +315,9 @@ RSPQ_SCRATCH_MEM: # the following 32-bytes can be used by any overlay as tempora .ascii "Dragon RSP Queue" .ascii "Rasky & Snacchus" + .align 3 +RSPQ_DMEM_BUFFER: .ds.b RSPQ_DMEM_BUFFER_SIZE + ################################################################ # RDPQ shared state # @@ -338,34 +358,34 @@ RDPQ_CURRENT: .long 0 RDPQ_SENTINEL: .long 0 # Current fill color RDPQ_FILL_COLOR: .word 0 -# Current target buffer color depth +# RDRAM address of the current command list. +RSPQ_RDRAM_PTR: .long 0 +# Current target buffer color depth (0-3). +# The top bit (0x80) is set if the rdpq validator is enabled RDPQ_TARGET_BITDEPTH: .byte 0 # True if there is a SYNC_FULL command in the current RDP output buffer RDPQ_SYNCFULL_ONGOING: .byte 0 -# True if the rdpq debug mode is active (validator). TODO: hide this bit elsewhere to save one byte -RDPQ_DEBUG: .byte 0 ################################################################ # End of RDPQ shared state ################################################################ - .byte 0 # explicit padding byte (would be emitted by the assembler anyway) - -# RDRAM address of the current command list. -RSPQ_RDRAM_PTR: .long 0 - -# ID of the current overlay -RSPQ_CURRENT_OVL: .half 0 - - - .align 3 -RSPQ_DMEM_BUFFER: .ds.b RSPQ_DMEM_BUFFER_SIZE - +#if RSPQ_PROFILE + .align 1 +RSPQ_PROFILE_CUR_SLOT: .long 0 +RSPQ_PROFILE_START_TIME: .long 0 +RSPQ_PROFILE_DATA: + RSPQ_PROFILESLOT_IDLE_CPU: .long 0,0 + RSPQ_PROFILESLOT_IDLE_RDPBUSY: .long 0,0 + RSPQ_PROFILESLOT_IDLE_RDPSYNCFULL: .long 0,0 + RSPQ_PROFILESLOT_IDLE_RDPSYNCFULLMULTI: .long 0,0 + RSPQ_PROFILESLOT_OVLSWITCH: .long 0,0 + RSPQ_PROFILESLOT_BUILTINS: .long 0,0 +#endif - .align 3 RSPQ_INTERNAL_COMMAND_TABLE: RSPQ_DefineCommand RSPQCmd_WaitNewInput, 0 # 0x00 -RSPQ_DefineCommand RSPQCmd_Noop, 4 # 0x01 +RSPQ_DefineCommand RSPQCmd_WriteWord, 8 # 0x01 RSPQ_DefineCommand RSPQCmd_Jump, 4 # 0x02 RSPQ_DefineCommand RSPQCmd_Call, 8 # 0x03 RSPQ_DefineCommand RSPQCmd_Ret, 4 # 0x04 @@ -373,15 +393,17 @@ RSPQ_DefineCommand RSPQCmd_Dma, 16 # 0x05 RSPQ_DefineCommand RSPQCmd_WriteStatus, 4 # 0x06 -- must be even (bit 24 must be 0) RSPQ_DefineCommand RSPQCmd_SwapBuffers, 12 # 0x07 RSPQ_DefineCommand RSPQCmd_TestWriteStatus, 8 # 0x08 -- must be even (bit 24 must be 0) -RSPQ_DefineCommand RSPQCmd_RdpWaitIdle, 4 # 0x09 -RSPQ_DefineCommand RSPQCmd_RdpSetBuffer, 12 # 0x0A -RSPQ_DefineCommand RSPQCmd_RdpAppendBuffer, 4 # 0x0B +RSPQ_DefineCommand RSPQCmd_RdpSetBuffer, 12 # 0x09 +RSPQ_DefineCommand RSPQCmd_RdpAppendBuffer, 4 # 0x0A - .align 4 -# Overlay data will be loaded at this address +# Overlay data will be loaded at this address. We force 8-byte alignment +# here so that the overlay header can layout 2/4 bytes field in a way +# to guarantee no padding. See RSPQ_BeginOverlayHeader. + .align 3 _ovl_data_start: .text +_text_start: # Just declare the shift macros, without emitting code. We will be emitting it later setup_vsll vshift, 0 @@ -437,13 +459,28 @@ rspq_fetch_buffer_with_ptr: # fallthrough into the main loop .endfunc -RSPQCmd_Noop: # noop -> repeat the loop .func RSPQ_Loop RSPQ_Loop: #define ovl_index t4 #define cmd_index t5 // referenced in rspq_assert_invalid_overlay #define cmd_desc t6 - + #define ovl_id t7 + +#if RSPQ_PROFILE + mfc0 t0, COP0_DP_CLOCK + lw s0, %lo(RSPQ_PROFILE_CUR_SLOT) + lw t1, %lo(RSPQ_PROFILE_START_TIME) + bltz s0, 1f + li ovl_index, 0 + jal RSPQ_ProfileAccumulate + subu t0, t1 + # Reset the current slot so we don't end up accumulating to it again. + # This could happen if highpri mode is entered right after this. + li s0, -1 + sw s0, %lo(RSPQ_PROFILE_CUR_SLOT) +1: + li s0, %lo(RSPQ_PROFILESLOT_BUILTINS) +#endif # Read SP_STATUS to check for highpri mfc0 t2, COP0_SP_STATUS @@ -457,44 +494,44 @@ RSPQ_Loop: beq t2, t0, rspq_switch_highpri # Index into overlay table - srl t0, a0, 28 + srl ovl_id, a0, 28 # Index into command table srl cmd_index, a0, 23 andi cmd_index, 0x1FE # Overlay 0 is reserved for internal commands - beqz t0, rspq_execute_command - # Load command descriptor from internal command table if using the default overlay. - # Otherwise, cmd_desc will be overwritten further down - lhu cmd_desc, %lo(RSPQ_INTERNAL_COMMAND_TABLE)(cmd_index) + beqz ovl_id, rspq_execute_command + # Load command descriptor from internal command table if using the default overlay. + # Otherwise, cmd_desc will be overwritten further down + lhu cmd_desc, %lo(RSPQ_INTERNAL_COMMAND_TABLE)(cmd_index) - lhu t1, %lo(RSPQ_CURRENT_OVL) +rspq_overlay_check: + lhu t1, %lo(_ovl_data_start) + RSPQ_OH_OVLID # Map overlay index through the ID map. This allows to resolve ID for # overlays that spans multiple IDs. - lbu ovl_index, RSPQ_OVERLAY_IDMAP(t0) + lbu ovl_index, %lo(RSPQ_OVERLAY_IDMAP)(ovl_id) # Load the command base (index of the first command of the overlay). - lhu t0, %lo(_ovl_data_start) + 14 + lhu t0, %lo(_ovl_data_start) + RSPQ_OH_CMDBASE # Initial vshift in preparation to running the command lpv vshift8, 0x00,zero - #if RSPQ_DEBUG - assert_ne ovl_index, 0, ASSERT_INVALID_OVERLAY - #endif - # Check if the requested overlay is already in memory bne ovl_index, t1, rspq_load_overlay - -rspq_overlay_loaded: # Subtract the command base to determine the final offset into the command table. sub cmd_index, t0 # Load command descriptor from overlay command table lhu cmd_desc, %lo(_ovl_data_start) + RSPQ_OVERLAY_HEADER_SIZE(cmd_index) + #if RSPQ_PROFILE + # Load the address of the profile slot for this overlay + lhu s0, %lo(_ovl_data_start) + RSPQ_OH_PROFILESLOT + #endif + rspq_execute_command: # Load second command word # Calculate vshift8 lw a1, %lo(RSPQ_DMEM_BUFFER) + 0x4 (rspq_dmem_buf_ptr); vsrl vshift, vshift8, 8 @@ -529,40 +566,64 @@ rspq_execute_command: # Jump to command. Set ra to the loop function, so that commands can # either do "j RSPQ_Loop" or "jr ra" (or a tail call) to get back to the main loop sll cmd_desc, 2 + +#if RSPQ_PROFILE + bnez a0, 1f + mfc0 t0, COP0_DP_CLOCK + # Write samples of opcode 0 to a special slot ("Idle (CPU)") + li s0, %lo(RSPQ_PROFILESLOT_IDLE_CPU) +1: + sw s0, %lo(RSPQ_PROFILE_CUR_SLOT) + sw t0, %lo(RSPQ_PROFILE_START_TIME) +#endif + jr cmd_desc li ra, %lo(RSPQ_Loop) rspq_load_overlay: - +#if RSPQ_PROFILE + mfc0 t3, COP0_DP_CLOCK +#endif + # Restore full command index before switching overlay addu cmd_index, t0 # Save current overlay state - lhu t0, %lo(_ovl_data_start) + 2 - lw s0, %lo(_ovl_data_start) + 4 + lhu t0, %lo(_ovl_data_start) + RSPQ_OH_STATESIZE + lw s0, %lo(_ovl_data_start) + RSPQ_OH_STATEADDR jal DMAOutAsync - lhu s4, %lo(_ovl_data_start) + 0 + lhu s4, %lo(_ovl_data_start) + RSPQ_OH_STATESTART + + #if RSPQ_DEBUG + assert_ne ovl_index, 0, ASSERT_INVALID_OVERLAY + #endif # Load data segment - sll s0, ovl_index, 2 - lw s0, %lo(RSPQ_OVERLAY_TABLE)(s0) + lw s0, %lo(RSPQ_OVERLAY_TABLE)(ovl_index) li s4, %lo(_ovl_data_start) + li t1, %lo(_ovl_data_start) + RSPQ_OH_LOADSEGTABLE srl t0, s0, 24-4 jal DMAIn - ori t0, 0xf + ori t0, 0xf - # Load text segment - lw s0, %lo(_ovl_data_start) + 8 - lhu t0, %lo(_ovl_data_start) + 12 + # Load text segment plus additional optionally segments +1: lw s0, 0(t1) + lhu t0, 4(t1) jal DMAIn - li s4, %lo(_ovl_text_start) - + lhu s4, 6(t1) + bltz s0, 1b + addiu t1, 8 + +#if RSPQ_PROFILE + mfc0 t0, COP0_DP_CLOCK + subu t0, t3 + jal RSPQ_ProfileAccumulate + li s0, %lo(RSPQ_PROFILESLOT_OVLSWITCH) +#endif # Load new overlay command base - lhu t0, %lo(_ovl_data_start) + 14 - j rspq_overlay_loaded - sh ovl_index, %lo(RSPQ_CURRENT_OVL) - + j rspq_overlay_check + # FALLTHROUGH - DELAY SLOT #undef ovl_index #undef cmd_index @@ -574,7 +635,7 @@ rspq_switch_highpri: # Switch to highpri mode, calling RSPQCMD_SwapBuffers. We prepare the # argument to switch to the highpri buffer, clears the highpri requested # signal, and set the highpri running signal. - li a2, SP_WSTATUS_CLEAR_SIG_HIGHPRI_REQUESTED | SP_WSTATUS_SET_SIG_HIGHPRI_RUNNING + li a2, SP_WSTATUS_CLEAR_SIG_HIGHPRI_REQUESTED | SP_WSTATUS_SET_SIG_HIGHPRI_RUNNING li a0, RSPQ_HIGHPRI_CALL_SLOT<<2 li a1, RSPQ_LOWPRI_CALL_SLOT<<2 #fallthrough @@ -583,16 +644,20 @@ rspq_switch_highpri: ############################################################# # RSPQCmd_SwapBuffers # - # Switch between lowpri and highpri or viceversa. This is - # called by RSP itself to go into highpri mode, and scheduled - # as normal command by CPU when going back into lowpri. + # Switch between buffers (eg: lowpri to highpri or viceversa). + # This is called by RSP itself to go into highpri mode, and scheduled + # as normal command by CPU when going back into lowpri, or when switching + # between lowpri buffers. # # ARGS: - # a0: Pointer stack slot that contains the address to switch to. - # (either RSPQ_LOWPRI_CALL_SLOT<<2 or RSPQ_HIGHPRI_CALL_SLOT<<2) - # a1: Pointer stack slot where to save the current address to. - # (either RSPQ_LOWPRI_CALL_SLOT<<2 or RSPQ_HIGHPRI_CALL_SLOT<<2) - # a2: New mask to check for HIGHPRI signal (0 in highpri mode). + # a0: Byte offset of the pointer-stack slot that contains the address + # to switch to. Highpri switching uses RSPQ_LOWPRI_CALL_SLOT or + # RSPQ_HIGHPRI_CALL_SLOT; lowpri buffer handoff uses an inactive + # block-call slot whose target was staged by RSPQCmd_WriteWord. + # a1: Byte offset of the pointer-stack slot where the current address is + # saved. This may match a0 when the saved address is discarded. + # a2: Write mask for SP_STATUS register. Useful to change signals + # atomically while swapping buffers. ############################################################# .func RSPQCmd_SwapBuffers RSPQCmd_SwapBuffers: @@ -616,7 +681,14 @@ RSPQCmd_SwapBuffers: RSPQCmd_Call: # a0: command opcode + RDRAM address # a1: call slot in DMEM + + ## If the MSB in the slot (a1) is set, it is seen as a placeholder. + ## This assumes the address has been placed in the pointer-stack instead. + bgez a1, 1f lw s0, %lo(RSPQ_RDRAM_PTR) + ## a0 now contains the DMEM location where to load the block address from + lw a0, 0(a0) + 1: add s0, rspq_dmem_buf_ptr sw s0, %lo(RSPQ_POINTER_STACK)(a1) # save return address # fallthrough @@ -771,20 +843,24 @@ RSPQCmd_RdpSetBuffer: bne t0, a1, 1f lbu t3, %lo(RDPQ_SYNCFULL_ONGOING) ori t3, DP_STATUS_DMA_BUSY -1: jal RSPQ_RdpWait - ori t3, DP_STATUS_END_VALID +1: ori t3, DP_STATUS_END_VALID + +RSPQCmd_RdpSetBuffer_RdpWait: + mfc0 t2, COP0_DP_STATUS + and t1, t2, t3 + bnez t1, RSPQCmd_RdpSetBuffer_RdpWait # In case there was a SYNC_FULL pending, before switching to next buffer, we need # to actually write the current one to RDP. Notice that the following two instructions # are actually nops in case there was no SYNC_FULL (as DP_END == RDPQ_CURRENT in that case). - sb zero, %lo(RDPQ_SYNCFULL_ONGOING) + sb zero, %lo(RDPQ_SYNCFULL_ONGOING) lw t0, %lo(RDPQ_CURRENT) #ifdef NDEBUG mtc0 t0, COP0_DP_END #else # Check if the RDPQ debug mode is active (validator) - lbu t2, %lo(RDPQ_DEBUG) - beqz t2, 1f + lb t2, %lo(RDPQ_TARGET_BITDEPTH) + bgez t2, 1f mtc0 t0, COP0_DP_END # Generate a RSP interrupt to tell the CPU to fetch the new DP_START / DP_END. @@ -825,30 +901,42 @@ RSPQCmd_RdpAppendBuffer: .endfunc ############################################################# - # RSPQCmd_RdpWaitIdle + # RSPQCmd_WriteWord # - # Wait until RDP is idle. + # Writes a 32bit word to a given DMEM location. # - # *NOTE*: a SYNC_FULL command *must* have been already written - # to the RDP output buffer, otherwise this function will stall - # forever. In fact, once the RDP is running, it never gets back - # to idle state until a SYNC_FULL is found, even if it has no - # more commands to run. + # ARGS: + # a0: DMEM address + # a1: 32bit value ############################################################# - .func RSPQCmd_RdpWaitIdle -RSPQCmd_RdpWaitIdle: - li t3, DP_STATUS_BUSY + .func RSPQCmd_WriteWord +RSPQCmd_WriteWord: + j RSPQ_Loop + sw a1, 0(a0) + .endfunc -RSPQ_RdpWait: - mfc0 t2, COP0_DP_STATUS -1: - # Wait for selected RDP status bits to become 0. - and t1, t2, t3 - bnez t1, 1b - mfc0 t2, COP0_DP_STATUS +#if RSPQ_PROFILE + ############################################################# + # RSPQ_ProfileAccumulate + # + # ARGS: + # t0: Sampled time + # s0: Slot offset + ############################################################# + .func RSPQ_ProfileAccumulate +RSPQ_ProfileAccumulate: + lw t2, 0x0(s0) + lw t1, 0x4(s0) + # We need to mask the time because the RDP clock wraps around at 24 bits + and t0, 0xFFFFFF + add t0, t2 + addi t1, 1 + sw t0, 0x0(s0) jr ra + sw t1, 0x4(s0) nop .endfunc +#endif #include #include diff --git a/include/rsp_rdpq.inc b/include/rsp_rdpq.inc index 643acacf3..6393d31e9 100644 --- a/include/rsp_rdpq.inc +++ b/include/rsp_rdpq.inc @@ -3,6 +3,7 @@ # RDPQ library # # Authors: Dennis Heinze +# Giovanni Bajo ############################################################################## # # This library allows rspq overlays to emit RDP commands. @@ -99,13 +100,16 @@ RDPQ_Send: # so using that bit in the RSPQ_RdpWait wait mask will make sure we wait for the RDP # to be idle. lbu t3, %lo(RDPQ_SYNCFULL_ONGOING) - jal RSPQ_RdpWait ori t3, DP_STATUS_END_VALID +rdpq_send_wait: + mfc0 t0, COP0_DP_STATUS + and t0, t3 + bnez t0, rdpq_send_wait # Switch to the next dynamic buffer. # Since there are two of them, also switch between # them so next time we will pick the other one. - lw rdram_cur, %lo(RDPQ_DYNAMIC_BUFFERS) + 4 + lw rdram_cur, %lo(RDPQ_DYNAMIC_BUFFERS) + 4 lw t1, %lo(RDPQ_DYNAMIC_BUFFERS) + 0 sw rdram_cur, %lo(RDPQ_DYNAMIC_BUFFERS) + 0 sw t1, %lo(RDPQ_DYNAMIC_BUFFERS) + 4 @@ -220,7 +224,8 @@ AA_BLEND_DEFAULT_FORMULA: .word RDPQ_BLENDER((IN_RGB, IN_ALPHA, MEMORY_RGB, MEMORY_CVG)) # Standard AA .word RDPQ_BLENDER((IN_RGB, IN_ALPHA, MEMORY_RGB, MEMORY_CVG)) & ~SOM_READ_ENABLE # Reduced AA -#define RDPQ_COMB_MIPMAP2 RDPQ_COMBINER2((TEX1, TEX0, LOD_FRAC, TEX0), (TEX1, TEX0, LOD_FRAC, TEX0), (0,0,0,0), (0,0,0,0)) +#define RDPQ_COMB_LOD_INTERP RDPQ_COMBINER2((TEX1, TEX0, LOD_FRAC, TEX0), (TEX1, TEX0, LOD_FRAC, TEX0), (0,0,0,0), (0,0,0,0)) +#define RDPQ_COMB_LOD_SHQ RDPQ_COMBINER2((TEX0, TEX1, K5, 0 ), (0, 0, 0, TEX1), (0,0,0,0), (0,0,0,0)) #define RDPQ_COMB_SHADE_FOG RDPQ_COMBINER1((0,0,0,SHADE), (0,0,0,1)) #define RDPQ_COMB_TEX_SHADE_FOG RDPQ_COMBINER1((TEX0,0,SHADE,0), (0,0,0,TEX0)) @@ -230,7 +235,9 @@ COMBINER_SHADE_FOG: .quad RDPQ_COMB_SHADE_FOG COMBINER_TEX_SHADE: .quad RDPQ_COMBINER_TEX_SHADE COMBINER_TEX_SHADE_FOG: .quad RDPQ_COMB_TEX_SHADE_FOG -COMBINER_MIPMAP2: .quad (RDPQ_COMB_MIPMAP2 & RDPQ_COMB0_MASK) | RDPQ_COMBINER_2PASS +COMBINER_MIPMAPS: + .quad (RDPQ_COMB_LOD_INTERP & RDPQ_COMB0_MASK) | RDPQ_COMBINER_2PASS + .quad (RDPQ_COMB_LOD_SHQ & RDPQ_COMB0_MASK) | RDPQ_COMBINER_2PASS .section .bss.rdpq_mode_api @@ -244,20 +251,21 @@ RDPQ_MODE_COMBINER_2CYC: .quad 0 .func RDPQ_SetBlendingMode RDPQ_SetBlendingMode: # Set the blending mode formula in the second step. Then: - # * If the formula is empty, clear also the first step if it was - # part of a previous two-step blending (SOMX_BLEND_2PASS). - # * If the formula is not empty and it is two-steps (SOMX_BLEND_2PASS), - # put it also in the first step. - # We cover both conditision in one single codeflow by putting into t0 - # the value to check against SOMX_BLEND_2PASS. + # * If the new formula is 2-pass (SOMX_BLEND_2PASS), mirror it into + # the first step as well. + # * If the new formula is 1-pass (or empty), keep the first step as-is + # unless it was previously part of an older 2-pass configuration. + # * If we detect this stale 2-pass state in step0, clear it so that + # switching from BLENDER2 -> BLENDER does not leave SOMX_BLEND_2PASS + # active in the render mode. lw t0, %lo(RDPQ_MODE_BLENDER_STEPS) + 0 - beqz a1, setblending_check sw a1, %lo(RDPQ_MODE_BLENDER_STEPS) + 4 - move t0, a1 -setblending_check: - andi t0, SOMX_BLEND_2PASS - beqz t0, RDPQ_UpdateRenderMode - nop + + andi t1, a1, SOMX_BLEND_2PASS + bnez t1, RDPQ_SetFogMode + andi t1, t0, SOMX_BLEND_2PASS + beqz t1, RDPQ_UpdateRenderMode + li a1, 0 # fallthrough! .endfunc @@ -353,7 +361,8 @@ RDPQ_UpdateRenderMode: calc_comb_1cyc: # Check if fogging is active - andi t0, som_hi, SOMX_FOG >> 32 + li t1, SOMX_FOG >> 32 + and t0, som_hi, t1 beqz t0, check_mipmap_interp # Create a copy of comb_hi without the cmd ID in the top MSB. @@ -385,10 +394,11 @@ fog_change: lw comb_lo, 4(s0) check_mipmap_interp: - and t0, som_hi, SOMX_LOD_INTERPOLATE >> 32 - beqz t0, store_comb_1cyc + andi t2, som_hi, SOMX_LOD_INTERP_MASK >> 32 + beqz t2, store_comb_1cyc + sll t2, 3 - (SOMX_LOD_INTERP_SHIFT - 32) # Prepare offset for the current mipmap interpolation mode - # Interpolated mipmapping is active. We want to add RDPQ_COMB_MIPMAP as step0 + # Interpolated mipmapping is active. We want to add a special combiner step as step0 # and use only step 1 of the incoming formula. Unfortunately, this # also means that all TEX0 slots must be converted into COMBINED slots. # We do this by using the mask already loaded in a2/a3 @@ -399,8 +409,8 @@ check_mipmap_interp: # Since this combiner now requires two-cycle mode, we can simply store in the # 2-cycle mode slot. No need to touch the 1-cycle mode slot as it will not # be used anyway. - lw t0, %lo(COMBINER_MIPMAP2) + 0 - lw t1, %lo(COMBINER_MIPMAP2) + 4 + lw t0, %lo(COMBINER_MIPMAPS) - 8(t2) + lw t1, %lo(COMBINER_MIPMAPS) - 4(t2) or comb_hi, t0 j store_comb_2cyc or comb_lo, t1 @@ -527,6 +537,22 @@ blender_1pass: #undef blend0_mask #undef blend1_mask + ###################################### + # + # YUV BILINEAR FILTERING + # + ###################################### + + # Isolate the TF and sample bits + andi t0, som_hi, (SOM_TF_MASK | SOM_SAMPLE_MASK) >> 32 + # Check if TF0=TF1=YUV and SAMPLE=BILINEAR + li t1, (SOM_SAMPLE_BILINEAR | SOM_TF0_YUV | SOM_TF1_YUV) >> 32 + bne t0, t1, 1f + # If so, set TF1_YUVTEX0 and TF0_RGB + li t0, (SOM_TF1_YUVTEX0 | SOM_TF0_RGB) >> 32 + or som_hi, t0 +1: + ###################################### # # 1 CYCLE / 2 CYCLE MODE SELECTION @@ -539,6 +565,8 @@ blender_1pass: # 2cycle mode: # * Blender: bit 15 is set if 2cyc mode is required (SOMX_BLEND_2PASS) # * Combiner: bit 63 is set if 2cyc mode is required (RDPQ_COMBINER_2PASS) + # NOTE: YUV bilinear filtering requires 2cycle mode, but that's not a special + # case here, as non-buggy uses of it must use TEX1. # # First, we align both bits in bit 31 and we OR them together. sll t2, blend_2cyc, 16 @@ -554,6 +582,10 @@ set_1cyc: move blend_final, blend_1cyc li cycle_type, ((SOM_CYCLE_MASK ^ SOM_CYCLE_1) >> 32) | 0x10000000 set_cycle_type: + # Before clearing the MSB in som_hi extract the SOMX ALPHACOMPARE flag into + # t3 (bit 31) that might be useful later. + sll t3, som_hi, 31 - (SOMX_ALPHACOMPARE_SHIFT-32) + # Set cycle type bits in other modes high word. Also put the correct # command (0xEF) in the top byte: we achieve this by first setting the # top byte to 0xFF, and then xoring with 0x10 (which is included in @@ -605,13 +637,32 @@ set_cycle_type: # Additionally we need to disable the actual alpha compare feature since it would # compare the threshold with the alpha multiplied by coverage in this case, which would # lead to visible seams at polygon edges. - li t0, SOM_ALPHACOMPARE_THRESHOLD | SOM_BLALPHA_CVG - and t1, som_lo, t0 - bne t0, t1, rdpq_update_finish - nop + sll t0, som_lo, 31 - (SOM_BLALPHA_SHIFT+1) # SOM_BLALPHA_CVG goes to bit 31 + and t0, t3 # SOMX_ALPHACOMPARE is already in bit 31 + bgez t0, 1f + nop or som_lo, SOM_BLALPHA_CVG_TIMES_CC and som_lo, ~SOM_ALPHACOMPARE_MASK +1: + ###################################### + # + # ZMODE OPAQUE/TRANSPARENT SWITCHING + # + ###################################### + + # Check whether SOM_ZMODE is either interpen or decal (both have 1<<10 set). + # If so, there's nothing to do. + # Otherwise, if it's standard mode, set bit 2<<10 to switch between + # opaque and transparent, depending on SOM_BLENDING. + andi t0, som_lo, 1<<10 + bnez t0, 1f + andi t1, som_lo, SOM_BLENDING + bnez t1, 1f + ori som_lo, 2<<10 + xori som_lo, 2<<10 +1: + ###################################### # # SAVE SETTINGS & APPLY TO RDP @@ -704,6 +755,7 @@ RDPQ_WriteSetScissor: RDPQ_WriteSetFillColor: sw a1, %lo(RDPQ_FILL_COLOR) lbu t0, %lo(RDPQ_TARGET_BITDEPTH) + andi t0, 3 beq t0, 3, RDPQ_Write8 lui a0, 0xF700 # SET_FILL_COLOR srl t0, a1, 24 + (8-5) - 11 @@ -830,6 +882,8 @@ RDPQ_Triangle: #define VTX_ATTR_X 0 #define VTX_ATTR_Y 2 #define VTX_ATTR_Z 4 + #define VTX_ATTR_CLIPCODE 6 // Clipping codes + #define VTX_ATTR_TRCODE 7 // Negated trivial-reject codes #define VTX_ATTR_RGBA 8 #define VTX_ATTR_S 12 #define VTX_ATTR_T 14 @@ -837,6 +891,11 @@ RDPQ_Triangle: #define VTX_ATTR_INVWi 20 #define VTX_ATTR_INVWf 22 +#if 0 + mfc0 fp, COP0_DP_CLOCK + xtracestart +#endif + j half_swap li t0, 1 @@ -882,6 +941,11 @@ swap_end: ssv vxy32.e5, 4,s3 # y2 ssv vxy32.e4, 2,s3 # y3 + # Add num mipmap levels to tricmd + lbu t1, %lo(RDPQ_OTHER_MODES) + 0 + andi t1, 0x38 # Isolate bits 2-5 (aka 59-61 of SOM) + or tricmd, t1 + # Now calculate: # vxy32 = X3 X2 X3 -- Y3 Y2 Y3 -- # - @@ -926,11 +990,6 @@ swap_end: sll t0, 7 or tricmd, t0 - # Add num mipmap levels - lbu t1, %lo(RDPQ_OTHER_MODES) + 0 - andi t1, 0x38 # Isolate bits 2-5 (aka 59-61 of SOM) - or tricmd, t1 - # Calculate reciprocal of normal vrcph vnz_i.e0, vnz_i.e4 vrcpl vnz_f.e0, vnz_f.e4 @@ -999,6 +1058,10 @@ swap_end: # v__ = 0 0 0 0 0 0 0 NZi # slope = -- -- -- -- 1/HY 1/MY 1/LY 1/NZ + #undef vtmp_f + #undef vtmp_i + #undef vk2 + vmov vnz_f.e0, vslope_f.e7 vmov vnz_i.e0, vslope_i.e7 @@ -1285,9 +1348,20 @@ no_texture: ssv vde_f.e7, 0x0A,s3 ssv vdy_i.e7, 0x0C,s3 ssv vdy_f.e7, 0x0E,s3 - jr ra addi s3, 0x10 +#if 0 + xtracestop + + mfc0 t0, COP0_DP_CLOCK + sub t0, t0, fp + xlog_string "RDPQ_Triangle: " + xlogregs_gpr t0 +#endif + + jr ra + nop + #undef tricm #undef vtx1 #undef vtx2 diff --git a/include/rsp_rdpq_tri.inc b/include/rsp_rdpq_tri.inc index d11a91060..e41f5c592 100644 --- a/include/rsp_rdpq_tri.inc +++ b/include/rsp_rdpq_tri.inc @@ -72,7 +72,9 @@ #define STORE_VTX3 #endif - +#ifndef RDPQ_TRIANGLE_TRICMD +#define RDPQ_TRIANGLE_TRICMD a0 +#endif ##################################################################### # RDPQ_Triangle_Send_Async @@ -129,7 +131,7 @@ .func RDPQ_Triangle_Send_Async RDPQ_Triangle_Send_Async: - #define tricmd a0 + #define tricmd RDPQ_TRIANGLE_TRICMD // a0 #define vtx1 RDPQ_TRIANGLE_VTX1 // a1 #define vtx2 RDPQ_TRIANGLE_VTX2 // a2 #define vtx3 RDPQ_TRIANGLE_VTX3 // a3 @@ -195,7 +197,7 @@ RDPQ_Triangle_Send_Async: #if RDPQ_TRIANGLE_PROFILE mfc0 fp, COP0_DP_CLOCK - emux_trace_start + xtracestart #endif #define vall1 $v01 @@ -638,11 +640,11 @@ rdpq_triangle_change_buffer_done: sdv vzout1.e0, 0x00,z_dmem #if RDPQ_TRIANGLE_PROFILE - emux_trace_stop + xtracestop mfc0 t0, COP0_DP_CLOCK sub t0, t0, fp - emux_log_string "RDPQ_Triangle: " - emux_dump_gpr t0 + xlog_string "RDPQ_Triangle: " + xlogregs_gpr t0 #endif jr ra diff --git a/include/rspq.h b/include/rspq.h index 1f2e63ac5..9e17c796d 100644 --- a/include/rspq.h +++ b/include/rspq.h @@ -96,6 +96,24 @@ * Blocks must always be created at runtime once (eg: at init time) before * being used. * + * ## Queues + * + * A queue (#rspq_queue_t) is a buffered sequence of commands that can + * be recorded and executed later. Unlike blocks, queues can only be run + * once, but they can be cleared and re-recorded multiple times, with efficient + * memory usage. + * + * A typical use case can be to create different draw layers, eg: a layer for + * opaque objects and a layer for transparent objects. You can add commands to + * each of the queue at any time, and then at the end of the frame, run each + * queue once to draw the corresponding layer. + * + * To create a queue, use #rspq_queue_create. Then, use #rspq_queue_switch to + * switch to the queue and add commands with #rspq_write. Use #rspq_queue_run to + * execute the queue. Use #rspq_queue_clear to clear the queue contents, keeping + * its memory for reuse. Use #rspq_queue_destroy to destroy the queue and free + * all its memory. + * * ## Syncpoints * * The RSP command queue is designed to be fully lockless, but sometimes it is @@ -168,7 +186,9 @@ #ifndef __LIBDRAGON_RSPQ_H #define __LIBDRAGON_RSPQ_H + #include +#include "preview.h" #include "rsp.h" #include "debug.h" #include "n64sys.h" @@ -201,6 +221,15 @@ extern "C" { */ typedef struct rspq_block_s rspq_block_t; +/** + * @brief A buffered queue of commands + * + * A queue (#rspq_queue_t) is a mutable, buffered sequence of commands that can + * be recorded and later executed. Unlike blocks, queues can be cleared and + * reused multiple times. + */ +typedef struct rspq_queue_s rspq_queue_t; + /** * @brief A syncpoint in the queue * @@ -221,6 +250,40 @@ typedef struct rspq_block_s rspq_block_t; */ typedef int rspq_syncpoint_t; + +/** + * @brief Placeholder #0 pointer to be used with rspq_block_run() + */ +#define RSPQ_BLOCK_PLACEHOLDER_0 ((rspq_block_t*)0) +/** + * @brief Placeholder #1 pointer to be used with rspq_block_run() + */ +#define RSPQ_BLOCK_PLACEHOLDER_1 ((rspq_block_t*)1) +/** + * @brief Placeholder #2 pointer to be used with rspq_block_run() + */ +#define RSPQ_BLOCK_PLACEHOLDER_2 ((rspq_block_t*)2) +/** + * @brief Placeholder #3 pointer to be used with rspq_block_run() + */ +#define RSPQ_BLOCK_PLACEHOLDER_3 ((rspq_block_t*)3) +/** + * @brief Placeholder #4 pointer to be used with rspq_block_run() + */ +#define RSPQ_BLOCK_PLACEHOLDER_4 ((rspq_block_t*)4) +/** + * @brief Placeholder #5 pointer to be used with rspq_block_run() + */ +#define RSPQ_BLOCK_PLACEHOLDER_5 ((rspq_block_t*)5) +/** + * @brief Placeholder #6 pointer to be used with rspq_block_run() + */ +#define RSPQ_BLOCK_PLACEHOLDER_6 ((rspq_block_t*)6) +/** + * @brief Number of placeholders that are available in blocks + */ +#define RSPQ_BLOCK_PLACEHOLDER_COUNT 7 + /** * @brief Initialize the RSPQ library. * @@ -293,6 +356,36 @@ uint32_t rspq_overlay_register(rsp_ucode_t *overlay_ucode); */ void rspq_overlay_register_static(rsp_ucode_t *overlay_ucode, uint32_t overlay_id); +/** + * @brief Make an overlay share the state with another + * @preview + * + * Sometimes it is useful for two overlays to share their state. This is + * common when a larger ucode is split into several ucodes for IMEM limit size, + * but the state must be shared among them. + * + * This function "patches" overlay_dest so that it will actually use the state + * of overlay_source. If the state of the dest overlay was declared as smaller, + * it is assumed to be a prefix of the source overlay state. It is not possible + * for the dest overlay to have a split state (part shared with source, part + * kept private). + * + * It is allowed for the state in the source and dest overlay to be kept at + * different offsets in DMEM. + * + * It is possible for more than two overlays to share the same state; make sure to + * always use the same single overlay as source of truth. + * + * This function must be called before beginning to use the overlays (before or + * after registration). + * + * @param overlay_dest Overlay that will be patched to share the state + * with overlay_source. + * @param overlay_source Overlay whose state will be shared. + */ +LIBDRAGON_PREVIEW_API +void rspq_overlay_share_state(rsp_ucode_t *overlay_dest, rsp_ucode_t *overlay_source); + /** * @brief Unregister a ucode overlay from the RSP queue engine. * @@ -637,9 +730,37 @@ void rspq_wait(void); * in the same order they have been created. * * @see #rspq_syncpoint_t + * @see #rspq_syncpoint_new_cb */ rspq_syncpoint_t rspq_syncpoint_new(void); +/** + * @brief Create a syncpoint in the queue that triggers a callback on the CPU. + * @preview + * + * This function is similar to #rspq_syncpoint_new: it creates a new "syncpoint" + * that references the current position in the queue. When the RSP reaches + * the syncpoint, it notifies the CPU, that will invoke the provided callback + * function. + * + * The callback function will be called *outside* of the interrupt context, so + * that it is safe for instance to call into most the standard library. + * + * The callback function is guaranteed to be called after the RSP has reached + * the syncpoint, but there is no guarantee on "how much" after. In general + * the callbacks will be treated as "lower priority" by rspq, so they will + * be called in best effort. + * + * @param func Callback function to call when the syncpoint is reached + * @param arg Argument to pass to the callback function + * @return rspq_syncpoint_t ID of the just-created syncpoint. + * + * @see #rspq_syncpoint_t + * @see #rspq_syncpoint_new + */ +LIBDRAGON_PREVIEW_API +rspq_syncpoint_t rspq_syncpoint_new_cb(void (*func)(void *), void *arg); + /** * @brief Check whether a syncpoint was reached by RSP or not. * @@ -668,25 +789,85 @@ bool rspq_syncpoint_check(rspq_syncpoint_t sync_id); */ void rspq_syncpoint_wait(rspq_syncpoint_t sync_id); - /** - * @brief Begin creating a new block. + * @brief Enqueue a callback to be called by the CPU + * @preview + * + * This function enqueues a callback that will be called by the CPU when + * the RSP has finished all commands put in the queue until now. + * + * An example of a use case for this function is to free resources such as + * rspq blocks that are no longer needed, but that you want to make sure that + * are not referenced anymore by the RSP. * - * This function begins writing a command block (see #rspq_block_t). - * While a block is being written, all calls to #rspq_write - * will record the commands into the block, without actually scheduling them for - * execution. Use #rspq_block_end to close the block and get a reference to it. + * See also #rdpq_call_deferred that, in addition to waiting for RSP, it also + * waits for RDP to process all pending commands before calling the callback. * - * Only one block at a time can be created. Calling #rspq_block_begin - * twice (without any intervening #rspq_block_end) will cause an assert. + * @note DO NOT CALL RSPQ FUNCTIONS INSIDE THE CALLBACK (including enqueueing + * new rspq commands). This might cause a deadlock or corruption, and it + * is not supported. + * + * @param func Callback function + * @param arg Argument to pass to the callback + * + * @see #rdpq_call_deferred + */ +LIBDRAGON_PREVIEW_API +inline void rspq_call_deferred(void (*func)(void *), void *arg) { + rspq_syncpoint_new_cb(func, arg); + rspq_flush(); +} + + +/** + * @brief Begin creating a new block. * - * During block creation, the RSP will keep running as usual and - * execute commands that have been already added to the queue. - * - * @note Calls to #rspq_flush are ignored during block creation, as the RSP - * is not going to execute the block commands anyway. + * While a block is being written, all calls to #rspq_write record commands into + * the block without scheduling them for execution. Use #rspq_block_end to close + * the block and obtain a reference, #rspq_block_run to enqueue playback, and + * #rspq_block_free when the block is no longer needed. + * + * Only one block at a time can be open. Calling #rspq_block_begin again before + * #rspq_block_end asserts. The same applies if you are in highpri mode or + * recording a #rspq_queue_t. + * + * During block creation the RSP keeps running as usual and executes commands + * already present in the normal queue. + * + * @note Calls to #rspq_flush are ignored while the block is open; the RSP does + * not execute recorded block commands until the block is run. + * + * @see #rspq_block_begin_reuse + * @see #rspq_block_run + * @see #rspq_block_free + */ +inline void rspq_block_begin(void) { + extern void rspq_block_begin_reuse(rspq_block_t *); + rspq_block_begin_reuse(NULL); +} + +/** + * @brief Begin creation of a block, reusing an existing allocation. + * + * This function is similar to #rspq_block_begin, but it reuses an existing + * allocation instead of allocating a new one. It can be useful to avoid + * allocating and freeing memory unnecessarily. + * + *If @p reuse_block is NULL, this function behaves like #rspq_block_begin. + * + * @note Just like #rspq_block_free, you must not call this function while + * the RSP has a command to run this block in its command queue. If you + * cannot guarantee that, use #rspq_call_deferred to free the previous + * block and allocate a new one with #rspq_block_begin. + * + * @param reuse_block The block to reuse + * + * @see #rspq_block_begin + * @see #rspq_block_run + * @see #rspq_block_free */ -void rspq_block_begin(void); +void rspq_block_begin_reuse(rspq_block_t *reuse_block); + /** * @brief Finish creating a block. @@ -705,6 +886,39 @@ void rspq_block_begin(void); */ rspq_block_t* rspq_block_end(void); +/** + * @brief Sets the target for a placeholder in a block + * @preview + * + * If a block contains calls to placeholders, for example: + * + * @code{.c} + * rspq_block_begin(); + * ... + * rdpq_tex_multi_begin(); + * rdpq_sprite_upload(TILE0, texA, NULL); + * rspq_block_run(RSPQ_BLOCK_PLACEHOLDER_0); + * rdpq_tex_multi_end(); + * ... + * rspq_block_t *block_caller = rspq_block_end(); + * @endcode + * + * Then this function can be used to set the actual target before running it: + * + * @code{.c} + * rspq_block_set_placeholder(RSPQ_BLOCK_PLACEHOLDER_0, the_target_block); + * rspq_block_run(RSPQ_BLOCK_PLACEHOLDER_0); + * @endcode + * + * @param ph the placeholder slot (RSPQ_BLOCK_PLACEHOLDER_0 - RSPQ_BLOCK_PLACEHOLDER_6) + * @param ph_target block the placeholder should point to + */ +LIBDRAGON_PREVIEW_API +void rspq_block_set_placeholder( + rspq_block_t *ph, + rspq_block_t *ph_target +); + /** * @brief Add to the RSP queue a command that runs a block. * @@ -717,8 +931,12 @@ rspq_block_t* rspq_block_end(void); * created, it is possible to call `rspq_block_run(A)` at any point during the * creation of a second block B; this means that B will contain the special * command that will call A. + * + * It is also possible to use a placeholder instead of a specific block. + * This allows the target to be set dynamically later on. + * For that pass 'RSPQ_BLOCK_PLACEHOLDER_0' to 'RSPQ_BLOCK_PLACEHOLDER_6' into this function. * - * @param block The block that must be run + * @param block The block or placeholder that must be run * * @note The maximum depth of nested block calls is 8. */ @@ -728,7 +946,13 @@ void rspq_block_run(rspq_block_t *block); * @brief Free a block that is not needed any more. * * After calling this function, the block is invalid and must not be called - * anymore. + * anymore. Notice that a block that was recently run via #rspq_block_run + * might still be referenced in the RSP queue, and in that case it is invalid + * to free it before the RSP has processed it. + * + * In this case, you must free it once you are absolutely sure that the RSP + * has processed it (eg: at the end of a frame), or use #rspq_call_deferred + * or #rdpq_call_deferred, that handle the synchronization for you. * * @param block The block * @@ -738,6 +962,112 @@ void rspq_block_run(rspq_block_t *block); */ void rspq_block_free(rspq_block_t *block); +/** + * @brief Returns true if a block is currently being built. + * @preview + * + * This function returns true if, and only if, it is called after + * #rspq_block_begin was called and before #rspq_block_end is called. + * Use this function to determine whether a block is currently being recorded. + */ +LIBDRAGON_PREVIEW_API +static inline bool rspq_block_is_recording(void) { + extern rspq_block_t *rspq_block; + return rspq_block != NULL; +} + +/** + * @brief Register a callback to be called when the current block is freed. + * @preview + * + * Calling this function is only valid when a block is currently being recorded + * (see #rspq_block_begin). The callback will be called with the given context + * when the currently recorded block is freed using #rspq_block_free. + * + * It is possible to call this function multiple times during the recording + * of the same block. In that case the callbacks will be called in the reverse + * order that they were passed into this function. + * + * This function is useful for binding the lifetime of resources to that of a block. + * For example if a certain command in an rspq overlay accesses a buffer and that + * command is recorded into a block, it's possible to make sure that the block + * never outlives that buffer without any additionally required scaffolding. + * + * @param cb The callback function + * @param ctx The context that will be passed to the callback + */ +LIBDRAGON_PREVIEW_API +void rspq_block_atexit(void (*cb)(void*), void* ctx); + +/** + * @brief Create a new buffered queue. + * @preview + * + * The queue is created empty and is not active by default. Use + * #rspq_queue_switch to start recording commands into it. + * + * @return A pointer to the newly created queue + */ + LIBDRAGON_PREVIEW_API + rspq_queue_t* rspq_queue_create(void); + + /** + * @brief Switch the current recording target to a queue. + * @preview + * + * After this call, all #rspq_write commands will go into the specified queue, + * and they will not be executed immediately. Use #rspq_queue_run to execute + * the queue. + * + * Passing NULL switches back to the default standard command ring buffer. + * + * @param q The queue to switch to, or NULL to switch back to default + * command ring buffer. + */ + LIBDRAGON_PREVIEW_API + void rspq_queue_switch(rspq_queue_t* q); + + /** + * @brief Execute the RSP commands in the queue. + * @preview + * + * This function will execute all pending RSP commands in the queue, in order. + * It is safe to call this function multiple times: each time, it will execute + * only new commands that were enqueued since the last call. + * + * @param q The queue to execute + */ + LIBDRAGON_PREVIEW_API + void rspq_queue_run(rspq_queue_t* q); + + /** + * @brief Clear a queue contents, keeping its memory for reuse. + * @preview + * + * This function clears the queue contents, basically resetting its internal + * write pointer to the start. + * + * Notice that this function cannot check if the RSP is still running the queue; + * given that the RSP is asynchronous, you must make sure that the RSP is not + * running the queue before calling this function. A good way to do this is + * to use a syncpoint, or double/triple buffer the queue(s). + * + * @param q The queue to clear + */ + LIBDRAGON_PREVIEW_API + void rspq_queue_clear(rspq_queue_t* q); + + /** + * @brief Destroy a queue and free all its memory. + * @preview + * + * After this call, the queue is invalid and must not be used anymore. + * + * @param q Queue to destroy + */ + LIBDRAGON_PREVIEW_API + void rspq_queue_destroy(rspq_queue_t* q); + /** * @brief Start building a high-priority queue. * @@ -795,6 +1125,11 @@ void rspq_highpri_end(void); * all high-priority queues. It is meant for debugging purposes or for situations * in which the high-priority queue is known to be very short and fast to run. * Also note that it is not possible to create syncpoints in the high-priority queue. + * + * The function can either be called while a high-priority queue is being built + * (that is, between #rspq_highpri_begin and #rspq_highpri_end), or + * after the high-priority queue has been closed (that is, after + * #rspq_highpri_end). */ void rspq_highpri_sync(void); diff --git a/include/rspq_constants.h b/include/rspq_constants.h index 52a79ea55..b8231cdd2 100644 --- a/include/rspq_constants.h +++ b/include/rspq_constants.h @@ -8,9 +8,10 @@ #define __RSPQ_INTERNAL #define RSPQ_DEBUG 1 ///< Enable RSPQ debug mode +#define RSPQ_PROFILE 0 ///< Enable RSPQ profiling #define RSPQ_DRAM_LOWPRI_BUFFER_SIZE 0x200 ///< Size of each RSPQ RDRAM buffer for lowpri queue (in 32-bit words) -#define RSPQ_DRAM_HIGHPRI_BUFFER_SIZE 0x80 ///< Size of each RSPQ RDRAM buffer for highpri queue (in 32-bit words) +#define RSPQ_DRAM_HIGHPRI_BUFFER_SIZE 0x200 ///< Size of each RSPQ RDRAM buffer for highpri queue (in 32-bit words) #define RSPQ_DMEM_BUFFER_SIZE 0x100 ///< Size of the RSPQ DMEM buffer (in bytes) @@ -24,7 +25,11 @@ #define RSPQ_DESCRIPTOR_MAX_SIZE RSPQ_DESCRIPTOR_SIZE_MASK ///< Maximum descriptor size /** @brief Internal overlay header size in bytes */ -#define RSPQ_OVERLAY_HEADER_SIZE 16 +#if RSPQ_PROFILE +#define RSPQ_OVERLAY_HEADER_SIZE 30 +#else +#define RSPQ_OVERLAY_HEADER_SIZE 28 +#endif #define RSPQ_BLOCK_MIN_SIZE 64 ///< Minimum / maximum size of a block's chunk (contiguous memory buffer) #define RSPQ_BLOCK_MAX_SIZE 8192 ///< Maximum block size @@ -65,4 +70,12 @@ #define ASSERT_INVALID_OVERLAY 0xFF01 ///< A command is referencing an overlay that is not registered #define ASSERT_INVALID_COMMAND 0xFF02 ///< The requested command is not defined in the overlay +#define RSPQ_PROFILE_CSLOT_WAIT_CPU (RSPQ_MAX_OVERLAYS+0) ///< Profile slot for CPU wait +#define RSPQ_PROFILE_CSLOT_WAIT_RDP (RSPQ_MAX_OVERLAYS+1) ///< Profile slot for RDP wait +#define RSPQ_PROFILE_CSLOT_WAIT_RDP_SYNCFULL (RSPQ_MAX_OVERLAYS+2) ///< Profile slot for RDP SYNC_FULL wait +#define RSPQ_PROFILE_CSLOT_WAIT_RDP_SYNCFULL_MULTI (RSPQ_MAX_OVERLAYS+3) ///< Profile slot for RDP SYNC_FULL multi wait +#define RSPQ_PROFILE_CSLOT_OVL_SWITCH (RSPQ_MAX_OVERLAYS+4) ///< Profile slot for overlay switch +#define RSPQ_PROFILE_CSLOT_COUNT 5 ///< Profile slot count +#define RSPQ_PROFILE_SLOT_COUNT (RSPQ_MAX_OVERLAYS+5) ///< Total profile slot count + #endif diff --git a/include/rspq_profile.h b/include/rspq_profile.h new file mode 100644 index 000000000..b582c8752 --- /dev/null +++ b/include/rspq_profile.h @@ -0,0 +1,52 @@ +/** + * @file rspq_profile.h + * @author Dennis Heinze + */ +#ifndef __LIBDRAGON_RSPQ_PROFILE_H +#define __LIBDRAGON_RSPQ_PROFILE_H + +#include +#include "rspq_constants.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Profiling data of a single slot (for example an overlay) */ +typedef struct rspq_profile_slot_s { + uint64_t total_ticks; ///< The total number of rcp ticks that were spent running in this slot + uint64_t sample_count; ///< The number of individual samples that were recorded + const char *name; ///< The name of this slot, if it is used; NULL otherwise +} rspq_profile_slot_t; + +/** @brief RSPQ Profiling data */ +typedef struct rspq_profile_data_s { + rspq_profile_slot_t slots[RSPQ_PROFILE_SLOT_COUNT]; ///< The list of slots + uint64_t total_ticks; ///< The total elapsed rcp ticks since the last reset + uint64_t rdp_busy_ticks; ///< The accumulated ticks sampled from DP_BUSY + uint64_t frame_count; ///< The number of recorded frames since the last reset +} rspq_profile_data_t; + +/** @brief Start the rspq profiler */ +void rspq_profile_start(void); + +/** @brief Stop the rspq profiler */ +void rspq_profile_stop(void); + +/** @brief Reset the rspq profiler and discard any recorded samples */ +void rspq_profile_reset(void); + +/** @brief Mark the start of the next frame to the rspq profiler */ +void rspq_profile_next_frame(void); + +/** @brief Dump the recorded data to the console */ +void rspq_profile_dump(void); + +/** @brief Copy the recorded data */ +void rspq_profile_get_data(rspq_profile_data_t *data); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/rtc.h b/include/rtc.h index 1fd5e373f..908b4a160 100644 --- a/include/rtc.h +++ b/include/rtc.h @@ -1,15 +1,17 @@ /** * @file rtc.h * @author Christopher Bonhage - * @author thekovic * @brief Real-Time Clock Subsystem * @ingroup rtc */ #ifndef __LIBDRAGON_RTC_H #define __LIBDRAGON_RTC_H -#include + #include +#include +#include +#include "preview.h" /** * @defgroup rtc Real-Time Clock Subsystem @@ -17,116 +19,102 @@ * @brief Real-time clock interface. * @author Christopher Bonhage * - * The Joybus real-time clock is a cartridge peripheral that uses a battery - * to power a clock that tracks the date, time, and day of the week. The - * real-time clock keeps running even when the N64 is powered-off. The - * Joybus RTC is accessed through the serial interface (SI) similar to EEPROM - * and controllers. The Joybus RTC was only ever available on one official - * cartridge that was only available in Japan: Dōbutsu no Mori (Animal Forest). - * Many emulators and flash carts include support for the Animal Forest RTC, - * which makes it possible to include real-time clock functionality in homebrew! - * There is also a real-time clock included in the N64DD hardware, which uses - * a different interface and is not currently supported by libdragon. - * - * To check if the real-time clock is available, call #rtc_init. - * To read the current time from the real-time clock, call #rtc_get. - * Once the RTC subsystem is initialized, you can also use ISO C Time functions - * to get the current time, for example: `time(NULL)` will return the number of - * seconds elapsed since the UNIX epoch (January 1, 1970 at 00:00:00). - * To check if the real-time clock supports writes, call #rtc_is_writable. - * To write a new time to the real-time clock, call #rtc_set. - * - * This subsystem handles decoding and encoding the date/time from its internal - * format into a struct called #rtc_time_t, which contains integer values for - * year, month, day-of-month, day-of-week, hour, minute, and second. - * - * The Joybus RTC contains 3 "blocks" (or zones) which contain 8 bytes of data: - * Block 0 contains a half-word control register and opaque calibration data. - * Block 1 is unused and unsupported. See notes below. - * Block 2 contains the current date/time as packed binary-coded decimal. - * - * Animal Forest did not use block 1 at all, so most emulators do not bother to - * implement it. Theoretically, block 1 could be used as 8-bytes of SRAM-backed - * storage, but this is not supported by libdragon's Real-Time Clock Subsystem. - * If you need storage, consider using a standard cartridge save type or saving - * to a Controller Pak. - * - * (As of July 2021) Joybus RTC does not work in combination with any EEPROM - * save type on EverDrive64 3.0 or X7. To have the best compatibility and player - * experience, it is not recommended to use the EEPROM + RTC ROM configuration. - * This is a bug in the EverDrive64 firmware and not a system limitation imposed - * by the Joybus protocol or Serial Interface. - * - * Unfortunately, since only one game ever used Joybus RTC (and that game was - * later re-released on the GameCube in English), real-time clock support in - * emulators and flash carts can be incomplete, inaccurate, or non-existent. - * Many emulators do not actually implement the Joybus RTC write command and - * always respond with the host device's current local time. Some emulators - * and flash carts support writing to RTC but will not persist the date/time - * after resetting or powering-off. You can run the `rtctest` example ROM on - * your preferred emulator or flash cart to what RTC support is available. - * - * The only reliable way to check if writes are actually supported is to write - * a time to the RTC and read the time back out. Many emulators that do - * support RTC reads will silently ignore RTC writes. You should detect - * whether writes are supported using #rtc_is_writable so that you can - * conditionally show the option to change the time if it's supported. If the - * RTC supports writes, it is safe to call #rtc_set to set the date and time. - * - * Due to the inaccurate and inconsistent behavior of RTC reproductions that - * currently exist, this subsystem trades-off complete accuracy with the actual - * Animal Forest RTC in favor of broader compatibility with the various quirks - * and bugs that exist in real-world scenarios like emulators and flash carts. - * - * Some notable examples of RTC support in the ecosystem (as of July 2021): - * - * 64drive hw2 fully implements Joybus RTC including writes, but requires - * delays after setting the time (see #JOYBUS_RTC_WRITE_FINISHED_DELAY). - * - * EverDrive64 3.0 and X7 partially support Joybus RTC, with caveats: The RTC - * must be explicitly enabled in the OS or with a ROM header configuration; - * RTC will not be detected if the EEPROM save type is used; RTC writes are - * not supported through the SI, so changing the time must be done in the OS. - * - * UltraPIF fully implements an emulated Joybus RTC that can be accessed even - * when the cartridge does not include the real-time clock circuitry. - * - * Special thanks to marshallh and jago85 for their hard work and research - * reverse-engineering and documenting the inner-workings of the Joybus RTC. + * The RTC subsystem integrates several hardware clocks with newlib time + * functions. Supported sources are identified by #rtc_source_t: + * + * - **Joybus** (#RTC_SOURCE_JOYBUS): PIF/controller-port RTC on retail + * hardware and most flash carts. This is preferred when present and + * reporting a good crystal and battery. + * - **64DD** (#RTC_SOURCE_DD): RTC in the 64DD ASIC when a disk drive is + * attached. It can coexist with the Joybus RTC; initialization may read + * the 64DD first, then switch to Joybus if that RTC is detected and healthy. + * - **BBPlayer / iQue** (#RTC_SOURCE_BB): RTC on iQue Player hardware only; + * it does not coexist with the other sources. + * + * Call #rtc_init_async to start detection without blocking, or #rtc_init to + * block until the subsystem is ready (typically within a few milliseconds). + * #rtc_init returns `true` if any **hardware** source was selected, `false` + * if the subsystem fell back to the software clock (#RTC_SOURCE_NONE). + * + * If no hardware clock is usable, the subsystem keeps time in software for + * the current session only; that value does not survive reset or power loss. + * + * Initialization installs hooks so POSIX `gettimeofday` / `settimeofday` and + * ISO C `time` work as usual (for example `time(NULL)` for seconds since the + * UNIX epoch). Use `struct tm` with `gmtime`, `localtime`, and `mktime` as on + * any hosted C environment. + * + * Use #rtc_is_source_available, #rtc_get_source, and #rtc_set_source to query + * or change the active source. #rtc_set_source resynchronizes cached time from + * the newly selected hardware. + * + * Each source accepts a different range of `time_t` values. Query + * #rtc_get_supported_range or #rtc_get_source_supported_range instead of + * hard-coding limits. Roughly: Joybus is represented from 1900 through 2099 + * (some real chips, e.g. UltraPIF, effectively only support from 2000); 64DD + * uses two-digit years interpreted as 1996–1999 and 2000–2095; BBPlayer spans + * 2000–2099 by default (a century bit can extend the hardware range; LibDragon + * does not enable it automatically). The software fallback allows 1970 through + * 2099 (#RTC_SOFT_TIMESTAMP_MIN / #RTC_SOFT_TIMESTAMP_MAX). When the cached + * time would leave the active source’s range, reads wrap inside that range. + * + * Writes outside the supported range fail with #RTC_EBADTIME. Prefer storing + * game-specific “fiction time” as an offset from the wall clock rather than + * programming impossible dates into hardware. * * @{ */ -/** - * @brief Structure for storing RTC time data. - */ -typedef struct rtc_time_t -{ - /** @brief Year. [1900-20XX] */ - uint16_t year; - /** @brief Month. [0-11] */ - uint8_t month; - /** @brief Day of month. [1-31] */ - uint8_t day; - /** @brief Hours. [0-23] */ - uint8_t hour; - /** @brief Minutes. [0-59] */ - uint8_t min; - /** @brief Seconds. [0-59] */ - uint8_t sec; - /** @brief Day of week. [0-6] (Sun-Sat) */ - uint8_t week_day; -} rtc_time_t; - #ifdef __cplusplus extern "C" { #endif +/** @brief RTC source values. */ +typedef enum { + /** @brief Software RTC source */ + RTC_SOURCE_NONE = 0, + /** @brief Joybus RTC source */ + RTC_SOURCE_JOYBUS = 1, + /** @brief 64DD RTC source */ + RTC_SOURCE_DD = 2, + /** @brief iQue/BBPlayer RTC source */ + RTC_SOURCE_BB = 3, +} rtc_source_t; + +/** + * @name RTC error codes + * @{ + */ +/** @brief RTC Operation successful */ +#define RTC_ESUCCESS 0 +/** @brief RTC source in unavailable. */ +#define RTC_ENOCLOCK -1 +/** @brief RTC source is not operational. */ +#define RTC_EBADCLOCK -2 +/** @brief RTC clock time is not representable. */ +#define RTC_EBADTIME -3 +/** @} */ + +/** @brief Get the string representation of an RTC error code. + * @preview + */ +LIBDRAGON_PREVIEW_API +const char *rtc_error_str( int error ); + +/** @brief Software RTC minimum timestamp (1970-01-01 00:00:00) */ +#define RTC_SOFT_TIMESTAMP_MIN 0 +/** @brief Software RTC maximum timestamp (2099-12-31 23:59:59) */ +#define RTC_SOFT_TIMESTAMP_MAX 4102444799 + +/** @brief Structure representing an RTC timestamp range. */ +typedef struct { + time_t min; ///< Minimum timestamp + time_t max; ///< Maximum timestamp +} rtc_range_t; + /** - * @brief High-level convenience helper to initialize the RTC subsystem. - * - * The RTC Subsystem depends on the libdragon Timer Subsystem, so make sure - * to call #timer_init before calling #rtc_init! + * @brief Initialize the RTC subsystem asynchronously. + * @preview * * Some flash carts require the RTC to be explicitly enabled before loading * the ROM file. Some emulators and flash carts do not support RTC at all. @@ -134,118 +122,128 @@ extern "C" { * This function will detect if the RTC is available and if so, will * prepare the RTC so that the current time can be read from it. * - * This operation may take up to 50 milliseconds to complete. + * This will also hook the RTC into the newlib gettimeofday and settimeofday + * functions, so you will be able to use the ISO C time functions. * - * This will also hook the RTC into the newlib gettimeofday function, so - * you will be able to use the ISO C time functions if RTC is available. + * This operation may take up to 5 milliseconds to complete, but does not + * block the CPU while detecting and initializing the RTC hardware. * - * @return whether the RTC is present and supported by the RTC Subsystem. + * Use #rtc_get_source to determine if a hardware RTC source was detected. */ -bool rtc_init( void ); - -/** - * @brief Close the RTC Subsystem, disabling system hooks. - * - * Unhooks the RTC from the newlib gettimeofday function. - * This will cause subsequent calls to gettimeofday to error with ENOSYS. - * - * The only reason you should ever need to call this is if you need to - * stop the Timer Subsystem, which the RTC Subsystem depends on. If you - * do need to do this, make sure to call #rtc_close BEFORE #timer_close - * and then call #rtc_init again after you call #timer_init to restart - * the Timer Subsystem! - */ -void rtc_close( void ); +LIBDRAGON_PREVIEW_API +void rtc_init_async( void ); /** - * @brief Determine whether the RTC supports writing the time. + * @brief Initialize the RTC subsystem. * - * Some emulators and flash carts do not support writing to the RTC, so - * this function makes an attempt to detect silent write failures and will - * return `false` if it is unable to change the time on the RTC. + * Some flash carts require the RTC to be explicitly enabled before loading + * the ROM file. Some emulators and flash carts do not support RTC at all. * - * This function is useful if your program wants to conditionally offer the - * ability to set the time based on hardware/emulator support. + * This function will detect if the RTC is available and if so, will + * prepare the RTC so that the current time can be read from it. * - * Unfortunately this operation may introduce a slight drift in the clock, - * but it is the only way to determine if the RTC supports the write command. + * This will also hook the RTC into the newlib gettimeofday and settimeofday + * functions, so you will be able to use the ISO C time functions. * - * This operation will take approximately 1 second to complete. + * This operation may take up to 5 milliseconds to complete. * - * @return whether RTC writes appear to be supported + * @return whether any supported hardware RTC source was initialized */ -bool rtc_is_writable( void ); +bool rtc_init( void ); /** - * @brief Read the current date/time from the real-time clock. - * - * If the RTC is not detected or supported, this function will - * not modify the destination rtc_time parameter. - * - * Your code should call this once per frame to update the #rtc_time_t - * data structure. The RTC Subsystem maintains a cache of the - * most-recent RTC time that was read and will only perform an - * actual RTC read command if the cache is invalidated. The - * destination rtc_time parameter will be updated regardless of - * the cache validity. - * - * Cache will invalidate every #RTC_GET_CACHE_INVALIDATE_TICKS. - * Calling #rtc_set will also invalidate the cache. - * - * If an actual RTC read command is needed, this function can take - * a few milliseconds to complete. - * - * @param[out] rtc_time - * Destination pointer for the RTC time data structure - * - * @return whether the rtc_time destination pointer data was modified + * @brief Close the RTC subsystem, disabling system hooks. */ -bool rtc_get( rtc_time_t * rtc_time ); +void rtc_close( void ); /** - * @brief High-level convenience helper to set the RTC date/time. - * - * Prepares the RTC for writing, sets the new time, and resumes the clock. - * - * This function will take approximately 570 milliseconds to complete. + * @brief Check if the specified RTC source is usable by the subsystem. + * @preview + */ +LIBDRAGON_PREVIEW_API +bool rtc_is_source_available( rtc_source_t source ); + +/** + * @brief Get the current source clock for the subsystem. + * @preview + */ +LIBDRAGON_PREVIEW_API +rtc_source_t rtc_get_source( void ); + +/** + * @brief Switch the preferred source clock for the subsystem. + * @preview * - * Unfortunately, the best way to ensure that writes to the RTC have - * actually finished is by waiting for a fixed duration. Emulators may not - * accurately reflect this, but this delay is necessary on real hardware. + * By default, the subsytem will use to the first available source, + * but some games may wish to specify the preferred RTC source. * - * @param[in] rtc_time - * Source pointer for the RTC time data structure + * This function will automatically resynchronize the time with the new clock. * - * @return false if the RTC does not support being set + * @retval RTC_ESUCCESS if the source was successfully set + * @retval RTC_ENOCLOCK if the source is not available + * @retval RTC_EBADCLOCK if the source is not operational */ -bool rtc_set( rtc_time_t * rtc_time ); +LIBDRAGON_PREVIEW_API +int rtc_set_source( rtc_source_t source ); /** - * @brief Calculate sane values for arbitrary time inputs. + * @brief Get the supported timestamp range for the given RTC source. + * @preview * - * If your time inputs are already sane, nothing should change. - * This function will clamp date/time values within the expected ranges, - * including the correct day-of-month based on year/month. It will also - * recalculate the day-of-week based on the clamped year/month/day. + * @param source the RTC source to check * - * This is useful to call while the player is adjusting the time after each - * input to ensure that the date being set always makes sense before they - * actually confirm and commit the updated date/time. The rtctest example - * demonstrates a user-interface for setting the time with live validation. - * - * Internally, RTC cannot represent dates before 1990-01-01, although some - * RTC implementations (like UltraPIF) only support dates after 2000-01-01. - * - * For highest compatibility, it is not recommended to set the date past - * 2038-01-19 03:14:07 UTC, which is the UNIX timestamp Epochalypse. - * - * Special thanks to networkfusion for providing the algorithm to - * calculate day-of-week from an arbitrary date. + * @return the supported timestamp range for the source + */ +LIBDRAGON_PREVIEW_API +rtc_range_t rtc_get_source_supported_range( rtc_source_t source ); + +/** + * @brief Get the supported timestamp range for the current RTC source. + * @preview * - * @param[in,out] rtc_time - * Pointer to the RTC time data structure + * @return the supported timestamp range for the current source clock + */ +LIBDRAGON_PREVIEW_API +rtc_range_t rtc_get_supported_range( void ); + +/************************************** + * DEPRECATED + **************************************/ + +/// @cond + +/** + * @brief Structure for storing RTC time data. + * @deprecated Use `struct tm` and `time_t` from instead. */ -void rtc_normalize_time( rtc_time_t * rtc_time ); +typedef struct rtc_time_t +{ + /** @brief Year. [1900-20XX] */ + uint16_t year; + /** @brief Month. [0-11] */ + uint8_t month; + /** @brief Day of month. [1-31] */ + uint8_t day; + /** @brief Hours. [0-23] */ + uint8_t hour; + /** @brief Minutes. [0-59] */ + uint8_t min; + /** @brief Seconds. [0-59] */ + uint8_t sec; + /** @brief Day of week. [0-6] (Sun-Sat) */ + uint8_t week_day; +} rtc_time_t; + +__attribute__((deprecated("just assume it's always writable"))) +static inline bool rtc_is_writable( void ) { return true; } + +__attribute__((deprecated("use time(NULL) instead"))) +bool rtc_get( rtc_time_t *rtc_time ); + +__attribute__((deprecated("use settimeofday instead"))) +bool rtc_set( rtc_time_t *rtc_time ); + +/// @endcond #ifdef __cplusplus } diff --git a/include/samplebuffer.h b/include/samplebuffer.h index 706f618f1..35a10f044 100644 --- a/include/samplebuffer.h +++ b/include/samplebuffer.h @@ -1,79 +1,84 @@ /** * @file samplebuffer.h * @author Giovanni Bajo - * @brief Sample buffer + * @brief Sample buffer * @ingroup mixer */ #ifndef __LIBDRAGON_SAMPLEBUFFER_H #define __LIBDRAGON_SAMPLEBUFFER_H + #include #include +#include "preview.h" #ifdef __cplusplus extern "C" { #endif -/** - * Tagged pointer to an array of samples. It contains both the void* - * sample pointer, and byte-per-sample information (encoded as shift value). +/// @cond +typedef struct waveform_s waveform_t; +/// @endcond + +/** + * Pointer to the sample area (historically a tagged pointer; the low bits are + * unused and the width of each unit lives in #samplebuffer_t::unit_bytes). */ typedef uint32_t sample_ptr_t; /** - * SAMPLES_BPS_SHIFT extracts the byte-per-sample information from a sample_ptr_t. - * Byte-per-sample is encoded as shift value, so the actual number of bits is - * 1 << BPS. Valid shift values are 0, 1, 2 (which corresponds to 1, 2 or 4 - * bytes per sample). + * Extract the raw void* to the sample array from a samplebuffer. */ -#define SAMPLES_BPS_SHIFT(buf) ((buf)->ptr_and_flags & 3) +#define SAMPLES_PTR(buf) ((void*)(uintptr_t)((buf)->ptr_and_flags)) /** - * SAMPLES_PTR extract the raw void* to the sample array. The size of array - * is not encoded in the tagged pointer. Notice that it is implemented with a - * XOR because on MIPS it's faster than using a reverse mask. + * Maximum window (in units) that #samplebuffer_get may request across the ring + * wrap. Matches one MIX_CHANNEL input span plus RSP sample-cache overread. + * Also the default size of the mirrored tail (see #samplebuffer_t::margin_units). */ -#define SAMPLES_PTR(buf) (void*)((buf)->ptr_and_flags ^ SAMPLES_BPS_SHIFT(buf)) +#define SAMPLEBUFFER_MARGIN_UNITS 128 + +/** @brief Units that #samplebuffer_prefetch keeps ready ahead of the mixer. */ +#define SAMPLEBUFFER_PREFETCH_UNITS SAMPLEBUFFER_MARGIN_UNITS /** - * SAMPLES_PTR_MAKE create a tagged pointer, given a pointer to an array of - * samples and a byte-per-sample value (encoded as shift value). + * @brief Size of the mirrored tail, in units, for the given append granularity. + * @preview + * + * The caller that allocates the memory of a samplebuffer has to add this on + * top of the usable size it wants (see #samplebuffer_t::margin_units). */ -#define SAMPLES_PTR_MAKE(ptr, bps) ((sample_ptr_t)(ptr) | (bps)) +LIBDRAGON_PREVIEW_API +inline int samplebuffer_margin_units(int append_units) +{ + return append_units > SAMPLEBUFFER_MARGIN_UNITS + ? append_units : SAMPLEBUFFER_MARGIN_UNITS; +} /** - * samplebuffer_t is a circular buffer of samples. It is used by the mixer - * to store and cache the samples required for playback on each channel. - * The mixer creates a sample buffer for each initialized channel. The size - * of the buffers is calculated for optimal playback, and might grow depending - * on channel usage (what waveforms are played on each channel). + * samplebuffer_t holds samples used by the mixer to feed the RSP. * * The mixer follows a "pull" architecture. During mixer_poll, it will call * samplebuffer_get() to extract samples from the buffer. If the required * samples are not available, the sample buffer will callback the waveform * decoder to produce more samples, through the WaveformRead API. The * waveform read function will push samples into the buffer via samplebuffer_append, - * so that they become available for the mixer. The decoder can be configured - * with samplebuffer_set_decoder. + * so that they become available for the mixer. * - * The current implementation of samplebuffer does not achieve full zero copy, - * because when the buffer is full, it is flushed and samples that need to - * be preserved (that is, already in the buffer but not yet played back) are - * copied back at the beginning of the buffer with the CPU. This limitation - * exists because the RSP ucode (rsp_audio.S) isn't currently able to "wrap around" - * in the sample buffer. In future, this limitation could be lifted to achieve - * full zero copy. + * Layout: `[0, size)` is the circular sample area; `[size, size+margin_units)` + * is a contiguous spill/mirror region so that any append of up to + * #margin_units units and any #samplebuffer_get window of up to + * #SAMPLEBUFFER_MARGIN_UNITS units is always linear in physical memory + * (required by RSP DMA). size * unit_bytes is kept a multiple of 8 so that + * absolute waveform positions preserve their 2-byte phase in the samplebuffer + * (dma_read / dfs compatibility). * - * The sample buffer tries to always stay 8-byte aligned to simplify operations - * of decoders that might need to use DMA transfers (either PI DMA or RSP DMA). - * To guarantee this property, #WaveformRead must collaborate by decoding - * the requested number of samples. If WaveformRead decodes a different - * number of samples, the alignment might be lost. Moreover, it always guarantees - * that the buffer has the same 2-byte phase of the waveforms (that is, odd - * samples of the waveforms are stored at odd addresses in memory); this is - * the minimal property required by #dma_read (libdragon's optimized PI DMA - * transfer for unaligned addresses). + * Physical cleanup has a single home: #samplebuffer_get mirrors the leading + * units into the tail when a window straddles the wrap, and a pending spill + * from a wrapping append is flushed back to the start before the next + * get/append/flush/undo. The samplebuffer itself does not wait on the RSP; + * that is the mixer's job (see mixer_poll_async). * * In general, the sample buffer assumes that the contained data is committed * to physical memory, not just CPU cache. It is responsibility of the client @@ -82,44 +87,82 @@ typedef uint32_t sample_ptr_t; */ typedef struct samplebuffer_s { /** - * Tagged pointer to the actual buffer. Lower bits contain bit-per-shift. + * Pointer to the sample area (8-byte aligned, uncached). */ sample_ptr_t ptr_and_flags; - /** - * Size of the buffer (in samples). + /** + * @preview + * Bytes per logical unit counted by wpos/widx/size. + * PCM: 1, 2, or 4. Compressed frame stores: e.g. 9 for mono VADPCM. + */ + uint8_t unit_bytes; + + /** + * Usable size in units (excludes the tail margin). **/ int size; + /** + * @preview + * Granularity in units of the appends done by the producer, or 0 if + * unknown: declared via #waveform_t::append_units. The mirrored tail is + * sized to #margin_units = MAX(#SAMPLEBUFFER_MARGIN_UNITS, this), so a + * single append of that size is always contiguous. + */ + LIBDRAGON_PREVIEW_SYM + int append_units; + + /** + * @preview + * Size of the mirrored tail in units: MAX(#SAMPLEBUFFER_MARGIN_UNITS, + * #append_units). Fixed for the life of a configuration. + */ + LIBDRAGON_PREVIEW_SYM + int margin_units; + /** * Absolute position in the waveform of the first sample - * in the sample buffer (the sample at index 0). It keeps track of - * which part of the waveform this sample buffer contains. + * currently valid in the samplebuffer. */ int wpos; /** - * Write pointer in the sample buffer (expressed as index of samples). - * Since sample buffers are always filled from index 0, it is also - * the number of samples stored in the buffer. + * Number of valid units currently stored in the samplebuffer. */ int widx; /** - * Read pointer in the sample buffer (expressed as index of samples). - * It remembers which sample was last read. Assuming a forward - * streaming, it is used by the sample buffer to discard unused samples - * when not needed anymore. + * Read cursor relative to wpos (first unit still needed for playback). */ int ridx; /** - * Value of the "next" sample to be loaded into the sample buffer, - * to continue a linear reading. This is used to notify the wv_read - * function whether a seeking was performed or not. + * Next absolute position expected for a linear read (for seeking detection). */ int wnext; + /** + * @brief Pointer to the state of the waveform decoder. + * @preview + */ + LIBDRAGON_PREVIEW_SYM + void *state; + + /** + * @brief Allocated state size + * @preview + */ + LIBDRAGON_PREVIEW_SYM + int state_size; + + /** + * @preview + * Waveform being played back on this sample buffer. + */ + LIBDRAGON_PREVIEW_SYM + waveform_t *wave; + /** * wv_read is invoked by samplebuffer_get whenever more samples are * requested by the mixer. See #WaveformRead for more information. @@ -127,9 +170,37 @@ typedef struct samplebuffer_s { WaveformRead wv_read; /** - * wv_ctx is the opaque pointer to pass as context to decoder functions. + * @preview + * Ticket of the most recent async PI DMA into this buffer (0 = none). + * Producers set it when issuing dma_read_async; consumers wait via + * #samplebuffer_dma_wait before touching the bytes or freeing memory. */ - void *wv_ctx; + LIBDRAGON_PREVIEW_SYM + uint64_t dma_ticket; + + /** + * @preview + * Total bytes allocated for the sample area (usable size + margin). */ + LIBDRAGON_PREVIEW_SYM + int capacity_bytes; + + /** + * @preview + * Physical index (0 .. size-1) of #wpos in the circular sample area. + * A unit at relative offset `rel` from wpos lives at + * `(head + rel) % size`. Advances when the oldest units are discarded. + */ + LIBDRAGON_PREVIEW_SYM + int head; + + /** + * @preview + * Units of the latest append that spilled past #size into the margin and + * still need to be copied back to the start of the sample area. 0 means + * nothing pending. Flushed by the next get/append/flush/undo. + */ + LIBDRAGON_PREVIEW_SYM + int spill; } samplebuffer_t; /** @@ -140,163 +211,123 @@ typedef struct samplebuffer_s { * in the uncached segment and not loaded in any CPU cacheline. It is * strongly advised to allocate the buffer via #malloc_uncached, that takes * care of these constraints. - * + * + * The memory buffer will be used for storing samples and for the state. + * Part of the sample area is reserved as a tail margin for contiguous + * RSP DMA across the wrap. + * * @param[in] buf Sample buffer * @param[in] uncached_mem Memory buffer to use. Must be 8-byte aligned, * and in the uncached segment. - * @param[in] size Size of the memory buffer, in bytes. + * @param[in] size Size of the memory buffer portion to use for samples (in bytes) + * @param[in] state_size Size of the memory buffer portion to use for the state (in bytes) */ -void samplebuffer_init(samplebuffer_t *buf, uint8_t *uncached_mem, int size); +void samplebuffer_init(samplebuffer_t *buf, uint8_t *uncached_mem, int size, int state_size); /** * @brief Return true if the samplebuffer is initialized. - * - * @param buf Sample buffer - * @return true If the sample buffer is initialized. - * @return false If the sample buffer is not initialized. */ bool samplebuffer_is_inited(samplebuffer_t *buf); /** - * @brief Configure the bit width of the samples stored in the buffer. - * - * Valid values for "bps" are 1, 2, or 4: 1 can be used for 8-bit mono samples, - * 2 for either 8-bit interleaved stereo or 16-bit mono, and 4 for 16-bit - * interleaved stereo. - * - * @param[in] buf Sample buffer - * @param[in] bps Bytes per sample. + * @brief Configure the logical unit size (bytes per wpos/widx step). + * + * After this call, wpos/widx/size are expressed in units of `unit_bytes` + * each (e.g. 2 for 16-bit mono PCM, 9 for mono VADPCM frames). The buffer + * must be empty. */ -void samplebuffer_set_bps(samplebuffer_t *buf, int bps); +void samplebuffer_set_unit_bytes(samplebuffer_t *buf, int unit_bytes); /** - * Connect a waveform reader callback to this sample buffer. The waveform - * will be use to produce samples whenever they are required by the mixer - * as playback progresses. + * @brief Configure the bit width of PCM samples stored in the buffer. * - * "read" is the main decoding function, that is invoked to produce a specified - * number of samples. Normally, the function is invoked by #samplebuffer_get, - * whenever the mixer requests more samples. See #WaveformRead for more - * information. - * - * @param[in] buf Sample buffer - * @param[in] read Waveform reading function, that produces samples. - * @param[in] ctx Opaque context that will be passed to the read - * function. + * Valid values are 8, 16, or 32 (total bits per interleaved frame). + * Wrapper for #samplebuffer_set_unit_bytes with `bps / 8`. */ -void samplebuffer_set_waveform(samplebuffer_t *buf, WaveformRead read, void *ctx); +static inline void samplebuffer_set_bps(samplebuffer_t *buf, int bps) { + samplebuffer_set_unit_bytes(buf, bps / 8); +} + +/** + * Connect a waveform reader callback to this sample buffer. + */ +void samplebuffer_set_waveform(samplebuffer_t *buf, waveform_t *wave, WaveformRead read); /** * @brief Get a pointer to specific set of samples in the buffer (zero-copy). - * + * * "wpos" is the absolute waveform position of the first sample that the * caller needs access to. "wlen" is the number of requested samples. - * - * The function returns a pointer within the sample buffer where the samples - * should be read, and optionally changes "wlen" with the maximum number of - * samples that can be read. "wlen" is always less or equal to the requested value. - * - * If the samples are available in the buffer, they will be returned immediately. - * Otherwise, if the samplebuffer has a sample decoder registered via - * samplebuffer_set_decoder, the decoder "read" function is called once to - * produce the samples. - * - * If "wlen" is changed with a value less than "wlen", it means that - * not all samples were available in the buffer and it was not possible to - * generate more, so the caller should not loop calling this function, but - * rather use what was obtained and possibly pad with silence. - * - * @param[in] buf Sample buffer - * @param[in] wpos Absolute waveform position of the first samples to - * return. - * @param[in,out] wlen Number of samples to return. After return, it is - * modified with the actual number of samples that - * have been returned. - * @return Pointer to samples. + * wpos must be monotonic between seeks. */ void* samplebuffer_get(samplebuffer_t *buf, int wpos, int *wlen); /** - * @brief Append samples into the buffer (zero-copy). + * @brief Start fetching samples that will be requested later (zero-wait). + * @preview * - * "wlen" is the number of samples that the caller will append. + * Fetches the part of [wpos, wpos+wlen) that the buffer does not hold yet, + * leaving any PI DMA in flight: the next #samplebuffer_get finds the samples + * already there and only has to wait for a transfer that ran meanwhile. * - * The function returns a pointer within the sample buffer where the samples - * should be written. The samples to be written to physical memory, not just - * CPU cache, and to enforce this, the function returns a pointer in the - * uncached segment. Most of the times, we expect samples to be generated - * or manipulated via RSP/DMA anyway. - * - * The function is meant only to "append" samples, as in add samples that are - * consecutive within the waveform to the ones already stored in the sample - * buffer. This is necessary because #samplebuffer_t can only store a single - * range of samples of the waveform; there is no way to hold two disjoint - * ranges. + * This is best-effort: the call does nothing unless the waveform declares + * #waveform_t::async_read (there would be nothing to overlap with), and + * likewise if the window is already available, if it is not contiguous with + * what the buffer holds (a seek is coming, so anything fetched now would be + * discarded), or if the samplebuffer has no room for it. + */ +LIBDRAGON_PREVIEW_API +void samplebuffer_prefetch(samplebuffer_t *buf, int wpos, int wlen); + +/** + * @brief Granularity, in units, that keeps appends 8-byte aligned. + * @preview * - * For instance, if the sample buffer currently contains 50 samples - * starting from position 100 in the waverform, the next call to - * samplebuffer_append will append samples starting at 150. + * A producer that writes through the RSP (#waveform_t::rsp_written) deposits + * its samples with SP DMA, which needs an 8-byte aligned destination. Appends + * of whole blocks preserve that alignment on their own, but a producer that + * undoes part of what it appended — a block codec trimming the samples past the + * end of a waveform — has to round the part it keeps to this granularity, or + * the next append lands mid-word. #samplebuffer_append checks it. + */ +LIBDRAGON_PREVIEW_API +inline int samplebuffer_align_units(const samplebuffer_t *buf) +{ + int n = 1; + while ((n * buf->unit_bytes) & 7) + n++; + return n; +} + +/** + * @brief Append samples into the buffer (zero-copy). * - * If required, samplebuffer_append will discard older samples to make space - * for the new ones, through #samplebuffer_discard. It will only discard samples - * that come before the "wpos" specified in the last #samplebuffer_get call, so - * to make sure that nothing required for playback is discarded. If there is - * not enough space in the buffer, it will assert. - * - * @param[in] buf Sample buffer - * @param[in] wlen Number of samples to append. - * @return Pointer to the area where new samples can be written. + * Returns a contiguous uncached pointer where the caller must write `wlen` + * units. `wlen` must not exceed #samplebuffer_t::margin_units when the write would wrap past + * the usable end (declare large chunks in #waveform_t::append_units so the + * margin covers them). */ void* samplebuffer_append(samplebuffer_t *buf, int wlen); /** * @brief Remove the a specified number of samples from the tail of the buffer. - * - * This function removes the last \a wlen samples from the buffer. It can be - * used to revert the behavior of a #samplebuffer_append call, if the data - * written to the buffer was too much. - * - * A common situation is an implementation of a waveform codec with fixed - * audio frames. The last frame at the end of the waveform will likely need - * to be truncated, but the compressor would have likely padded it with zeros - * to make it the same size as the others. In this case, at playing time, - * it is possible to call #samplebuffer_append to append the whole frame, - * but then remove the unneeded padding with a call to #samplebuffer_undo. - * - * @param buf Sample buffer - * @param wlen Number of samples to remove */ void samplebuffer_undo(samplebuffer_t *buf, int wlen); /** - * Discard all samples from the buffer that come before a specified - * absolute waveform position. - * - * This function can be used to discard samples that are not needed anymore - * in the sample buffer. "wpos" specifies the absolute position of the first - * sample that should be kept: all samples that come before will be discarded. - * This function will silently do nothing if there are no samples to discard. - * - * @param[in] buf Sample buffer - * @param[in] wpos Absolute waveform position of the first sample that - * must be kept. + * Flush (reset) the sample buffer to empty status, discarding all samples. */ -void samplebuffer_discard(samplebuffer_t *buf, int wpos); +void samplebuffer_flush(samplebuffer_t *buf); /** - * Flush (reset) the sample buffer to empty status, discarding all samples. - * - * @param[in] buf Sample buffer. + * @preview + * Wait for any in-flight async PI DMA into this buffer to complete. */ -void samplebuffer_flush(samplebuffer_t *buf); +LIBDRAGON_PREVIEW_API +void samplebuffer_dma_wait(samplebuffer_t *buf); /** * Close the sample buffer. - * - * After calling close, the sample buffer must be initialized again before - * using it. - * - * @param[in] buf Sample buffer. */ void samplebuffer_close(samplebuffer_t *buf); diff --git a/include/scratch.h b/include/scratch.h new file mode 100644 index 000000000..416664fd3 --- /dev/null +++ b/include/scratch.h @@ -0,0 +1,231 @@ +/** + * @file scratch.h + * @author Giovanni Bajo + * @brief Public API for the scratch allocator. + * + * The scratch allocator provides malloc/free-like functions backed by memory + * that is allocated from a separate section of the main heap. + * + * It is intended for short-lived allocations such as load buffers, + * decompression workspaces, staging areas, and temporary conversion data. + * Long-lived allocations should continue to use the regular heap API. + * The goal is to reduce memory fragmentation, by avoiding leaving holes in + * the main heap caused by these short-lived allocations. + * + * API invariants: + * - pointers returned by scratch_malloc/scratch_calloc/scratch_realloc must + * be released with scratch_free (not free); + * - only pointers from this allocator may be passed to scratch_free/realloc; + */ + + #ifndef LIBDRAGON_SCRATCH_H + #define LIBDRAGON_SCRATCH_H + + #include + #include + #include "preview.h" + + #ifdef __cplusplus + extern "C" { + #endif + + /** + * @brief Allocate memory from the scratch heap. + * @preview + * + * Allocates @p size bytes from the scratch allocator. + * The returned pointer is suitably aligned for normal C object access. + * + * @param size Number of bytes to allocate. + * @return Pointer to the allocated memory, or NULL if there is not enough + * memory. + * + * @note Memory returned by this function is uninitialized. + * + * @see scratch_free + * @see scratch_calloc + * @see scratch_realloc + */ + LIBDRAGON_PREVIEW_API + void *scratch_malloc(size_t size); + + /** + * @brief Allocate zero-initialized memory from the scratch heap. + * @preview + * + * Allocates enough memory for @p count objects of @p size bytes each and clears + * the allocation to zero. + * + * This function performs overflow checking on the multiplication + * @p count * @p size. + * + * @param count Number of elements to allocate. + * @param size Size of each element, in bytes. + * + * @return Pointer to the allocated zero-filled memory, or NULL if there is not + * enough memory or if the requested size overflows. + * + * @see scratch_malloc + * @see scratch_free + */ + LIBDRAGON_PREVIEW_API + void *scratch_calloc(size_t count, size_t size); + + /** + * @brief Resize a scratch allocation. + * @preview + * + * Changes the size of the allocation pointed to by @p ptr to at least + * @p size bytes. + * + * The contents are preserved up to the minimum of the old and new sizes. Newly + * allocated bytes, if any, are uninitialized. + * + * @param ptr Pointer previously returned by #scratch_malloc(), #scratch_calloc(), + * or #scratch_realloc(). It may be NULL. + * @param size New requested size in bytes. + * + * @return Pointer to the resized allocation, or NULL if the allocation could not + * be resized. If NULL is returned because of allocation failure, the + * original allocation remains valid. + * + * @note If @p ptr is NULL, this function behaves like scratch_malloc(). + * @note If @p size is zero, this function frees @p ptr and returns NULL. + * + * @see scratch_malloc + * @see scratch_free + */ + LIBDRAGON_PREVIEW_API + void *scratch_realloc(void *ptr, size_t size); + + /** + * @brief Free a scratch allocation. + * @preview + * + * Releases a block previously returned by any scratch allocator: + * #scratch_malloc(), #scratch_calloc(), #scratch_realloc(), + * #scratch_memalign(), or #scratch_malloc_uncached(). The single + * entry point transparently handles cached vs uncached pointer views + * and direct vs aligned allocations; callers don't need to track + * which allocator produced the pointer. + * + * Passing NULL is allowed and has no effect. + * + * @param ptr Pointer to the scratch allocation to free, or NULL. + */ + LIBDRAGON_PREVIEW_API + void scratch_free(void *ptr); + + /** + * @brief Allocate uncached memory from the scratch heap. + * @preview + * + * Allocates @p size bytes from the scratch allocator and returns an uncached + * (KSEG1) mapping of the buffer. This mirrors #malloc_uncached() and is + * intended for short-lived buffers shared between CPU and RSP/RDP DMA paths + * (decoder workspaces, staging surfaces, etc.) where the coherency cost of + * cached access would otherwise force manual flushes around every transfer. + * + * The underlying allocation is 16-byte aligned and the size is rounded up to + * a multiple of 16 bytes, so the buffer exclusively owns its cachelines and + * cannot suffer false sharing with neighbouring allocations. + * + * Free with the standard #scratch_free(); it handles cached/uncached views + * transparently. + * + * @param size Number of bytes to allocate. + * @return Uncached pointer to the allocated memory, or NULL if there is not + * enough memory. + * + * @note Memory returned by this function is uninitialized. + * + * @see scratch_free + * @see malloc_uncached + */ + LIBDRAGON_PREVIEW_API + void *scratch_malloc_uncached(size_t size); + + /** + * @brief Allocate aligned memory from the scratch heap. + * @preview + * + * Allocates @p size bytes from the scratch allocator and returns a pointer + * aligned to @p alignment bytes. @p alignment must be a power of two and + * at least the natural scratch alignment (16 bytes); requests smaller than + * 16 are clamped up. + * + * Free with #scratch_free(). + * + * @param alignment Required alignment in bytes (power of two, >= 16). + * @param size Number of bytes to allocate. + * @return Pointer to the aligned allocation, or NULL on failure. + */ + LIBDRAGON_PREVIEW_API + void *scratch_memalign(size_t alignment, size_t size); + + /** + * @brief Check internal consistency of the scratch heap. + * @preview + * + * Runs a full consistency check over the scratch heap metadata. + * + * This function validates allocator invariants and may assert + * if corruption or inconsistent state is detected. + */ + LIBDRAGON_PREVIEW_API + void scratch_check(void); + +/** + * @brief Scratch allocator statistics. + */ +typedef struct { + size_t live_bytes; ///< Sum of user-requested sizes for live allocations + size_t peak_bytes; ///< Peak value of live_bytes since boot + size_t live_blocks; ///< Number of currently live scratch allocations + size_t reserved_bytes; ///< Bytes currently reserved by the scratch allocator +} scratch_stats_t; + + /** + * @brief Return statistics about scratch allocator usage. + * @preview + * + * This function writes all current scratch allocator counters to @p stats. + * + * @param[out] stats + * Pointer to destination structure. + */ +LIBDRAGON_PREVIEW_API +void scratch_get_stats(scratch_stats_t *stats); + + /** + * @brief Check whether the scratch heap has no live allocations. + * @preview + * + * @return true if there are no live scratch allocations, false otherwise. + */ + LIBDRAGON_PREVIEW_API + bool scratch_empty(void); + + /** + * @brief Check whether an address belongs to the scratch heap. + * @preview + * + * Returns true if @p ptr falls inside the scratch heap reservation. The cached + * and uncached views of the same scratch address are both recognised. + * + * Intended for code paths that don't know up-front which allocator owns a + * pointer (e.g. fallbacks between scratch and the main heap) and need to + * dispatch to the correct deallocator. + * + * @param ptr Pointer to test. + * @return true if the address is inside the scratch heap, false otherwise + * (including NULL). + */ + LIBDRAGON_PREVIEW_API + bool scratch_owns(const void *ptr); + + #ifdef __cplusplus + } + #endif + + #endif /* LIBDRAGON_SCRATCH_H */ diff --git a/include/sf64.h b/include/sf64.h new file mode 100644 index 000000000..f1e1f6aa5 --- /dev/null +++ b/include/sf64.h @@ -0,0 +1,101 @@ +/** + * @file sf64.h + * @author Giovanni Bajo + * @brief SoundFont 2 banks converted to SF64 + * @ingroup mixer + * + * SF64 is the runtime format produced by audioconv64 from a SoundFont 2 + * (`.sf2`) file. Load a bank with #sf64_load and free it with #sf64_close. + * Playback is performed by #sf64_synth_t (see sf64_synth.h). + */ +#ifndef LIBDRAGON_SF64_H +#define LIBDRAGON_SF64_H + +#include "preview.h" +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief SF64 sound bank. */ +typedef struct sf64_bank_s sf64_bank_t; + +/** + * @brief Load an SF64 bank from disk. + * @preview + * + * Samples are streamed from the filesystem during playback rather than + * preloaded into memory. Free with #sf64_close. + * + * @param fn Filename with filesystem prefix (e.g. `rom:/bank.sf64`) + * @return Loaded bank + */ +LIBDRAGON_PREVIEW_API +sf64_bank_t *sf64_load(const char *fn); + +/** + * @brief Close a bank and free all associated resources. + * @preview + * + * @param bank Bank returned by #sf64_load + */ +LIBDRAGON_PREVIEW_API +void sf64_close(sf64_bank_t *bank); + +/** + * @brief Find a preset by MIDI bank and program number. + * @preview + * + * @param bank Loaded bank + * @param midi_bank MIDI bank (0–127 melodic, 128 percussion) + * @param program MIDI program (0–127) + * @return Preset index, or -1 if not found + */ +LIBDRAGON_PREVIEW_API +int sf64_find_preset(sf64_bank_t *bank, int midi_bank, int program); + +/** + * @brief Number of presets in the bank. + * @preview + * + * Presets are indexed `0 .. count-1` for #sf64_preset_name / #sf64_preset_id. + */ +LIBDRAGON_PREVIEW_API +int sf64_preset_count(sf64_bank_t *bank); + +/** + * @brief Name of the preset at @p index (NUL-terminated, owned by the bank). + * @preview + */ +LIBDRAGON_PREVIEW_API +const char *sf64_preset_name(sf64_bank_t *bank, int index); + +/** + * @brief MIDI bank and program of the preset at @p index. + * @preview + * + * @param bank Loaded bank + * @param index Preset index (`0 .. #sf64_preset_count - 1`) + * @param[out] midi_bank MIDI bank number (may be NULL) + * @param[out] program MIDI program number (may be NULL) + */ +LIBDRAGON_PREVIEW_API +void sf64_preset_id(sf64_bank_t *bank, int index, int *midi_bank, int *program); + +/** + * @brief First region of a preset that matches @p key and @p velocity. + * @preview + * + * @param bank Loaded bank + * @param preset_index Preset index (`0 .. #sf64_preset_count - 1`) + * @param key MIDI key (0–127) + * @param velocity MIDI velocity (1–127) + * @return Region index, or -1 if none match + */ +LIBDRAGON_PREVIEW_API +int sf64_find_region(sf64_bank_t *bank, int preset_index, int key, int velocity); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/sf64_synth.h b/include/sf64_synth.h new file mode 100644 index 000000000..9e2b45145 --- /dev/null +++ b/include/sf64_synth.h @@ -0,0 +1,276 @@ +/** + * @file sf64_synth.h + * @author Giovanni Bajo + * @brief Polyphonic synthesizer for SF64 sound banks + * @ingroup mixer + * + * A synthesizer plays notes from an #sf64_bank_t on a range of mixer channels. + * Create with #sf64_synth_create, reserve channels with #sf64_synth_set_channels, + * select programs with #sf64_synth_set_program, then drive MIDI channels (0–15) + * with #sf64_synth_note_on / #sf64_synth_note_off. Advance time with + * #sf64_synth_process using the same sample counts passed to #mixer_poll. + * + * The bank is borrowed and must outlive the synthesizer. For MID64 playback, + * use #sf64_synth_midi_target. + */ +#ifndef LIBDRAGON_SF64_SYNTH_H +#define LIBDRAGON_SF64_SYNTH_H + +#include +#include +#include "preview.h" +#include "sf64.h" +#include "mixer.h" +#include "midi_target.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Number of MIDI channels tracked by #sf64_synth_t. */ +#define SF64_MIDI_CHANNELS 16 + +/** @brief MIDI semantics mode for #sf64_synth_t. */ +typedef enum { + SF64_MODE_NATIVE, ///< Arbitrary SF2 banks; program change on all channels + SF64_MODE_GM1, ///< General MIDI Level 1 (channel 10 = percussion) +} sf64_synth_mode_t; + +/** @brief SF64 synthesizer. */ +typedef struct sf64_synth_s sf64_synth_t; + +/** + * @brief Return the synthesizer as a #midi_target_t for MID64 playback. + * @preview + * + * Controllers mapped: CC7 volume, CC11 expression, CC10 pan, CC64 sustain, + * CC0/CC32 bank select, CC6/CC38/CC100/CC101 RPN (pitch bend sensitivity), + * CC120 All Sound Off, CC121 Reset All Controllers, CC123 All Notes Off. + * #midi_target_ops_t::system_reset handles GM1 System On. + */ +LIBDRAGON_PREVIEW_API +midi_target_t *sf64_synth_midi_target(sf64_synth_t *synth); + +/** + * @brief Create a synthesizer that plays from @p bank. + * @preview + * + * Starts in #SF64_MODE_NATIVE. Each of the #SF64_MIDI_CHANNELS starts at + * bank/program 0 if present, except MIDI channel 10 (index 9) which defaults + * to percussion bank 128. Volume and expression are 127, pan is center, pitch + * bend is center with a ±200 cent range. Call #sf64_synth_set_channels before + * the first #sf64_synth_note_on. Free with #sf64_synth_close. + * + * @param bank Bank returned by #sf64_load + * @return New synthesizer + */ +LIBDRAGON_PREVIEW_API +sf64_synth_t *sf64_synth_create(sf64_bank_t *bank); + +/** + * @brief Select Native or General MIDI Level 1 semantics. + * @preview + * + * Does not silence voices or reset controllers. GM1 System On both switches + * to #SF64_MODE_GM1 and performs a full reset via #midi_target_ops_t::system_reset. + * + * @param synth Synthesizer + * @param mode Desired mode + */ +LIBDRAGON_PREVIEW_API +void sf64_synth_set_mode(sf64_synth_t *synth, sf64_synth_mode_t mode); + +/** + * @brief Current #sf64_synth_mode_t. + * @preview + * + * @param synth Synthesizer + */ +LIBDRAGON_PREVIEW_API +sf64_synth_mode_t sf64_synth_get_mode(sf64_synth_t *synth); + +/** + * @brief Stop all sounding notes and free the synthesizer. + * @preview + * + * Does not close the bank. + * + * @param synth Synthesizer returned by #sf64_synth_create + */ +LIBDRAGON_PREVIEW_API +void sf64_synth_close(sf64_synth_t *synth); + +/** + * @brief Reserve a contiguous range of mixer channels for this synthesizer. + * @preview + * + * Stops any notes currently sounding. Must be called before + * #sf64_synth_note_on. Polyphony is limited to @p num_channels; when the pool + * is full, older or quieter voices are stolen. @p priority is the base + * voice-stealing priority for this synthesizer (e.g. #MIXER_PRIORITY_MUSIC); + * envelope phase adds a small offset above it. + * + * @param synth Synthesizer + * @param first_channel First mixer channel index (see #mixer_init) + * @param num_channels Number of channels to reserve (`>= 1`) + * @param priority Base stealing priority for this synth's voices + */ +LIBDRAGON_PREVIEW_API +void sf64_synth_set_channels(sf64_synth_t *synth, int first_channel, + int num_channels, int priority); + +/** + * @brief Select bank and program for subsequent notes on @p midi_channel. + * @preview + * + * Does not affect notes that are already sounding on that channel. + * + * @param synth Synthesizer + * @param midi_channel MIDI channel (0–15) + * @param bank MIDI bank (0–127 melodic, 128 percussion) + * @param program MIDI program (0–127) + * @return true if the preset exists in the bank + */ +LIBDRAGON_PREVIEW_API +bool sf64_synth_set_program(sf64_synth_t *synth, int midi_channel, + int bank, int program); + +/** + * @brief Set MIDI CC7 volume for @p midi_channel (0–127). + * @preview + * + * Applies immediately to notes already sounding on that channel. + * + * @param synth Synthesizer + * @param midi_channel MIDI channel (0–15) + * @param volume Volume (0–127) + */ +LIBDRAGON_PREVIEW_API +void sf64_synth_set_volume(sf64_synth_t *synth, int midi_channel, int volume); + +/** + * @brief Mute or unmute a MIDI channel (debug / mixer-style solo). + * @preview + * + * When muted, new note-ons are ignored and sounding voices on that channel + * are hard-cut. Survives MIDI resets and CC7 volume; not part of GM protocol. + * + * @param synth Synthesizer + * @param midi_channel MIDI channel (0–15) + * @param mute true to mute, false to unmute + */ +LIBDRAGON_PREVIEW_API +void sf64_synth_mute_channel(sf64_synth_t *synth, int midi_channel, bool mute); + +/** + * @brief Set MIDI CC11 expression for @p midi_channel (0–127). + * @preview + * + * Same voice-update rules as #sf64_synth_set_volume. + * + * @param synth Synthesizer + * @param midi_channel MIDI channel (0–15) + * @param expression Expression (0–127) + */ +LIBDRAGON_PREVIEW_API +void sf64_synth_set_expression(sf64_synth_t *synth, int midi_channel, int expression); + +/** + * @brief Set MIDI CC10 pan for @p midi_channel (0 = left, 64 = center, 127 = right). + * @preview + * + * Combined with each region's pan. Same voice-update rules as volume. + * + * @param synth Synthesizer + * @param midi_channel MIDI channel (0–15) + * @param pan Pan (0–127) + */ +LIBDRAGON_PREVIEW_API +void sf64_synth_set_pan(sf64_synth_t *synth, int midi_channel, int pan); + +/** + * @brief Set pitch bend for @p midi_channel (0–16383, center 8192). + * @preview + * + * Updates the pitch of notes already sounding on that channel. The bend spans + * the channel pitch range (default ±200 cents; changeable via RPN). + * + * @param synth Synthesizer + * @param midi_channel MIDI channel (0–15) + * @param pitch_bend 14-bit bend value + */ +LIBDRAGON_PREVIEW_API +void sf64_synth_set_pitch_bend(sf64_synth_t *synth, int midi_channel, int pitch_bend); + +/** + * @brief Set MIDI CC64 sustain pedal for @p midi_channel (0–127). + * @preview + * + * Values `>= 64` hold the pedal: note-offs keep sounding until the pedal is + * released (`< 64`). + * + * @param synth Synthesizer + * @param midi_channel MIDI channel (0–15) + * @param value Pedal value (0–127) + */ +LIBDRAGON_PREVIEW_API +void sf64_synth_set_sustain(sf64_synth_t *synth, int midi_channel, int value); + +/** + * @brief Start a note on @p midi_channel's current program. + * @preview + * + * Starts every matching region for @p key and @p velocity as one note. + * Allocation is all-or-nothing: either every matching region starts, or none + * do. When the channel pool is full, existing voices are stolen (preferring + * released, held-by-sustain, sustain, then quieter/older voices). A velocity + * of 0 is treated as #sf64_synth_note_off. Starting a region with a non-zero + * exclusive class silences other sounding regions of the same preset and class. + * + * @param synth Synthesizer + * @param midi_channel MIDI channel (0–15) + * @param key MIDI key (0–127) + * @param velocity MIDI velocity (1–127; 0 = note-off) + * @return Note identity (`> 0`) if at least one voice started, else 0 + */ +LIBDRAGON_PREVIEW_API +uint32_t sf64_synth_note_on(sf64_synth_t *synth, int midi_channel, + int key, int velocity); + +/** + * @brief Release the oldest still-held note for @p key on @p midi_channel. + * @preview + * + * Layered regions of that note release together. While the sustain pedal is + * down they keep sounding until #sf64_synth_set_sustain releases the pedal. + * Call #sf64_synth_process so release fades can finish and free channels. + * Repeated calls peel stacked note-ons on the same key from oldest to newest. + * + * @param synth Synthesizer + * @param midi_channel MIDI channel (0–15) + * @param key MIDI key that should be released + */ +LIBDRAGON_PREVIEW_API +void sf64_synth_note_off(sf64_synth_t *synth, int midi_channel, int key); + +/** + * @brief Tell the synthesizer that @p num_samples of audio have elapsed. + * @preview + * + * Call this with the same sample counts you feed to #mixer_poll (or from a + * #mixer_add_event callback). Pass 0 to query the next deadline without + * advancing time. + * + * @param synth Synthesizer + * @param num_samples Elapsed output samples (`>= 0`) + * @return Samples until the next scheduled change, or `< 0` + * if nothing is pending + */ +LIBDRAGON_PREVIEW_API +int sf64_synth_process(sf64_synth_t *synth, int num_samples); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/skc.h b/include/skc.h new file mode 100644 index 000000000..46e586511 --- /dev/null +++ b/include/skc.h @@ -0,0 +1,77 @@ +/** + * @file skc.h + * @author Giovanni Bajo + * @brief SKC - Secure Kernel Calls + * @ingroup ique + * + * This module defines the interfaces to do the secure kernel calls (SKC) + * on the iQue Player. These are basically syscalls into the kernel to + * perform security-sensitive operations like cryptography, launching + * an application or so on. + */ + +#ifndef LIBDRAGON_BB_SKC_H +#define LIBDRAGON_BB_SKC_H + +#include +#include "preview.h" + +///@cond +typedef struct { + void *ticket; + void *ticket_certs[5]; + void *ticket_cmd[5]; +} bb_ticket_bundle_t; +///@endcond + +/** + * @brief Retrieve the console unique ID + * @preview + * + * @param id Will be filled with the console unique ID + * @return 0 on success, negative on failure + */ +LIBDRAGON_PREVIEW_API +int skc_get_id(uint32_t *id); + +/** + * @brief Launch an application, performing some verifications + * @preview + * + * @param address Entrypoint of the application in RDRAM + * @return -1 on error, or if the application exits and returns. + */ +LIBDRAGON_PREVIEW_API +int skc_launch(void *address); + +/** + * @brief Prepare SK for launching an application. + * @preview + * + * @param bundle TBC + * @param crls TBC + * @param recrypt_list TBC + * @return Negative numbers for errors, 0 on success + */ +LIBDRAGON_PREVIEW_API +int skc_launch_setup(bb_ticket_bundle_t *bundle, void *crls, void *recrypt_list); + +/** + * @brief Exit the application immediately, jumping back to iQue OS. + * @preview + */ +LIBDRAGON_PREVIEW_API +void skc_exit(void) __attribute__((noreturn)); + +/** + * @brief Reset the watchdog timer to keep the application alive + * @preview + * + * On iQue, applications running as trials are expected to call this function + * to keep themselves alive. Otherwise, the secure kernel (via the kernel + * timer) might reset them. + */ +LIBDRAGON_PREVIEW_API +void skc_keep_alive(void); + +#endif diff --git a/include/sprite.h b/include/sprite.h index 0e2c2df1b..a40ba0264 100644 --- a/include/sprite.h +++ b/include/sprite.h @@ -1,16 +1,17 @@ /** * @file sprite.h * @author Giovanni Bajo - * @author Dennis Heinze * @brief 2D Graphics * @ingroup graphics */ #ifndef __LIBDRAGON_SPRITE_H #define __LIBDRAGON_SPRITE_H + #include #include #include +#include "preview.h" #ifdef __cplusplus extern "C" { @@ -87,6 +88,7 @@ typedef struct sprite_detail_s #define SPRITE_FLAGS_TEXFORMAT 0x1F ///< Pixel format of the sprite #define SPRITE_FLAGS_OWNEDBUFFER 0x20 ///< Flag specifying that the sprite buffer must be freed by sprite_free +#define SPRITE_FLAGS_NODATA 0x40 ///< Sprite does not contain data in the base section; all data is in the extended section #define SPRITE_FLAGS_EXT 0x80 ///< Sprite contains extended information (new format) @@ -102,7 +104,7 @@ typedef struct sprite_detail_s * * @param fn Filename of the sprite, including filesystem specifier. * For instance: "rom:/hero.sprite" to load from DFS. - * @return sprite_t* The loaded sprite + * @return The loaded sprite */ sprite_t *sprite_load(const char *fn); @@ -120,7 +122,7 @@ sprite_t *sprite_load(const char *fn); * * @param buf Pointer to the sprite file data * @param sz Size of the sprite file buffer - * @return sprite_t* The loaded sprite + * @return The loaded sprite */ sprite_t *sprite_load_buf(void *buf, int sz); @@ -167,7 +169,7 @@ surface_t sprite_get_pixels(sprite_t *sprite); * * @param sprite The sprite to access * @param num_level The number of LOD level. 0 is the main sprite. - * @return surface_t The surface containing the data. + * @return The surface containing the data. */ surface_t sprite_get_lod_pixels(sprite_t *sprite, int num_level); @@ -189,7 +191,7 @@ surface_t sprite_get_lod_pixels(sprite_t *sprite, int num_level); * @param sprite The sprite to access * @param info The detail information struct to fill if needed * @param infoparms The detail texture sampling struct to fill if needed - * @return surface_t The surface containing the data. + * @return The surface containing the data. */ surface_t sprite_get_detail_pixels(sprite_t *sprite, sprite_detail_t *info, rdpq_texparms_t *infoparms); @@ -224,6 +226,16 @@ surface_t sprite_get_tile(sprite_t *sprite, int h, int v); */ uint16_t* sprite_get_palette(sprite_t *sprite); +/** + * @brief Get number of colors actually used in palette + * @preview + * + * @param sprite The sprite to access + * @return Number of used palette colors + */ +LIBDRAGON_PREVIEW_API +int sprite_get_palette_used_colors(sprite_t *sprite); + /** * @brief Get a copy of the RDP texparms, optionally stored within the sprite. * @@ -270,6 +282,18 @@ int sprite_get_lod_count(sprite_t *sprite); */ bool sprite_fits_tmem(sprite_t *sprite); +/** + * @brief Return true if the sprite is in SHQ format + * @preview + * + * This is a special sprite made of two mipmaps (one I4 and one RGBA16) + * that must be displayed using subtractive blending. + * + * @param sprite The sprite to access + * @return True if the sprite is in SHQ format, false otherwise + */ +LIBDRAGON_PREVIEW_API +bool sprite_is_shq(sprite_t *sprite); #ifdef __cplusplus } diff --git a/include/sram.h b/include/sram.h new file mode 100644 index 000000000..f2e65c5cb --- /dev/null +++ b/include/sram.h @@ -0,0 +1,82 @@ +/** + * @file sram.h + * @author thekovic + * @brief SRAM access functions for N64 cartridges + * + * This header defines the interface for accessing SRAM present in certain N64 + * cartridges for the purposes of saving game data. It provides functions to + * initialize the SRAM subsystem, read from SRAM, and write to SRAM. + * + */ + +#ifndef LIBDRAGON_SRAM_H +#define LIBDRAGON_SRAM_H + +#include +#include +#include "preview.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define SRAM_ADDRESS 0x08000000 ///< Base address of SRAM in PI address space. + +/** + * @brief Initialize the SRAM subsystem + * @preview + * + * This function configures the PI DOM2 registers to enable access to the SRAM + * in the cartridge. It must be called before any other SRAM functions are used. + */ +LIBDRAGON_PREVIEW_API +void sram_init(void); + +/** + * @brief Detect if SRAM is present in the cartridge + * @preview + * + * This function checks if the cartridge has SRAM available for use. + * + * @return Size of available SRAM if is detected, -1 otherwise. + */ +LIBDRAGON_PREVIEW_API +int sram_detect(void); + +/** + * @brief Read data from SRAM + * @preview + * + * This function reads data from the SRAM in the cartridge and stores them into + * the provided destination buffer. To read the entire content of the SRAM, + * the length should be set to the size of the SRAM as detected by sram_detect(). + * + * @param dst Destination buffer to store the read data. + * @param offset Offset in SRAM to read from. Allowed range is 0 to 0x7FFF. + * @param len Number of bytes to read. + * @return Number of bytes read, or negative value in case of error. + */ +LIBDRAGON_PREVIEW_API +int sram_read(void* dst, size_t offset, size_t len); + +/** + * @brief Write data to SRAM + * @preview + * + * This function writes data to the SRAM in the cartridge from the provided + * source buffer. To write the entire content of the SRAM, the length should + * be set to the size of the SRAM as detected by sram_detect(). + * + * @param src Source buffer containing the data to write. + * @param offset Offset in SRAM to write to. Allowed range is 0 to 0x7FFF. + * @param len Number of bytes to write. + * @return Number of bytes written, or negative value in case of error. + */ +LIBDRAGON_PREVIEW_API +int sram_write(const void* src, size_t offset, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/string_hash.h b/include/string_hash.h new file mode 100644 index 000000000..f614abb80 --- /dev/null +++ b/include/string_hash.h @@ -0,0 +1,56 @@ +/** + * @file string_hash.h + * @author Giovanni Bajo + * @brief Fast string hashing function with compile-time support + */ +#ifndef LIBDRAGON_STRING_HASH_H +#define LIBDRAGON_STRING_HASH_H + +#include +#include "debug.h" + +#ifdef __cplusplus +extern "C" { +#endif + +///@cond +extern uint32_t __string_hash(const char *str); + +__attribute__((always_inline)) +inline uint32_t __string_hash_literal(const char *str) +{ + uint32_t hash = 5381; const uint32_t prime = 33; + +#define NEXT_CHAR() \ + if (*str == '\0') return hash; \ + hash = hash * prime + (*str++); + + NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); + NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); + NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); + NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); + NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); + NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); + NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); + NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); NEXT_CHAR(); + + assertf(0, "String too long for hash_literal_string"); +} + +///@endcond + +/** + * @brief Hash a string at compile-time or run-time. + * + * This function allows for a quick hash of a string, either at compile-time + * (for literals) or at run-time (for variables). It is using the djb2 hash. + * + * @hideinitializer + */ +#define string_hash(s) (__builtin_constant_p(s) ? __string_hash_literal(s) : __string_hash(s)) + +#ifdef __cplusplus +} +#endif + +#endif // LIBDRAGON_STRING_HASH_H diff --git a/include/subtitles.h b/include/subtitles.h new file mode 100644 index 000000000..694a0f57d --- /dev/null +++ b/include/subtitles.h @@ -0,0 +1,250 @@ +/** + * @file subtitles.h + * @author Giovanni Bajo + * @brief Subtitles API via the SUB64 format + * @ingroup video + * + * SUB64 is libdragon-specific format for subtitles, created by the videoconv64 + * tool when converting video files with embedded subtitles, or standalone + * subtitle files. + * + * It allows to store subtitles in a linear stream of events, with each event + * containing a timestamp and a text string. It has very basic support for + * styling (bold, italic, and bold+italic), regions (just 3: bottom, top, center), + * and for multiple subtitles visible at the same time (up to 3: one per region). + * + * This library provides a simple API to load and parse SUB64 files. It also + * offers 3 different backends to visualize subtitles on the screen: + * + * - Via rdpq_text.h API to perform hardware-accelerated text rendering. This is + * the most efficient way to render subtitles on top of videos. You can provide + * your own font files in font64 format. Coloring can be adjusted at runtime, + * including foreground and background color and opacity. + * + */ +#ifndef LIBDRAGON_SUBTITLES_H +#define LIBDRAGON_SUBTITLES_H + +#include +#include +#include "preview.h" +#include "graphics.h" + +#ifdef __cplusplus +extern "C" { +#endif + +///@cond +typedef struct subtitles_s subtitles_t; +///@endcond + +/** @brief Subtitle cue: a single subtitle visible on screen */ +typedef struct subtitle_cue_s { + /** + * @brief The text of the cue, in UTF-8 encoding. + * + * The text might contain markup tags for styling (bold, italic, underline), + * in rdpq_text format (see rdpq_text.h). + * + * It might also contain embedded newlines (\n), though it is recommended + * to also handle word-wrapping in case the text is too long for a single line. + */ + const char *text; + + /** + * @brief The region hint for the cue. + * + * This is a hint for the renderer to place the cue in one of the three + * regions: bottom (0), top (1), center (2). + */ + int8_t region_hint; + + /** + * @brief Whether the cue is visible at the current frame. + * + * subtitles_get_current_cue() will return cues with a few frames of advance, + * before they should be shown, in case the renderer needs to perform + * advance preparation. + */ + bool visible; +} subtitle_cue_t; + +/** + * @brief Load subtitles from a SUB64 file. + * @preview + * + * SUB64 is libdragon-specific format for subtitles, created by the videoconv64 + * tool when converting video files with embedded subtitles, or standalone + * subtitle files. + * + * + * @param fn Filename to load (including filesystem prefix, eg: "rom:/subtitles.sub64") + * @return Handle to the loaded subtitles + */ +LIBDRAGON_PREVIEW_API +subtitles_t* subtitles_load(const char *fn); + +/** + * @brief Free the subtitles handle. + * @preview + * + * This function frees the subtitles handle and all the resources associated with it. + * + * @param sub Handle to the subtitles + */ +LIBDRAGON_PREVIEW_API +void subtitles_free(subtitles_t *sub); + + +/** + * @brief Advance to the next frame. + * @preview + * + * This function advances to the next frame. It is used to synchronize the subtitles + * with the video stream. It should be called once per frame. + * + * @param sub Handle to the subtitles + */ +LIBDRAGON_PREVIEW_API +void subtitles_next_frame(subtitles_t *sub); + +/** + * @brief Get the current cue. + * @preview + * + * This function returns the current cues. It is used to get the current cues + * to render on the screen. + * + * @param sub Handle to the subtitles + * @param cues Array to store the current cues (if not NULL) + * @param max_cues Maximum number of cues to store in the array. Since + * sub64 supports up to 3 subtitles visible at the same time (one per region), + * the max_cues parameter should be 3. + * @return If cues is not NULL, the function will return the + * number of cues stored in the array. Otherwise, + * the function will return the number of cues available. + */ +LIBDRAGON_PREVIEW_API +int subtitles_get_current_cues(subtitles_t *sub, subtitle_cue_t *cues, int max_cues); + +/** + * @brief Seek to a specific frame index. + * @preview + * + * This function seeks to a specific frame index. Seeking is a relatively + * expensive operation, and should be used only when necessary. + * + * @param sub Handle to the subtitles + * @param frame_idx Frame index to seek to + */ +LIBDRAGON_PREVIEW_API +void subtitles_seek(subtitles_t *sub, int frame_idx); + + +/** + * @name Subtitle renderers + * + * These are different subtitle renderers that can be used to render subtitles + * on the screen. They are all based on the subrenderer_t interface. + * + * @{ + */ + +/** @brief A subtitle renderer object */ +typedef struct subrenderer_s subrenderer_t; + + +/** @brief Configuration parameters for the RDPQ subtitle renderer */ +typedef struct { + /** + * @brief Font filenames (normal, bold, italic) + * + * This array must be filled with the filenames of the font64 files to use + * for rendering subtitles (including filesystem prefix, eg: "rom:/arial.font64"). + * + * The first font is the most important one (normal). If not provided, a + * builtin font will be used. + * + * The other fonts (bold, italic) are optional. If not provided, the normal font + * will be used for the other styles as well. + */ + const char *fonts[3]; + + /** + * @brief Background box color+alpha (default: no background) + * + * This color is used to draw a semi-transparent background box behind + * the subtitles, to improve readability. If the alpha component is 0, + * no background box is drawn. + */ + color_t bkg_color; + +} subrenderer_rdpq_parms_t; + + +/** + * @brief Create a RDPQ-based subtitle renderer. + * @preview + * + * This renderer uses the rdpq_text API to render subtitles on the screen. + * It is the most efficient way to render subtitles on top of videos. + * + * @param parms Parameters for the RDPQ renderer (fonts, background color, etc.) + * @return Handle to the created subtitle renderer + */ +LIBDRAGON_PREVIEW_API +subrenderer_t* subrenderer_create_rdpq(subrenderer_rdpq_parms_t *parms); + +/** + * @brief Create an EIA-608 subtitle renderer. + * @preview + * + * @return subrenderer_t* + */ +LIBDRAGON_PREVIEW_API +subrenderer_t* subrenderer_create_eia608(void); + +/** + * @brief Set a custom frame size for the subtitle renderer. + * @preview + * + * By default, the subtitle renderer assumes a frame size equal to the + * display size. If the video being played has a different size, this function + * can be used to set the correct frame size, so that subtitles are rendered + * in the correct position. + * + * @param base Subtitle renderer handle + * @param width Frame width + * @param height Frame height + */ +LIBDRAGON_PREVIEW_API +void subrenderer_set_frame_size(subrenderer_t *base, int width, int height); + +/** + * @brief Render the subtitles for the current frame. + * @preview + * + * @param renderer Subtitle renderer handle + * @param cues Array of cues to render + * @param num_cues Number of cues in the array + */ +LIBDRAGON_PREVIEW_API +void subrenderer_render(subrenderer_t *renderer, subtitle_cue_t *cues, int num_cues); + +/** + * @brief Free the subtitle renderer. + * @preview + * + * @param renderer Subtitle renderer handle + */ +LIBDRAGON_PREVIEW_API +void subrenderer_free(subrenderer_t *renderer); + +/** @} */ + + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/surface.h b/include/surface.h index 35e656890..654ce49ab 100644 --- a/include/surface.h +++ b/include/surface.h @@ -1,7 +1,7 @@ /** * @file surface.h - * @author Giovanni Bajo * @author Dennis Heinze + * @author Giovanni Bajo * @brief Surface buffers used to draw images * @ingroup graphics * @@ -287,7 +287,7 @@ inline bool surface_has_owned_buffer(const surface_t *surface) * @param width Width of the surface in pixels * @param height Height of the surface in pixels * @param stride Stride of the surface in bytes - * @return surface_t The initialized placeholder surface + * @return The initialized placeholder surface * * @see #surface_make_placeholder_linear * @see #rdpq_set_lookup_address @@ -313,7 +313,7 @@ inline surface_t surface_make_placeholder(int index, tex_format_t format, uint16 * @param format Pixel format * @param width Width of the surface in pixels * @param height Height of the surface in pixels - * @return surface_t The initialized placeholder surface + * @return The initialized placeholder surface * * @see #surface_make_placeholder */ @@ -329,7 +329,7 @@ inline surface_t surface_make_placeholder_linear(int index, tex_format_t format, * Otherwise, if it is a normal surface, this function will return 0. * * @param surface Placeholder surface - * @return int The lookup index of the placeholder surface, or 0 if it is a normal surface + * @return The lookup index of the placeholder surface, or 0 if it is a normal surface */ inline int surface_get_placeholder_index(const surface_t *surface) { diff --git a/include/system.h b/include/system.h index 6601c01d2..7980a1d44 100644 --- a/include/system.h +++ b/include/system.h @@ -1,7 +1,7 @@ /** * @file system.h * @author Jennifer Taylor - * @author thekovic + * @author Giovanni Bajo * @brief newlib Interface Hooks * @ingroup system */ @@ -40,6 +40,11 @@ * @{ */ +#include +#include +#include +#include + /** @brief Number of filesystems that can be attached to the system */ #define MAX_FILESYSTEMS 10 /** @brief Number of open handles that can be maintained at one time */ @@ -49,9 +54,6 @@ extern "C" { #endif -#include -#include - /** * @brief Filesystem hook structure * @@ -64,6 +66,33 @@ extern "C" { */ typedef struct { + /** + * @brief True if the filesystem is thread safe + * + * This flag is used to determine if the filesystem can be accessed + * concurrently by multiple threads, through *different* file handles. + * + * If the filesystem is not thread safe, system code will protect + * all accesses to the filesystem with a mutex. This guarantees that if + * eg. a thread is suspended while reading a file, another thread will + * not be able to read or write any other file until the first thread + * resumes. + * + * On the other hand, if the filesystem is thread safe, the system code + * will not use a mutex to protect accesses to the filesystem. This means + * that the filesystem code must be able to handle concurrent accesses + * to different files without any protection. + * + * In general, read-only filesystems are easily thread safe: only pay + * attention to some shared mutable state like eg some global cache. + * On the other hand, read-write filesystems are usually not thread safe + * because of shared mutable structures like directories or inode tables. + * + * Notice that concurrent accesses to the same file handle are always + * meant to be unsafe and must be protected by the user code itself. + */ + bool thread_safe; + /** * @brief Function to call when performing an open command * @@ -229,6 +258,18 @@ typedef struct * @return 0 on success or a negative value on failure (errno must be set) */ int (*ioctl)(void *file, unsigned long cmd, void *argp); + /** + * @brief Change the access and modification times of a file + * + * @param[in] path + * Full path of the file to modify + * @param[in] times + * Pointer to a timeval structure containing the new access and + * modification times. This must not be NULL. + * + * @return 0 on success or a negative value on failure (errno must be set) + */ + int (*utimes)(const char *path, const struct timeval times[2]); } filesystem_t; /** @@ -276,24 +317,46 @@ typedef struct } stdio_t; /** - * @brief Register a filesystem with newlib - * - * This function will take a prefix in the form of 'prefix:/' and a pointer - * to a filesystem structure of relevant callbacks and register it with newlib. - * Any standard open/fopen calls with the registered prefix will be passed - * to this filesystem. Userspace code does not need to know the underlying - * filesystem, only the prefix that it has been registered under. + * @brief Real-time clock hook structure * - * The filesystem pointer passed in to this function should not go out of scope - * for the lifetime of the filesystem. + * This structure provides optional callback hooks for code wishing to + * implement C time functions. Any function that code does not wish to handle + * should be left as a NULL pointer. + */ +typedef struct +{ + /** + * @brief Function to call to retrieve the current date/time + * + * @param[out] out pointer to time_t (number of seconds since 1970-01-01 00:00:00 UTC) + * + * @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 clock time is not representable + */ + int (*gettime)( time_t * ); + /** + * @brief Function to call to set the current date/time + * + * @param time number of seconds since 1970-01-01 00:00:00 UTC + * + * @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 (*settime)( time_t ); +} rtc_hooks_t; + +/** + * @brief Register a filesystem with newlib * * @param[in] prefix - * Prefix of the filesystem + * The prefix that will be used to register the filesystem * @param[in] filesystem - * Structure of callbacks for various functions in the filesystem. - * If the registered filesystem doesn't support an operation, it - * should leave the callback null. - * + * Pointer to structure containing filesystem callbacks + * * @retval -1 if the parameters are invalid * @retval -2 if the prefix is already in use * @retval -3 if there are no more slots for filesystems @@ -316,6 +379,21 @@ int attach_filesystem( const char * const prefix, filesystem_t *filesystem ); */ int detach_filesystem( const char * const prefix ); +/** + * @brief Unregister a filesystem from newlib by pointer + * + * @note This function will make sure all files are closed before unregistering + * the filesystem. + * + * @param[in] filesystem + * The filesystem pointer that was passed to #attach_filesystem + * + * @retval -1 if the parameters were invalid + * @retval -2 if the filesystem couldn't be found + * @retval 0 if the filesystem was successfully unregistered + */ +int detach_filesystem_by_pointer( filesystem_t *filesystem ); + /** * @brief Hook into stdio for STDIN, STDOUT and STDERR callbacks @@ -337,27 +415,78 @@ int hook_stdio_calls( stdio_t *stdio_calls ); */ int unhook_stdio_calls( stdio_t *stdio_calls ); - /** - * @brief Hook into gettimeofday with a current time callback. + * @brief Hook into POSIX time callbacks * - * @param[in] time_fn - * Pointer to callback for the current time function + * @param[in] hooks Pointer to real-time clock callbacks structure * - * @return 0 if successful or a negative value on failure. + * @return 0 on a successful hook or a negative value on failure */ -int hook_time_call( time_t (*time_fn)( void ) ); +int hook_rtc_calls( rtc_hooks_t *hooks ); /** - * @brief Unhook from gettimeofday current time callback. + * @brief Unhook from POSIX time callbacks * - * @param[in] time_fn - * Pointer to callback for the current time function + * @param[in] hooks Pointer to real-time clock callbacks structure + * + * @return 0 on a successful unhook or a negative value on failure + */ +int unhook_rtc_calls( rtc_hooks_t *hooks ); + +/************************************** + * DEPRECATED FUNCTIONS + **************************************/ + +/// @cond + +/** + * @brief Time hook structure + * @deprecated Use rtc_hooks_t instead * - * @return 0 if successful or a negative value on failure. + * This structure provides optional callback hooks for code wishing to + * implement C time functions. Any function that code does not wish to handle + * should be left as a NULL pointer. */ +typedef struct +{ + /** + * @brief Function to call to retrieve the current date/time + * + * @return number of seconds since 1970-01-01 00:00:00 UTC + */ + time_t (*gettime)( void ); + /** + * @brief Function to call to set the current date/time + * + * @param time number of seconds since 1970-01-01 00:00:00 UTC + * + * @return whether the time was set successfully + */ + bool (*settime)( time_t ); +} time_hooks_t; + +#ifndef SYSTEM_NO_DEPRECATED +__attribute__((deprecated("use hook_rtc_calls instead"))) +#endif +int hook_time_calls( time_hooks_t *hooks ); + +#ifndef SYSTEM_NO_DEPRECATED +__attribute__((deprecated("use unhook_rtc_calls instead"))) +#endif +int unhook_time_calls( time_hooks_t *hooks ); + +#ifndef SYSTEM_NO_DEPRECATED +__attribute__((deprecated("use hook_time_calls instead"))) +#endif +int hook_time_call( time_t (*time_fn)( void ) ); + +#ifndef SYSTEM_NO_DEPRECATED +__attribute__((deprecated("use unhook_time_calls instead"))) +#endif int unhook_time_call( time_t (*time_fn)( void ) ); +/// @endcond + #ifdef __cplusplus } #endif diff --git a/include/ucontext.h b/include/ucontext.h new file mode 100644 index 000000000..7ba5db2f6 --- /dev/null +++ b/include/ucontext.h @@ -0,0 +1,82 @@ +/** + * @file ucontext.h + * @author Max Bebök + * @author Giovanni Bajo + * @brief ucontext POSIX API + * @ingroup lowlevel + */ + +#ifndef LIBDRAGON_UCONTEXT_H +#define LIBDRAGON_UCONTEXT_H + +#include +#include +#include "preview.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Defines the stack used for a context. + */ +typedef struct stack_s { + void *ss_sp; ///< stack base or pointer + size_t ss_size; ///< stack size + int ss_flags; ///< flags +} stack_t; + +/** + * Hardware specific data that needs backing up. + * This contains relevant register that are not baked up by + * the compiler itself during a function call. + */ +typedef struct mcontext_s { + uint64_t s[8]; ///< saved saved registers $s0-$s7 + uint64_t fp; ///< saved frame pointer + uint32_t ra; ///< saved return address + uint32_t a[1]; ///< saved initial entry argument $a0 + uint64_t f[12]; ///< saved floating point registers $f0-$f11 + uint32_t fc32; ///< saved floating point control register 32 +} mcontext_t; + +/** + * System-V compatible struct that defines a context. + * This includes an allocated stack, as well as currently used registers. + * + * It can be created via makecontext, and later switched to with swapcontext. + */ +typedef struct ucontext_s { + struct ucontext_s *uc_link; ///< pointer to the context that will be resumed when this context returns + stack_t uc_stack; ///< stack used for the context + mcontext_t uc_mcontext; ///< machine context for the context +} ucontext_t; + +// @TODO: allow proper varargs (is that even needed in practice?) + +/** + * @brief creates a new context to be used with swapcontext. + * @preview + * + * @param ctx context to write data to + * @param entry entry point when first switched to + * @param argc number of arguments to pass into the initial entry + * @param arg0 arguments for the initial entry + */ +LIBDRAGON_PREVIEW_API +void makecontext(ucontext_t *ctx, void (*entry)(void), int argc, uint32_t arg0); + +/** + * @brief Swaps context + * @preview + * @param old_ctx + * @param new_ctx + */ +LIBDRAGON_PREVIEW_API +extern void swapcontext(ucontext_t *old_ctx, const ucontext_t *new_ctx); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/usb.h b/include/usb.h index a62f2c09c..cd95a7947 100644 --- a/include/usb.h +++ b/include/usb.h @@ -66,9 +66,10 @@ @param The DATATYPE that is being sent @param A buffer with the data to send @param The size of the data being sent + @return 1 on success, 0 on fail, -1 on timeout ==============================*/ - extern void usb_write(int datatype, const void* data, int size); + extern char usb_write(int datatype, const void* data, int size); /*============================== diff --git a/include/vaddr64.h b/include/vaddr64.h new file mode 100644 index 000000000..0b09f1f42 --- /dev/null +++ b/include/vaddr64.h @@ -0,0 +1,177 @@ +/** + * @file vaddr64.h + * @author Giovanni Bajo + * @brief 64-bit virtual address space access functions + * @ingroup n64sys + * + * Libdragon uses the O64 ABI, in which pointers are 32-bit wide. This + * is the right choice for basically all standard use cases because + * doubling the size of the pointers would waste more memory in all data + * structures where pointers are stored. + * + * The VR4300 CPU does support a full 64-bit virtual address space + * though, which might be used for some very niche use case + * (like e.g. emulator tests) Since it is not possible to create a + * 64-bit pointer in C because of the chosen ABI, these functions + * are provided in substitution. + * + * The virtual address must be provided as a #vaddr64_t (uint64_t), as + * defined in n64types.h. + */ + +#ifndef LIBDRAGON_VADDR64_H +#define LIBDRAGON_VADDR64_H + +#include +#include "preview.h" +#include "n64types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Read a 8-bit value from memory at the given 64-bit virtual address + * @preview + * + * @param vaddr 64-bit virtual address + * @return the read value + */ +LIBDRAGON_PREVIEW_API +inline uint8_t sys_vaddr_read8(vaddr64_t vaddr) { + uint8_t value; + asm volatile ( + "lbu %[value], 0(%[vaddr]) \n" : + [value] "=r" (value): + [vaddr] "r" (vaddr) + ); + return value; +} + +/** + * @brief Read a 16-bit value from memory at the given 64-bit virtual address + * @preview + * + * @param vaddr 64-bit virtual address + * @return the read value + */ +LIBDRAGON_PREVIEW_API +inline uint16_t sys_vaddr_read16(vaddr64_t vaddr) { + uint16_t value; + asm volatile ( + "lhu %[value], 0(%[vaddr]) \n" : + [value] "=r" (value): + [vaddr] "r" (vaddr) + ); + return value; +} + +/** + * @brief Read a 32-bit value from memory at the given 64-bit virtual address + * @preview + * + * @param vaddr 64-bit virtual address + * @return the read value + */ +LIBDRAGON_PREVIEW_API +inline uint32_t sys_vaddr_read32(vaddr64_t vaddr) { + uint32_t value; + asm volatile ( + "lw %[value], 0(%[vaddr]) \n" : + [value] "=r" (value): + [vaddr] "r" (vaddr) + ); + return value; +} + +/** + * @brief Read a 64-bit value from memory at the given 64-bit virtual address + * @preview + * + * @param vaddr 64-bit virtual address + * @return the read value + */ +LIBDRAGON_PREVIEW_API +inline uint64_t sys_vaddr_read64(vaddr64_t vaddr) { + uint64_t value; + asm volatile ( + "ld %[value], 0(%[vaddr]) \n" : + [value] "=r" (value): + [vaddr] "r" (vaddr) + ); + return value; +} + +/** + * @brief Write an 8-bit value to memory at the given 64-bit virtual address + * @preview + * + * @param vaddr 64-bit virtual address + * @param value 8-bit value to write + */ +LIBDRAGON_PREVIEW_API +inline void sys_vaddr_write8(vaddr64_t vaddr, uint8_t value) { + asm volatile ( + "sb %[value], 0(%[vaddr]) \n" : + : + [value] "r" (value), + [vaddr] "r" (vaddr) + ); +} + +/** + * @brief Write a 16-bit value to memory at the given 64-bit virtual address + * @preview + * + * @param vaddr 64-bit virtual address + * @param value 16-bit value to write + */ +LIBDRAGON_PREVIEW_API +inline void sys_vaddr_write16(vaddr64_t vaddr, uint16_t value) { + asm volatile ( + "sh %[value], 0(%[vaddr]) \n" : + : + [value] "r" (value), + [vaddr] "r" (vaddr) + ); +} + +/** + * @brief Write a 32-bit value to memory at the given 64-bit virtual address + * @preview + * + * @param vaddr 64-bit virtual address + * @param value 32-bit value to write + */ +LIBDRAGON_PREVIEW_API +inline void sys_vaddr_write32(vaddr64_t vaddr, uint32_t value) { + asm volatile ( + "sw %[value], 0(%[vaddr]) \n" : + : + [value] "r" (value), + [vaddr] "r" (vaddr) + ); +} + +/** + * @brief Write a 64-bit value to memory at the given 64-bit virtual address + * @preview + * + * @param vaddr 64-bit virtual address + * @param value 64-bit value to write + */ +LIBDRAGON_PREVIEW_API +inline void sys_vaddr_write64(vaddr64_t vaddr, uint64_t value) { + asm volatile ( + "sd %[value], 0(%[vaddr]) \n" : + : + [value] "r" (value), + [vaddr] "r" (vaddr) + ); +} + +#ifdef __cplusplus +} +#endif + +#endif // LIBDRAGON_VADDR64_H diff --git a/include/vi.h b/include/vi.h new file mode 100644 index 000000000..730daa889 --- /dev/null +++ b/include/vi.h @@ -0,0 +1,1223 @@ +/** + * @file vi.h + * @author Giovanni Bajo + * @brief Video Interface Subsystem + * @ingroup display + * + * This module offers a low-level interface to VI programming. Most applications + * should use display.h, which sits on top of vi.h, and offers a higher level + * interface, plus automatic memory management of multiple framebuffers, + * FPS utilities, and much more. + * + * ## Register read/write access + * + * This module defines a set of macros to directly access the VI registers, + * including macros to compose different fields in the registers. While read + * access is straightforward, write access is generally more complicated + * because VI is quite picky about the timing of register writes: most registers + * can only be written during vblank. + * + * To help with that, this module provides a set of functions to write registers + * that will be applied at the next vblank: #vi_write and #vi_write_masked. + * To batch multiple register writes, you can use #vi_write_begin and #vi_write_end, + * so that you can be sure that all the registers will be written atomically + * at the next vblank. + * + * #vi_read can be used to read the current state of a register, including any + * pending changes (applied via #vi_write) that were not yet applied. + * + * ## Higher level APIs + * + * This module also includes some higher level APIs to configure the VI: + * + * * #vi_show is a simple function to just show a #surface_t to the screen. + * * #vi_set_origin, #vi_set_xscale and #vi_set_yscale are more granular + * functions to configure the framebuffer. + * * #vi_set_aa_mode, #vi_set_divot, #vi_set_dedither and #vi_set_gamma + * are functions to configure various filters and modes of the VI. + * * #vi_set_interlaced is a function to turn on and off interlaced display mode. + * * #vi_set_output defines the active display area on the screen. + * #vi_get_output_bounds will provide the limits for it, while + * #vi_move_output and #vi_scroll_output can be used to just pan the display. + * * #vi_set_borders is an alternative to #vi_set_output, that specifies the + * display area in terms of borders (with respect to the default one). + * + * ## Active display area and framebuffer scaling + * + * VI has a powerful resampling engine: the framebuffer is not displayed as-is + * on TV, but it is actually resampled (scaled, stretched), optionally with + * bilinear filtering. This is a very powerful hardware feature that is a bit + * complicated to configure, so an effort was made to expose an intuitive API + * to programmers. + * + * By default, this module configures the VI to resample an arbitrary framebuffer + * picture into a virtual 640x480 display output with 4:3 aspect ratio (on NTSC + * and MPAL), or a virtual 640x576 display output though with the same 4:3 + * aspect ratio on PAL. This is done to achieve TV type independence: since + * both resolution share the same aspect ratio on their respective TV standards + * (given that dots are square on NTSC but they are not on PAL), it means + * that the framebuffer will look the same on all TVs; on PAL, you will be + * trading additional vertical resolution (assuming the framebuffer is big + * enough to have that detail). + * + * This also means that games don't have to do anything special to handle NTSC + * vs PAL (besides maybe accounting for the different refresh rate), which is + * an important goal. + * + * In reality, TVs didn't have a 480-lines vertical resolution so the actual + * output depends on whether you request interlaced display or not: + * + * * In case of non interlaced display, the actual resolution is 640x240, but + * since dots will be configured to be twice as big vertically, the aspect + * ratio will be 4:3 as-if the image was 640x480 (with duplicated scanlines) + * * In case of interlaced display, you do get to display 480 scanlines, by + * alternating two slightly-shifted 640x240 pictures. + * + * As an example, if you configure a framebuffer resolution like 512x320, with + * interlacing turned off, what happens is that the image gets scaled into + * 640x240, so horizontally some pixels will be duplicated to enlarge the + * resolution to 640, but vertically some scanlines will be dropped. The + * output display aspect ratio will still be 4:3, which is not the source aspect + * ratio of the framebuffer (512 / 320 = 1.6666 = 16:10), so the image will + * appear squished, unless obviously this was accounted for while drawing to + * the framebuffer. + * + * While resampling the framebuffer into the display output, the VI can use either + * bilinear filtering or simple nearest sampling (duplicating or dropping pixels). + * See #vi_aa_mode_t for more information on configuring the VI image filters. + * + * The 640x480 virtual display output can be fully viewed on emulators and on + * modern screens (via grabbers, converters, etc.). When displaying on old + * CRTs though, part of the display will be hidden because of the overscan. + * To account for this, you can configure a smaller display output via two + * different APIs which are just two ways to express the same concept: + * + * * #vi_set_borders: this function configures the display output on the screen + * by specifying the size of the borders, with respect to the default 640x480 + * display output. + * * #vi_set_output: this function configures the display output on the screen + * by specifying the top-left and bottom-right corners of the display area, + * as absolute coordinates in the VI coordinate system. + * + * For instance, if you specify 12 dots of borders on all the four edges, you + * will get a 616x456 display output (on NTSC), plus the requested 12 dots of + * borders on all sides; The same effect can be obtained by specifying the + * display output area as (120, 47) - (736, 503) with #vi_set_output. This + * is because the default display area on NTSC is (108, 35) - (748, 515) + * (which can be found out via #vi_get_output). + * + * Notice that adding borders also affects the aspect ratio of the display output; + * for instance, in the above example, the 616x456 display output is not + * exactly 4:3 anymore, but more like 4.05:3. By carefully calculating borders, + * thus, it is possible to obtain specific display outputs with custom aspect + * ratios (eg: 16:9). + * + * To help calculating the borders by taking both potential goals into account + * (overscan compensation and aspect ratio changes), you can use #vi_calc_borders. + * + * ## Blank mode vs turning off the VI + * + * Blank mode is a special state where the VI is still active (TV sync is generated) + * but no framebuffer is shown. This is useful for temporarily displaying a + * black picture while readying next framebuffer, and without losing TV sync + * in the process (as TVs or framegrabbers can be slow at re-locking to sync). + * + * Blank mode is automatically enabled when you call `vi_show(NULL)`, which is + * probably the most natural way for the user to temporarily blank the display. + * It can also be selectively enabled and disabled via #vi_blank, for lower-level + * configurations. + * + * Blank mode is different from turning off the VI via #vi_close: in the latter + * case, no video sync is generated, so most TVs will show static or a "no + * signal" message. This is useful mainly for testing purposes: normal applications + * shouldn't need to ever turn off the VI after boot, as VI allows to even change + * the framebuffer resolution without ever losing sync. + * + * Another important difference is that when the VI is turned off, VI interrupts + * are not generated. So handlers installed via #vi_install_vblank_handler + * (or the lower level #register_VI_handler in interrupt.h) will not be called + * anymore. On the contrary, in blank mode, VI interrupts are still generated + * normally, so the application can still rely on them for timing purposes. + * + * ## Working with interrupts disabled + * + * Sometimes, the VI library can be used in highly specified contexts where + * interrupts are disabled (special test ROMs, emergency debug outputs, etc.) + * + * In these cases, after you change any setting with this library (eg: + * call #vi_show to display a new buffer), you must also call #vi_wait_vblank. + * This will block execution until the next vblank happens, and at that point + * all pending VI settings will also be applied. + * + * Normally, this is not necessary as interrupts are used to synchronize with + * vblank. But missing that possibility, the synchronization must be performed + * explicitly via #vi_wait_vblank. + */ +#ifndef __LIBDRAGON_VI_H +#define __LIBDRAGON_VI_H + + +#include +#include +#include "preview.h" + +#ifdef __cplusplus +extern "C" { +#endif + +///@cond +typedef struct surface_s surface_t; +///@endcond + +/** @brief Number of useful 32-bit registers at the register base */ +#define VI_REGISTERS_COUNT 14 + +/** @brief Base pointer to hardware Video interface registers that control various aspects of VI configuration. + * Shouldn't be used by itself, use VI_ registers to get/set their values. */ +#define VI_REGISTERS ((volatile uint32_t*)0xA4400000) +/** @brief VI register of controlling general display filters/bitdepth configuration */ +#define VI_CTRL (&VI_REGISTERS[0]) +/** @brief VI register of RDRAM base address of the video output Frame Buffer. */ +#define VI_ORIGIN (&VI_REGISTERS[1]) +/** @brief VI register of width in pixels of the frame buffer. */ +#define VI_WIDTH (&VI_REGISTERS[2]) +/** @brief VI register of vertical interrupt. */ +#define VI_V_INTR (&VI_REGISTERS[3]) +/** @brief VI register of the current half line, sampled once per line. */ +#define VI_V_CURRENT (&VI_REGISTERS[4]) +/** @brief VI register of sync/burst values */ +#define VI_BURST (&VI_REGISTERS[5]) +/** @brief VI register of total visible and non-visible lines. */ +#define VI_V_TOTAL (&VI_REGISTERS[6]) +/** @brief VI register of total width of a line */ +#define VI_H_TOTAL (&VI_REGISTERS[7]) +/** @brief VI register of an alternate scanline length for one scanline during vsync. */ +#define VI_H_TOTAL_LEAP (&VI_REGISTERS[8]) +/** @brief VI register of start/end of the active video image, in screen pixels */ +#define VI_H_VIDEO (&VI_REGISTERS[9]) +/** @brief VI register of start/end of the active video image, in screen half-lines. */ +#define VI_V_VIDEO (&VI_REGISTERS[10]) +/** @brief VI register of start/end of the color burst enable, in half-lines. */ +#define VI_V_BURST (&VI_REGISTERS[11]) +/** @brief VI register of horizontal subpixel offset and 1/horizontal scale up factor. */ +#define VI_X_SCALE (&VI_REGISTERS[12]) +/** @brief VI register of vertical subpixel offset and 1/vertical scale up factor. */ +#define VI_Y_SCALE (&VI_REGISTERS[13]) + +/** @brief VI register by index (0-13)*/ +#define VI_TO_REGISTER(index) (((index) >= 0 && (index) <= VI_REGISTERS_COUNT)? &VI_REGISTERS[index] : NULL) + +/** @brief VI index from register */ +#define VI_TO_INDEX(reg) ((reg) - VI_REGISTERS) + +/** Under VI_CTRL */ + +/** @brief VI_CTRL Register setting: enable dedither filter. */ +#define VI_DEDITHER_FILTER_ENABLE (1<<16) +/** @brief VI_CTRL Register setting: default value for pixel advance. */ +#define VI_PIXEL_ADVANCE_DEFAULT (0b0011 << 12) +/** @brief VI_CTRL Register setting: default value for pixel advance on iQue. */ +#define VI_PIXEL_ADVANCE_BBPLAYER (0b0001 << 12) +/** @brief VI_CTRL Register setting: mask for AA mode configuration */ +#define VI_AA_MODE_MASK (0b11 << 8) +/** @brief VI_CTRL Register setting: enable interlaced output. */ +#define VI_CTRL_SERRATE (1<<6) +/** @brief VI_CTRL Register setting: enable divot filter (fixes 1 pixel holes after AA). */ +#define VI_DIVOT_ENABLE (1<<4) +/** @brief VI_CTRL Register setting: framebuffer source format */ +#define VI_CTRL_TYPE (0b11) +/** @brief VI_CTRL Register setting: set the framebuffer source as 32-bit. */ +#define VI_CTRL_TYPE_32_BPP (0b11) +/** @brief VI_CTRL Register setting: set the framebuffer source as 16-bit (5-5-5-3). */ +#define VI_CTRL_TYPE_16_BPP (0b10) +/** @brief VI_CTRL Register setting: turn off the VI, emitting no sync, TV screens will either show static, black, or a "no signal" message). */ +#define VI_CTRL_TYPE_OFF (0b00) + +/** Under VI_ORIGIN */ +/** @brief VI_ORIGIN Register: set the address of a framebuffer. */ +#define VI_ORIGIN_SET(value) ((value & 0xFFFFFF) << 0) + +/** Under VI_WIDTH */ +/** @brief VI_ORIGIN Register: set the width of a framebuffer. */ +#define VI_WIDTH_SET(value) ((value & 0xFFF) << 0) + +/** Under VI_V_CURRENT */ +/** @brief VI_V_CURRENT Register: default value for vblank begin line. */ +#define VI_V_CURRENT_VBLANK 2 + +/** Under VI_V_INTR */ +/** @brief VI_V_INTR Register: set value for vertical interrupt. */ +#define VI_V_INTR_SET(value) ((value & 0x3FF) << 0) + +/** Under VI_V_TOTAL */ +/** @brief VI_V_TOTAL Register: set the total number of visible and non-visible half-lines. */ +#define VI_V_TOTAL_SET(vsync) ((vsync)-1) + +/** Under VI_H_TOTAL */ +/** @brief VI_H_TOTAL Register: set the total width of a line in quarter-pixel units (-1), and the 5-bit leap pattern. */ +#define VI_H_TOTAL_SET(leap_pattern, hsync) ((((leap_pattern) & 0x1F) << 16) | ((int)((hsync)*4-1) & 0xFFF)) + +/** Under VI_H_TOTAL_LEAP */ +/** @brief VI_H_TOTAL_LEAP Register: set alternate scanline lengths for one scanline during vsync, leap_a and leap_b are selected based on the leap pattern in VI_H_SYNC. */ +#define VI_H_TOTAL_LEAP_SET(leap_a, leap_b) ((((int)((leap_a)*4-1) & 0xFFF) << 16) | ((int)((leap_b)*4-1) & 0xFFF)) + +/** Under VI_H_VIDEO */ +/** @brief VI_H_VIDEO Register: set the horizontal start and end of the active video area, in screen pixels */ +#define VI_H_VIDEO_SET(start, end) ((((start) & 0x3FF) << 16) | ((end) & 0x3FF)) + +/** Under VI_V_VIDEO */ +/** @brief VI_V_VIDEO Register: set the vertical start and end of the active video area, in half-lines */ +#define VI_V_VIDEO_SET(start, end) ((((start) & 0x3FF) << 16) | ((end) & 0x3FF)) + +/** Under VI_V_BURST */ +/** @brief VI_V_BURST Register: set the start and end of color burst enable, in half-lines */ +#define VI_V_BURST_SET(start, end) ((((start) & 0x3FF) << 16) | ((end) & 0x3FF)) + +/** Under VI_X_SCALE */ +/** @brief VI_X_SCALE Register: set 1/horizontal scale up factor (value is converted to 2.10 format) */ +#define VI_X_SCALE_SET(from, to) ((1024 * (from) + (to) / 2 ) / (to)) + +/** Under VI_Y_SCALE */ +/** @brief VI_Y_SCALE Register: set 1/vertical scale up factor (value is converted to 2.10 format) */ +#define VI_Y_SCALE_SET(from, to) ((1024 * (from) + (to) / 2 ) / (to)) + +/** Under VI_BURST */ +/** @brief VI_BURST Register: set start of color burst in pixels from hsync. */ +#define VI_BURST_START(value) ((value & 0x3FF) << 20) +/** @brief VI_BURST Register: set vertical sync width in half lines. */ +#define VI_VSYNC_WIDTH(value) ((value & 0xF) << 16) +/** @brief VI_BURST Register: set color burst width in pixels. */ +#define VI_BURST_WIDTH(value) ((value & 0xFF) << 8) +/** @brief VI_BURST Register: set horizontal sync width in pixels. */ +#define VI_HSYNC_WIDTH(value) ((value & 0xFF) << 0) +/** @brief VI_BURST Register: set all values. */ +#define VI_BURST_SET(burst_start, vsync_width, burst_width, hsync_width) \ + VI_BURST_START(burst_start) | VI_VSYNC_WIDTH(vsync_width) | \ + VI_BURST_WIDTH(burst_width) | VI_HSYNC_WIDTH(hsync_width) + +///@cond +// Private symbols, do not use +extern uint32_t __vi_cfg[VI_REGISTERS_COUNT]; +///@endcond + +/** + * @brief Video Interface borders structure + * + * This structure defines how thick (in dots) should the borders around + * a framebuffer be. + * + * The dots refer to the VI virtual display output (640x480, on both NTSC, PAL, + * and M-PAL), and thus reduce the actual display output, and even potentially + * modify the aspect ratio. The framebuffer will be scaled to fit under them. + * + * For example, when displaying on CRT TVs, one can add borders around a + * framebuffer so that the whole image can be seen on the screen. + * + * If no borders are applied, the output will use the entire virtual dsplay + * output (640x480) for showing a framebuffer. This is useful for emulators, + * upscalers, and LCD TVs. + * + * Notice that borders can also be *negative*: this obtains the effect of + * actually enlarging the output, growing from 640x480. Doing so will very + * likely create problems with most TV grabbers and upscalers, but it might + * work correctly on most CRTs (though the added pixels will surely be + * part of the overscan so not really visible). Horizontally, the maximum display + * output will probably be ~700-ish on CRTs, after which the sync will be lost. + * Vertically, any negative number will likely create immediate syncing problems, + */ +typedef struct vi_borders_s { + int16_t left, right, up, down; +} vi_borders_t; + +/** + * @brief VI AA Mode + * + * This setting configures the resampling and AA filters performed + * by VI during the framebuffer display. + * + * Notice that VI seems a bit incomplete / buggy in this area, so + * not all filter-related settings are guaranteed to work in all + * contexts. + */ +typedef enum vi_aa_mode_e { + /** + * @brief Disable the resampling and AA filter. + * + * @note Because of a hardware bug on NTSC units, don't use + * this mode in 16 bpp mode and framebuffer widths lower or + * equal to 320 pixels. + */ + VI_AA_MODE_NONE = (0b11 << 8), + + /** + * @brief Enable bilinear filtering during resampling + * + * With this setting, the VI will perform true 2x2 bilinear filtering + * while resampling the framebuffer into the display output. + * + * @note This mode is not compatible with dedithering + * (#vi_set_dedither). + */ + VI_AA_MODE_RESAMPLE = (0b10 << 8), + + /** + * @brief Enable AA filter (in addition to bilinear resampling). + * + * This filter is useful to reduce aliasing artifacts. It works by + * smoothing pixels that represent external edges of polygons/meshes. + * Since VI works on the final framebuffer picture, it has no knowledge + * of the 3D scene that was drawn over it, so it uses pixel coverage + * information (left by RDP in the framebuffer), to identify which + * pixels to smooth. + * + * Doing AA filter will have a performance impact (compared to pure + * bilinear resampling, which is #VI_AA_MODE_RESAMPLE) because of the + * extra memory bandwidth required. + * + * @note This setting seems to cause image corruption in high-bandwidth + * scenarios, like high-resolution framebuffers. Use + * #VI_AA_MODE_RESAMPLE_FETCH_ALWAYS in those cases (though it will + * impact general performance more). + * + * @note This setting is not compatible with dedithering + * (#vi_set_dedither). + */ + VI_AA_MODE_RESAMPLE_FETCH_NEEDED = (0b01 << 8), + + /** + * @brief Enable AA filter (in addition to bilinear resampling). + * + * This is similar to #VI_AA_MODE_RESAMPLE_FETCH_NEEDED. The exact + * difference is not known at this time: it seems to impact the way + * internally VI fetches pixels from memory. The AA filter itself is + * identical, so the picture will look the same on the screen. + * + * The known outcomes of setting this mode are: + * + * * It seems completely broken in 32-bpp mode. It's not clear if it's + * a bandwidth issue or it's just a bug in the hardware. + * * It seems to fix screen artifacts that appear with #VI_AA_MODE_RESAMPLE_FETCH_NEEDED + * in high-bandwidth scenarios, like high-resolution framebuffers. + * * It causes a bit more performance impact. + */ + VI_AA_MODE_RESAMPLE_FETCH_ALWAYS = (0b00 << 8), +} vi_aa_mode_t; + +/** + * @brief VI gamma correction mode + * + * See #vi_set_gamma for more information. + */ +typedef enum vi_gamma_e { + /** @brief Disable gamma correction */ + VI_GAMMA_DISABLE = 0, + /** @brief Enable gamma correction */ + VI_GAMMA_ENABLE = (1<<2), + /** @brief Enable gamma correction with dithering */ + VI_GAMMA_DITHER_ENABLE = (1<<2) | (1<<3), +} vi_gamma_t; + + +/** + * @brief Timing preset of settings for VI + * + * This is an advanced, low-level structure that defines all timing-related + * settings for VI. Most users will not need to use this directly, as + * default VI timings are automatically applied by #vi_init and #vi_reset. + */ +typedef struct vi_timing_preset_s { + uint32_t vi_h_total; ///< Total horizontal length (in 1/4th pixels) + uint32_t vi_h_total_leap; ///< Leap setting (alternate scanline lengths) + uint32_t vi_v_total; ///< Total vertical length (in scanlines) + uint32_t vi_burst; ///< Horizontal Burst settings + uint32_t vi_v_burst; ///< Vertical burst settings + struct { + int x0; ///< Default active area x0 + int y0; ///< Default active area y0 + int width; ///< Default active area width + int height; ///< Default active area height (in half-lines) + } display; ///< Default active area +} vi_timing_preset_t; + +/** @brief Default timing preset for PAL consoles */ +extern const vi_timing_preset_t VI_TIMING_PAL; +/** @brief Default timing preset for NTSC consoles */ +extern const vi_timing_preset_t VI_TIMING_NTSC; +/** @brief Default timing preset for M-PAL consoles */ +extern const vi_timing_preset_t VI_TIMING_MPAL; +/** + * @brief Alternative timing preset for PAL consoles to target 60 Hz + * + * This timing preset configures the VI to emit a 60 Hz video signal + * with PAL timings. This creates a special "PAL60" mode that can be + * used to draw up to 60 frames per second on PAL TVs. This feature was + * also available more officially on 6th generation consoles (eg: + * GameCube, PlayStation 2), + * + * Being a non-standard extension to the spec, not all PAL devices will support it. + * CRTs usually do support it, but some upscalers and grabbers might not. + * + * Do *NOT* use this timing preset by default on PAL consoles, as that might + * make your applications unusable on some devices. If you want to support it, + * make sure to let the user opt-in to it and test if their device supports it. + * + * @see #vi_set_timing_preset + */ +extern const vi_timing_preset_t VI_TIMING_PAL60; + +/** + * @brief Initialize the VI module + * @preview + * + * This also calls #vi_reset to reset the VI configuration to the default state. + * + * In its default state, video sync is turned on (so the TV output will find a sync), + * VBlank interrupts are generated by the hardware, but the display is blanked + * (no framebuffer is shown). + * + * To show a first image, call #vi_show. + */ +LIBDRAGON_PREVIEW_API +void vi_init(void); + +/** + * @brief Close the VI module + * @preview + * + * This function turns off the VI (no sync will be generated, so most TVs + * will show static or a "no signal" message). + * + * After calling this function, also video interrupts will be disabled. + */ +LIBDRAGON_PREVIEW_API +void vi_close(void); + +/** + * @brief Reset VI configuration + * @preview + * + * This brings back the VI configuration to the default state. + * Since there is no meaningful hardware default, the default is the one + * chosen by this library: + * + * * Correct video timings for NTSC, PAL, and M-PAL + * * Default active display area ("borderless") + * * AA mode: resample + * * Interlaced, divot, dedithering, and gamma correction disabled + * * VI turned on, but display is forcibly blanked (see #vi_blank) + * * VBlank interrupts are active + */ +LIBDRAGON_PREVIEW_API +void vi_reset(void); + +/** + * @name Low-level register access + * + * These functions allow to read/write VI hardware registers directly, in a safe + * way. Consider that most timing-related registers can cause a hard-crash of VI + * if they are changed during the active display area, so it is generally good + * practice to only change them during vblank. Thus, we provide a way to delay + * writing registers until next vblank. + * + * @{ + */ + +/** + * @brief Read the current state of a register, including pending changes. + * @preview + * + * This function is similar to directly sampling the hardware register, + * but also takes into account any pending write that might have issued + * before but was still not applied. This helps achieving consistency during + * multi-register writes. + * + * @param reg Register to read + * @return Current value of the register (or pending value if any) + */ +LIBDRAGON_PREVIEW_API +inline uint32_t vi_read(volatile uint32_t *reg) { + return __vi_cfg[reg - VI_REGISTERS]; +} + +/** + * @brief Partially write a VI register at next vblank + * @preview + * + * This is similar to #vi_write, but only write some bits of the register. + * The specified \p wmask specifies which bits will actually be changed + * using the \p value, while the other bits will be left untouched. + * + * @param reg Register to write + * @param wmask Mask of bits to write + * @param value Value to write + * + * @see #vi_write + * @see #vi_write_begin + */ +LIBDRAGON_PREVIEW_API +void vi_write_masked(volatile uint32_t *reg, uint32_t wmask, uint32_t value); + +/** + * @brief Write a VI register at next vblank + * @preview + * + * The write will be pending until the vblank, but #vi_read will immediately + * start returning the new value. + * + * Notice that if the function is called while the VI is currently in vblank + * period, the batched changes will be applied immediately. + * + * If you want to atomically change multiple registers, use #vi_write_begin + * and #vi_write_end to group the changes. + * + * @param reg Register to write + * @param value Value to write + * + * @see #vi_write_begin + * @see #vi_write_masked + */ +LIBDRAGON_PREVIEW_API +inline void vi_write(volatile uint32_t *reg, uint32_t value) +{ + vi_write_masked(reg, 0xFFFFFFFF, value); +} + +/** + * @brief Begin a batch of register writes + * @preview + * + * This function starts a batch of register writes, so that they will be + * applied atomically at the next vblank. This is useful when you want to + * change multiple registers at once, to avoid display artifacts. + * + * @see #vi_write_end + */ +LIBDRAGON_PREVIEW_API +void vi_write_begin(void); + +/** + * @brief End a batch of register writes + * @preview + * + * This function ends a batch of register writes started with #vi_write_begin. + * It doesn't block until vblank, so when the function returns, the registers + * are still pending to be written (though #vi_read will return the new values). + * + * Notice that if the function is called while the VI is currently in vblank + * period, the batched changes will be applied immediately. + * + * @see #vi_write_begin + */ +LIBDRAGON_PREVIEW_API +void vi_write_end(void); + +/** + * @brief Wait for the beginning of next vblank period + * @preview + * + * This function waits for the beginning of the next vblank period, and + * returns immediately after that. + * + * If the VI is not active, this function will return immediately. + */ +LIBDRAGON_PREVIEW_API +void vi_wait_vblank(void); + +/** + * @brief Dump the current status of all VI registers + * @preview + * + * @param verbose Verbosity mode of dump + * 0=just hex values, 1=decoded values + */ +LIBDRAGON_PREVIEW_API +void vi_debug_dump(int verbose); + +/** + * @brief Install a custom vblank handler + * @preview + * + * This function installs a custom callback that will be run at the beginning + * of the vblank period. The most common use of this function is to + * switch framebuffers, by calling #vi_show, #vi_set_origin, or even just + * #vi_write to change #VI_ORIGIN or other registers. + * + * Notice that you can call vi_show() at any time, but using the callback + * handler allows you more sophisticated logics like keeping + * a queue of framebuffers to show (eg: for triple buffering). + * + * @param handler Function to call at the beginning of the vblank + * @param arg Argument to pass to the handler + * + * @see #vi_show + * @see #vi_set_origin + * @see #vi_uninstall_vblank_handler + */ +LIBDRAGON_PREVIEW_API +void vi_install_vblank_handler(void (*handler)(void *), void *arg); + +/** + * @brief Uninstall a custom vblank handler + * @preview + * + * This function removes a previously installed vblank handler. The + * handler and the argument must be the same as the ones used in + * #vi_install_vblank_handler. + * + * @param handler Function to remove + * @param arg Argument associated with the handler + * + * @see #vi_install_vblank_handler + */ +LIBDRAGON_PREVIEW_API +void vi_uninstall_vblank_handler(void (*handler)(void *), void *arg); + +/** + * @brief Stabilize the value of a VI register by rewriting it at vblank + * @preview + * + * @note This is an advanced function, which is normally not needed. + * + * This function forces a certain register to be rewritten with its programmed + * value (last value written via #vi_write functions) at every vblank. + * + * This can be useful in cases where you are playing with a certain register + * mid-frame, and you want to ensure that the original value is reset at the + * beginning of each frame. + * + * @param reg Register to stabilize + * @param enable Whether to enable or disable the stabilization + */ +LIBDRAGON_PREVIEW_API +void vi_stabilize(volatile uint32_t *reg, bool enable); + +/** + * @brief Set blanking mode + * @preview + * + * This function sets the VI to blanking mode, which means that the VI + * will display fully black frames, but *without* turning off the video + * signal (and thus losing sync with the TV). + * + * This can be useful for instance during transitions where we might not have + * a framebuffer ready to display yet, but at the same time we don't want + * to cause a sync loss. + * + * @note This function is not the same as vi_set_type(VI_CTRL_TYPE_OFF), + * which will turn off the video signal and cause a sync loss. + * + * @param blank Whether to enable or disable blanking mode + */ +LIBDRAGON_PREVIEW_API +void vi_blank(bool blank); + +/** + * @brief Configure a custom timing preset for VI + * @preview + * + * This is a low-level function that allows to configure a custom timing + * preset for VI. Most users will not need to use this directly, as + * default VI timings are automatically applied by #vi_init and #vi_reset. + * + * The goal of this function is to allow advanced users to experiment + * with custom timings, potentially to support non-standard video modes. + * + * The only non-standard timing preset provided by default is #VI_TIMING_PAL60, + * which allows PAL consoles to (try to) emit a 60 Hz video signal. + * + * @param preset Timing preset to apply + * + */ +LIBDRAGON_PREVIEW_API +void vi_set_timing_preset(const vi_timing_preset_t *preset); + +/** @} */ + +/** + * @name Functions to query VI state + * + * These functions allow to query the current state of VI, by also providing + * simple processing of raw register values. + * + * @{ + */ + +/** + * @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 vi_get_refresh_rate(void); + +/** + * @brief Get the current active display output area + * @preview + * + * This function returns the current VI display output area, which is the + * area on the output screen within which the framebuffer is displayed. + * The area is expressed in screen dots. + * + * The theoretical total size of the screen is 773x525 on NTSC and M-PAL, + * and 794x625 on PAL, but some of that range won't actually be available for + * display as it would conflict with various sync signals. The maximum allowed + * output area can be queried at runtime using #vi_get_output_bounds. + * + * These are the default display areas in the various TV standards: + * + * * NTSC and M-PAL: (108,35) - (748,515) + * * PAL: (128,45) - (768,621) + * + * @note The output area has nothing to do with the framebuffer size. It just + * describes which part of the (virtual) TV screen is not black. + * The framebuffer can have any size, and normally it will resampled to + * fit the output area. + * + * @param[out] x0 Horizontal start of the output area + * @param[out] y0 Vertical start of the output area + * @param[out] x1 Horizontal end of the output area (exclusive) + * @param[out] y1 Vertical end of the output area (exclusive) + * + * @see #vi_get_output_bounds + * @see #vi_set_output + * @see #vi_move_output + */ +LIBDRAGON_PREVIEW_API +void vi_get_output(int *x0, int *y0, int *x1, int *y1); + +/** + * @brief Get the bounds for the output area + * @preview + * + * The output area is the area on the screen where the framebuffer is displayed. + * + * The theoretical total size of the screen is 773x525 on NTSC and M-PAL, + * and 794x625 on PAL, but some of that range won't actually be available for + * display as it would conflict with various sync signals. If the output area + * was configure to overlap those signals, the picture would desync. + * + * This function returns the actual bounds which the outut area can be + * configured. The bounds depend on the TV standard and whether interlacing + * is on or off. + * + * @param[out] x0 Minimum X coordinate that can be used for output. + * @param[out] y0 Minimum Y coordinate that can be used for output. + * @param[out] x1 Maximum X coordinate that can be used for output (exclusive) + * @param[out] y1 Maximum Y coordinate that can be used for output (exclusive) + */ +LIBDRAGON_PREVIEW_API +void vi_get_output_bounds(int *x0, int *y0, int *x1, int *y1); + +/** + * @brief Get the output area as a border structure + * @preview + * + * This function returns the current output area as a border structure. The + * output area is normally queried using #vi_get_output, but this function + * returns a different representation of the same area, in terms of size + * of the "borders". + * + * The borders are expressed in dots, and they represent the offset from the + * default output area. For instance, on NTSC, the default output area is + * be (108,35) - (748,515). If you call this function, you will see that + * borders are 0. This means that the output area is the default one. + * + * If you change the display area to (140,51) - (716,499), the borders will + * be (left=32, right=32, up=16, down=16), as they represent the offset + * from the default area. Positive values mean that the output area is + * smaller than the default one, while negative values mean that the output + * area is larger. + * + * @return Size of the borders in the output area, with + * respect to the default output area. + */ +LIBDRAGON_PREVIEW_API +vi_borders_t vi_get_borders(void); + +/** + * @brief Return the current scanline, and optionally the current field + * @preview + * + * This function returns the current scanline being displayed on the screen. + * This counter is sometimes called "half-line": it is a *even* value between + * 0 and 524 (or 624 on PAL), which can be thought of as two times the actual TV + * scanline (which is 262 or 312 on PAL). + * + * This scanline number is in the same coordinate system as the one used by + * other VI registers and functions such as #vi_set_output. + * + * If the \p field parameter is not NULL, it will be filled with the current + * field being displayed. The field is 0 for the first field, and 1 for the + * second field, in interlaced mode. In progressive mode, the field is + * undefined (normally it is 0, but it's not guaranteed). + * + * @param[out] field Field number (0 or 1) in interlaced mode (undefined in progressive mode) + * @return Current scanline (always an even number) + */ +LIBDRAGON_PREVIEW_API +inline int vi_get_scanline(int *field) { + uint32_t v_current = *VI_V_CURRENT; + if (field) *field = v_current & 1; + return v_current & ~1; +} + +/** @} */ + +/** + * @name Functions to configure VI + * + * These functions allow to configure VI in various ways, like setting the + * framebuffer, the borders, the scaling factors, and more. The functions + * provide a "middle-level" interface to VI, which is easier to use than + * just writing raw registers. + * + * @{ + */ + +/** + * @brief Configure VI to display the specified framebuffer + * @preview + * + * @note You can use #vi_show as a shortcut to do everything required to + * display a framebuffer. + * + * This is the most basic function to display a buffer on the screen. + * It just updates the buffer pointer in RDRAM and its properties + * (width and bitdepth). + * + * Notice that VI doesn't know the actual width and height of the framebuffer: + * it will always display contents within the active display area. Instead, + * you should call #vi_set_xscale and #vi_set_yscale to configure the scaling + * factors, so that the framebuffer is fits the display area. + * + * @param buffer Pointer to the framebuffer to display + * @param pixel_stride Distance between two consecutive lines in the framebuffer, + * measured in pixels. + * @param bpp Bit depth of the framebuffer. Only allowed values are 16 and 32. + * + * @see #vi_show + * @see #vi_set_xscale + * @see #vi_set_yscale + */ +LIBDRAGON_PREVIEW_API +void vi_set_origin(void *buffer, int pixel_stride, int bpp); + +/** + * @brief Get the bit depth of the current framebuffer + * @preview + * + * @return Bit depth of the current framebuffer (16 or 32), or 0 if VI is disabled + */ +LIBDRAGON_PREVIEW_API +int vi_get_bpp(void); + +/** + * @brief Configure the horizontal scale factor to display the specified framebuffer width + * @preview + * + * This function calculates and configures the horizontal scale factor + * needed to display the specified framebuffer width on the screen, so + * that it fits exactly the active display area. + * + * If OUT_WIDTH is the width of the output area (as returned by #vi_get_output), + * this function is equivalent to calling `vi_set_xscale_factor(fb_width / OUT_WIDTH)`. + * + * @param fb_width Width of the framebuffer in pixels + * + * @ßee #vi_set_xscale_factor + */ +LIBDRAGON_PREVIEW_API +void vi_set_xscale(float fb_width); + +/** + * @brief Configure the vertical scale factor to display the specified framebuffer height. + * @preview + * + * This function calculates and configures the vertical scale factor + * needed to display the specified framebuffer width on the screen, so + * that it fits exactly the active display area. + * + * @param fb_height Height of the framebuffer in pixels + */ +LIBDRAGON_PREVIEW_API +void vi_set_yscale(float fb_height); + +/** + * @brief Set the horizontal scaling factor. + * @preview + * + * Set the scale factor applied to the framebuffer width to display it on the screen. + * The \p xfactor term describes how many framebuffer pixels advance for each + * output dot. For instance, a factor of 0.5 means that each framebuffer pixel + * will be repeated twice horizontally on the screen, onto two output dots. + * + * You can use #vi_set_xscale to automatically calculate the factor needed + * to display the framebuffer width on the screen. + * + * @param xfactor Horizontal scale factor to set + * + * @see #vi_set_xscale + */ +LIBDRAGON_PREVIEW_API +void vi_set_xscale_factor(float xfactor); + +/** + * @brief Set the vertical scaling factor. + * @preview + * + * Set the scale factor applied to the framebuffer height to display it on the screen. + * The \p yfactor term describes how many framebuffer pixels advance for each + * output scanline. For instance, a factor of 0.5 means that each framebuffer pixel + * will be repeated twice vertically on the screen, onto two scanlines. + * + * @param yfactor Horizontal scale factor to set + */ +LIBDRAGON_PREVIEW_API +void vi_set_yscale_factor(float yfactor); + +/** + * @brief Enable or disable the interlaced mode + * @preview + * + * @param interlaced If true, the VI will display the framebuffer in + * interlaced mode. + */ +LIBDRAGON_PREVIEW_API +void vi_set_interlaced(bool interlaced); + +/** + * @brief Enable / disable AA mode for the VI + * @preview + * + * The AA mode specifies a set of filters to apply to the framebuffer + * during the resampling process. See #vi_aa_mode_t for more information. + * + * @param aa_mode AA mode to set + */ +LIBDRAGON_PREVIEW_API +void vi_set_aa_mode(vi_aa_mode_t aa_mode); + +/** + * @brief Get the current VI AA mode + * @preview + * @return the current AA mode + */ +LIBDRAGON_PREVIEW_API +vi_aa_mode_t vi_get_aa_mode(void); + +/** + * @brief Enable / disable the divot filter + * @preview + * + * Divot filter is a post-processing filter, on top of anti-alias filters, + * that is designed to remove a few single-point artifacts that can appear + * in the framebuffer. + * + * Divot only makes sense if you are using an AA filter, that is, the AA mode + * is set to either #VI_AA_MODE_RESAMPLE_FETCH_NEEDED or + * #VI_AA_MODE_RESAMPLE_FETCH_ALWAYS. + * + * @param divot If true, the divot filter will be enabled + */ +LIBDRAGON_PREVIEW_API +void vi_set_divot(bool divot); + +/** + * @brief Enable / disable dedithering + * @preview + * + * Dedithering (or "dither filter") is a filter that tries to apply an + * error correction on top of a dithered 16-bit framebuffer, trying to + * restore part of the original 32-bit color information. It works best + * when the framebuffer was drawn with the RDP "magic square" dithering + * pattern, as it's designed to reverse exactly that. + * + * Dedithering only makes sense for 16-bit, dithered framebuffers. + * + * @note Dedithering is not compatible with all AA filters. In particular, it + * only works with #VI_AA_MODE_NONE and #VI_AA_MODE_RESAMPLE_FETCH_ALWAYS. + */ +LIBDRAGON_PREVIEW_API +void vi_set_dedither(bool dedither); + +/** + * @brief Get the current VI dedither filter state + * @preview + * @return true if dedithering is enabled + */ +LIBDRAGON_PREVIEW_API +bool vi_get_dedither(void); + +/** + * @brief Enable / disable gamma correction + * @preview + * + * VI is able to apply a gamma correction filter to the framebuffer. + * + * Gamma correction is meant to convert a framebuffer with pixels in linear + * RGB space, into the non-linear sRGB space expected by most displays. + * Normally, this is not required because all assets are already provided in + * sRGB color space, which is how most authoring tools save PNGs nowadays. + * + * Drawing in linear space would in theory be useful to produce more accurate + * lighting and blending effects, though testing showed that this is really + * only visible with 32 bpp framebuffers, which are normally not used. If you + * want to experiment with working in linear space, make sure to provide + * assets in that format (which normally means using the --gamma option to + * mksprite to convert sRGB PNG pixels into linear space). + * + * @param gamma Gamma correction mode to set + */ +LIBDRAGON_PREVIEW_API +void vi_set_gamma(vi_gamma_t gamma); + +/** + * @brief Set the active display output area + * @preview + * + * This function sets the active display output area to the specified + * coordinates. The area is expressed in screen dots. + * + * Notice that not all positions are valid for the output, so the + * function will clamp it to make sure to keep the display stable (and + * avoid crashing the VI, which is not very tolerant to invalid values). + * The bounds for the output area can be queried using #vi_get_output_bounds. + * + * @param x0 Horizontal start of the output area + * @param y0 Vertical start of the output area + * @param x1 Horizontal end of the output area (exclusive) + * @param y1 Vertical end of the output area (exclusive) + */ +LIBDRAGON_PREVIEW_API +void vi_set_output(int x0, int y0, int x1, int y1); + +/** + * @brief Set the specified start position for the active display output area + * @preview + * + * This function sets the start position for the active display area (without + * changing the size). The position is expressed in VI dots. + * + * Notice that not all positions are valid for the output, so the + * function will clamp it to make sure to keep the display stable (and + * avoid crashing the VI, which is not very tolerant to invalid values). + * + * To query the bounds for the output area, use #vi_get_output_bounds. Notice + * that you must ensure that the *full* output area fits the bounds; for instance + * if the horizontal bounds are 100-800 and output width is 500, the start + * position must be within 100 and 300. If you set it to 400, for instance, + * it will be automatically clamped to 300. + * + * @param x Horizontal scroll offset + * @param y Vertical scroll offset + * + * @see #vi_get_output_bounds + * @see #vi_get_output + * @see #vi_scroll_output + */ +LIBDRAGON_PREVIEW_API +void vi_move_output(int x, int y); + +/** + * @brief Scroll the active display area by the specified amount + * @preview + * + * This function is similar to #vi_move_output, but scrolls the display area + * by the specified relative amount, instead of setting the absolute position. + * + * Like #vi_move_output, the function will clamp the position to make + * sure it is within the allowed bounds. + * + * @param deltax Horizontal scroll amount + * @param deltay Vertical scroll amount + * + * @see #vi_move_output + * @see #vi_get_output_bounds + */ +LIBDRAGON_PREVIEW_API +void vi_scroll_output(int deltax, int deltay); + +/** + * @brief Calculate correct VI borders for a target aspect ratio. + * @preview + * + * This function calculates the appropriate VI borders to obtain the specified + * aspect ratio, and optionally adding a margin to make the picture CRT-safe. + * + * The margin is expressed as a percentage relative to the virtual VI display + * output (640x480). A good default for this margin for most CRTs is + * #VI_CRT_MARGIN (5%). + * + * For instance, to create a 16:9 resolution, you can do: + * + * \code{.c} + * vi_borders_t borders = vi_calc_borders(16./9, false, 0, 0); + * \endcode + * + * @param aspect_ratio Target aspect ratio + * @param overscan_margin Margin to add to compensate for TV overscan. Use 0 + * to use full picture (eg: for emulators), and something + * like #VI_CRT_MARGIN to get a good CRT default. + * @return The requested border settings + */ +LIBDRAGON_PREVIEW_API +vi_borders_t vi_calc_borders(float aspect_ratio, float overscan_margin); + +/** + * @brief Get the current display aspect ratio from VI configuration + * @preview + * + * Recomputes the aspect ratio from the current VI output area (as returned + * by the internal output rectangle) and the active timing preset dimensions. + * + * @return The current display aspect ratio (e.g. 4.0f/3.0f, 16.0f/9.0f) + */ +LIBDRAGON_PREVIEW_API +float vi_get_aspect_ratio(void); + +/** + * @brief Configure the output area via the specified borders + * @preview + * + * Configures the VI to the specified border size. This function is an + * alterantive to #vi_set_output: instead of specifying the output area + * directly, you can specify it in terms of borders with respect to the + * default output area. See #vi_get_borders for more information. + * + * @param b Size of the borders to apply + */ +LIBDRAGON_PREVIEW_API +void vi_set_borders(vi_borders_t b); + +/** + * @brief Show the specified surface (at next vblank) + * @preview + * + * This function is a shortcut to do the following: + * + * * Call #vi_set_origin to specify the new buffer, stride and bpp + * * Call #vi_set_xscale to set the horizontal scale factor + * * Call #vi_set_yscale to set the vertical scale factor + * * Call #vi_blank to disable blanking mode (if it was enabled) + * + * If the specified surface is NULL, instead, blanking mode will be + * enabled (via #vi_blank), so that the screen will be black. + * + * @param fb Surface to show + */ +LIBDRAGON_PREVIEW_API +void vi_show(surface_t *fb); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/include/video.h b/include/video.h new file mode 100644 index 000000000..c1c42c431 --- /dev/null +++ b/include/video.h @@ -0,0 +1,300 @@ +/** + * @file video.h + * @author Giovanni Bajo + * @brief Video player subsystem + * + * This library allows to playback videos on the N64, using video codecs that + * can be registered into it. It provides an uniform and flexible API for any + * video-related tasks, including advanced uses such as showing videos are part + * of a 3D scene, or making a game based on videos. + * + * If you just want to display a full-motion video (FMV) with audio, consider using + * the fmv.h module instead, which provides a much higher-level API to do + * just that. + * + * Currently two video codecs are supported and provided as part of libdragon: + * + * * #mpeg1_codec: MPEG1 + * * #h264_codec: H.264 Custom Profile (Baseline + Weighted Prediction) + * + * Higher-level information on how to encode videos and suggested settings can be found + * in the Libdragon wiki: https://github.com/n64brew/libdragon/wiki/MPEG1-Player + * + * The API in this file is quite simple. The main entry point is #video_open, + * which opens a video file and returns a handle to it. #video_get_info can be used + * to query information about the video, such as width, height, framerate, etc. + * + * To play the video, the main loop should call #video_next_frame any time + * a new frame is needed (depending on the desired playback frequency), + * and then #video_get_frame to get the frame to display. The frame is + * returned as a #yuv_frame_t. You can then use the YUV library to display + * it, either fullscreen, in a smaller portion, or even into an offscreen + * surface to be used eg. like a texture. + * + * In other words, this library only handles video decoding into a YUV surface + * in memory. It does not perform any display by itself; it is up to the + * client code to display the frames using the yuv.h library or any other + * method. + * + * Notice that the time required to decode a frame is not constant, and can + * vary a lot; especially I-frames tend to be much heavier to decode, so if + * possible allow for some buffering to avoid slowdowns. Buffering can be + * done by calling #video_poll when possible (eg: in idle time), so that the + * decoder can keep decoding frames ahead of time. + */ + +/** + * @defgroup video Video Subsystem + * @ingroup libdragon + * @brief Compressed video playback libraries (for FMVs, etc.) + * + * This group contains modules to play compressed video files, such as + * full-motion videos (FMVs): + * + * * yuv.h: Hardware-accelerated YUV to RGB conversion using RDP and RSP. + * It provides functions to blit YUV frames to RGB surfaces. + * * video.h: Video player API to open and playback video files. It allows + * to retrieve video information, decode video frames, and seek. + * This can be used for building custom video players, including + * using videos in more complex scenes (eg: as textures in 3d scenes). + * * fmv.h: Very High-level single-function API to playback full-motion videos, + * synchronized with audio. It provides a one-function-does-it-all + * API to play a video file with minimal code. + */ + +#ifndef LIBDRAGON_VIDEO_H +#define LIBDRAGON_VIDEO_H + +#include "preview.h" +#include "yuv.h" + +#ifdef __cplusplus +extern "C" { +#endif + +///@cond +struct video_s; +struct video_codec_s; +///@endcond + +/** @brief A video codec */ +typedef struct video_codec_s video_codec_t; + +/** @brief A video handle */ +typedef struct video_s video_t; + +/** @brief Video playback parameters*/ +typedef struct video_parms_s { + /** + * @brief Number of fully decoded pictures the decoder is allowed to keep buffered + * + * This acts as a "cushion" for the player, allowing background decoding + * (via #video_poll) to run ahead of time. Not all codecs use this setting. + * This setting obviously impacts the memory usage of the decoder; decoded + * pictures are kept in a codec-specific format, though this is normally + * YUV 4:2:0 (16 bpp). + * + * If 0 (default), #video_poll is effectively disabled and cannot decode ahead. + */ + int buffered_pics; +} video_parms_t; + +/** + * @brief Information about a video stream + * + * This structure contains information about a video stream. It is returned by + * #video_get_info. + */ +typedef struct video_info_s { + /** @brief Width of the video in pixels */ + int width; + /** @brief Height of the video in pixels */ + int height; + /** + * @brief Display aspect ratio (DAR) of the video (eg: 4.0/3.0) + * + * For normal video streams with square pixels, this will be identical + * to width/height. Some streams might be anamorphic, meaning that the picture + * has been encoded with non-square pixels. In this case, the aspect ratio + * will be different, and must be used to correctly display the video. + */ + float aspect_ratio; + /** @brief Framerate of the video in frames per second */ + float framerate; + /** + * @brief YUV colorspace for the video + * + * YUV pictures can be encoded with different colorspaces. This field + * describes the colorspace used by the video stream, so that the + * yuv.h library can correctly convert the frames to RGB. + */ + yuv_colorspace_t colorspace; +} video_info_t; + +/** + * @brief Register a video codec + * @preview + * + * This function registers a video codec into the video subsystem. It is necessary + * to call this function for each codec that the video subsystem must support, + * before calling #video_open. + * + * @param codec Video codec to register + */ +LIBDRAGON_PREVIEW_API +void video_register_codec(video_codec_t *codec); + +/** + * @brief Open a video file and decode its header info + * @preview + * + * This function opens a video file and returns a handle to it. The video + * file will be handled by the appropriate codec, based on the file extension. + * + * @param fn Path to the video file (including filesystem prefix) + * @param parms Optional parameter block (can be NULL) + * @return Handle to the opened video + */ +LIBDRAGON_PREVIEW_API +video_t* video_open(const char *fn, const video_parms_t *parms); + +/** + * @brief Get information about the video + * @preview + * + * This function retrieves information about the opened video, such as + * its width, height, framerate, aspect ratio, and colorspace. It is useful + * to configure the display subsystem before starting playback. + * + * @param v Handle to the video + * @return Information about the video + */ +LIBDRAGON_PREVIEW_API +video_info_t video_get_info(video_t *v); + +/** + * @brief Move to the next frame in the video stream + * @preview + * + * This function decodes the next frame in the video stream. If the frame + * is successfully decoded, it can be retrieved with #video_get_frame. Otherwise, + * the stream is finished and the function will return false. + * + * @param v Handle to the video + * @return true If a frame was successfully decoded + * @return false If the stream is finished + */ +LIBDRAGON_PREVIEW_API +bool video_next_frame(video_t *v); + +/** + * @brief Retrieve the last fully-decoded frame + * @preview + * + * This function returns the last frame decoded by #video_poll or #video_next_frame. + * The frame is returned as a #yuv_frame_t, which can be used to display the + * frame on the screen via the yuv.h library. + * + * @param v Handle to the video + * @return The current decoded frame + * + * @see #yuv_tex_blit + * @see #yuv_blitter_new_fmv + * @see #yuv_blitter_new + */ +LIBDRAGON_PREVIEW_API +yuv_frame_t video_get_frame(video_t *v); + +/** + * @brief Do some work of video decoding + * @preview + * + * This function tells the video decoder to do a chunk of work, and keep + * decoding the video stream ahead. It can be used for scenarios where the + * client needs to perform other tasks while the video is being decoded. + * + * Usage of this function is totally optionally. A client can simply call + * #video_next_frame repeatedly to decode the video stream frame by frame. + * It is however useful to make use of idle CPU time to buffer ahead. + * Depending on the exact video encoding parameters and how the codec is + * written, this function can decode multiple frames ahead of time. + * + * For #video_poll to decode ahead, video_parms_t::buffered_pics must + * be greater than 0 (when video_open is called). In fact this is the number + * of pictures that the decoder is allowed to keep buffered. + * + * The function returns 1 if the decoder did a chunk of work, or 0 + * if no work could be done. Typically, if poll returns 0, no more work + * will be possible until the client calls #video_next_frame to move to the + * next frame. + * + * @param v Handle to the video + * @return 0 Next frame is not yet ready + * @return 1 Next frame is ready + * @return -1 End of stream reached + */ +LIBDRAGON_PREVIEW_API +int video_poll(video_t *v); + +/** + * @brief Rewind the video to the beginning + * @preview + * + * @param v Handle to the video + */ +LIBDRAGON_PREVIEW_API +void video_rewind(video_t *v); + +/** + * @brief Seek the video to a specific frame index + * @preview + * + * In general, video codecs only allow fast seeking to keyframes. If the + * specified frame index is not a keyframe, the videoplayer will seek to the closest + * previous keyframe. In that case, the client will need to call #video_next_frame + * repeatedly until the desired frame index is reached. + * + * Seeking can be done instantly if the seek index has been generated at build + * time (using videoconv64 --seek). The seek index is stored in a file with + * the same name as the video, but with .seek extension. If no seek index file + * is found, seeking will be performed by linearly scanning the video, and + * will thus be slower. + * + * @param v Handle to the video + * @param frame_idx Index of the frame to seek to + * @return Actual frame index seeked to, or -1 if the codec does not + * support seeking (with or without the seektable). + */ +LIBDRAGON_PREVIEW_API +int video_seek(video_t *v, int frame_idx); + +/** + * @brief Check if a frame index is a seekpoint (keyframe) according to the seektable + * @preview + * + * This function returns true only if a seek index (.seek file) has been loaded for + * the specified video and the specified frame index is an exact entry in that table. + * If no seek index is available, the function returns false. + * + * @param v Handle to the video + * @param frame_idx Frame index to check + * @return true If the frame index is a seekpoint (keyframe) + * @return false Otherwise (including missing seektable) + */ +LIBDRAGON_PREVIEW_API +bool video_is_seekable(video_t *v, int frame_idx); + +/** + * @brief Close the video file + * @preview + * + * @param v Handle to the video + */ +LIBDRAGON_PREVIEW_API +void video_close(video_t *v); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/include/video_sync.h b/include/video_sync.h new file mode 100644 index 000000000..3376fecc9 --- /dev/null +++ b/include/video_sync.h @@ -0,0 +1,108 @@ +/** + * @file video_sync.h + * @author Giovanni Bajo + * @brief Video sync helper (frameskipping/seek decisions driven by a master clock) + * @ingroup video + * + * This module provides a reusable, display-agnostic helper to keep a decoded video + * in sync with an external master clock (typically audio). It does not perform any + * rendering or pacing; it only decides what the caller should do (render next, + * skip next, or seek and render). + */ +#ifndef LIBDRAGON_VIDEO_SYNC_H +#define LIBDRAGON_VIDEO_SYNC_H + +#include + +#include "video.h" + +#ifdef __cplusplus +extern "C" { +#endif + +///@cond +typedef struct video_sync_s video_sync_t; +///@endcond + +/** @brief Action kind returned by #video_sync_step */ +typedef enum { + /** Decode and render the next frame */ + VIDEO_SYNC_RENDER_NEXT = 0, + /** Decode the next frame but skip rendering it */ + VIDEO_SYNC_SKIP_NEXT = 1, + /** Seek to a specified frame (caller should then decode+render) */ + VIDEO_SYNC_SEEK_AND_RENDER = 2, +} video_sync_action_kind_t; + +/** @brief Action returned by #video_sync_step */ +typedef struct { + video_sync_action_kind_t kind; + /** Valid only when kind==VIDEO_SYNC_SEEK_AND_RENDER */ + int seek_frame; +} video_sync_action_t; + +/** @brief Parameters controlling sync behavior */ +typedef struct video_sync_params_s { + /** + * Desired time to recover from lag (in seconds). Larger => softer recovery. + * + * Default: 1.0 + */ + float recovery_time_sec; + /** + * If a keyframe exists within this window (in seconds) and budget allows, + * prefer a seek over present-skip. Use a negative value to disable + * keyframe-based seeking. + * + * Default: 0.20 + */ + float keyframe_window_sec; + /** + * If lag exceeds this threshold (in seconds), allow a more aggressive seek + * towards the current target frame. Use a negative value to disable + * aggressive seeking. + * + * Default: 0.75 + */ + float max_lag_sec; +} video_sync_params_t; + +/** + * @brief Create a video sync helper. + * + * @param v Video handle. Ownership is not transferred. + * @param p Optional parameters (NULL for defaults). Libdragon-style: fields left + * at 0 are treated as "use default". + */ +video_sync_t* video_sync_create(video_t *v, const video_sync_params_t *p); + +/** @brief Destroy a video sync helper */ +void video_sync_destroy(video_sync_t *s); + +/** + * @brief Reset internal state (call after external seek/rewind) + * + * @param s Sync helper + * @param frame_idx Current frame index after the external operation + */ +void video_sync_reset(video_sync_t *s, int frame_idx); + +/** + * @brief Decide what to do next to keep video in sync with master time. + * + * The function is pure decision-making: it does not call #video_next_frame or + * #video_seek by itself. The caller should execute the returned action. + * + * @param s Sync helper + * @param master_time_sec Master time in seconds (typically audio clock) + * @param cur_frame_idx Current frame index (the next frame that would be decoded/rendered) + */ +video_sync_action_t video_sync_step(video_sync_t *s, double master_time_sec, int cur_frame_idx); + +#ifdef __cplusplus +} +#endif + +#endif + + diff --git a/include/wav64.h b/include/wav64.h index 6b9c19631..bafe59b19 100644 --- a/include/wav64.h +++ b/include/wav64.h @@ -8,6 +8,8 @@ #ifndef __LIBDRAGON_WAV64_H #define __LIBDRAGON_WAV64_H + +#include "preview.h" #include "mixer.h" #ifdef __cplusplus @@ -15,6 +17,8 @@ extern "C" { #endif /// @cond +typedef struct wav64_state_s wav64_state_t; +extern void __wav64_init_compression_lvl2(void); extern void __wav64_init_compression_lvl3(void); /// @endcond @@ -37,11 +41,9 @@ typedef struct wav64_s { */ waveform_t wave; - /** @brief File descriptor to read WAV64 */ - int current_fd; ///< File descriptor for the wav64 file - int base_offset; ///< Start of Wav64 data. - int format; ///< Internal format of the file - void *ext; ///< Pointer to extended data (internal use) + ///@cond + wav64_state_t *st; // Extra opaque data + ///@endcond } wav64_t; /** @@ -52,9 +54,8 @@ typedef struct wav64_s { * for which no initialization is required. Level 0 (uncompressed) also * requires no initialization. * - * Currently, only level 3 requires initialization (level 2 does not exist yet). - * If you have any wav64 compressed with level 3, you must call this function - * before opening them. + * Levels 2 and 3 require initialization. If you have any wav64 compressed with + * either level, you must call this function before opening them. * * @code{.c} * wav64_init_compression(3); @@ -71,6 +72,7 @@ typedef struct wav64_s { switch (level) { \ case 0: break; \ case 1: break; \ + case 2: __wav64_init_compression_lvl2(); break; \ case 3: __wav64_init_compression_lvl3(); break; \ default: assertf(0, "Unsupported compression level: %d", level); \ } \ @@ -87,6 +89,58 @@ typedef struct wav64_s { */ void wav64_open(wav64_t *wav, const char *fn); +/** @brief WAV64 streaming mode */ +typedef enum { + /** + * @brief Full streaming + * + * This is the default mode for streaming. All samples of the files are + * streamed from the file (typically, from ROM but could also be SD) on-demand. + * + * This uses the least amount of memory but requires data transfers from + * the storage device during playback. + */ + WAV64_STREAMING_FULL, + + /** + * @brief Preload and decompress the whole file + * + * This mode preloads the whole file into memory and decompresses it in full + * at the beginning. This is useful for small files that can fit in memory, + * and for which you do not want to pay for the streaming overhead. + */ + WAV64_STREAMING_NONE, + +} wav64_streaming_mode_t; + +/** @brief WAV64 loading parameters (to be passed to #wav64_load) */ +typedef struct wav64_loadparms_s { + /** + * @brief Streaming mode for the wav64 + * + * See #wav64_streaming_mode_t for details. + */ + wav64_streaming_mode_t streaming_mode; + +} wav64_loadparms_t; + +/** + * @brief Load a WAV64 file for playback. + * @preview + * + * This function opens the file, parses the header, and initializes for + * playing back through the audio mixer. + * + * You can use the #wav64_loadparms_t structure to specify additional parameters + * for the loading process, like the maximum number of simultaneous playbacks + * or the streaming mode. + * + * @param fn Filename of the wav64 (with filesystem prefix). + * @param parms Optional loading parameters (or NULL for defaults). + */ +LIBDRAGON_PREVIEW_API +wav64_t *wav64_load(const char *fn, wav64_loadparms_t *parms); + /** @brief Configure a WAV64 file for looping playback. */ void wav64_set_loop(wav64_t *wav, bool loop); @@ -106,11 +160,28 @@ void wav64_set_loop(wav64_t *wav, bool loop); */ void wav64_play(wav64_t *wav, int ch); +/** + * @brief Seek a playing WAV64 to a given time position (in seconds). + * @preview + * + * This is a convenience wrapper around #mixer_ch_set_pos that calculates the + * nearest seekable position for the given time position, depending on how + * the file was compressed. In general, compressed audio files can only seek to + * specific precomputed skip points. + * + * @param wav Pointer to wav64_t structure + * @param ch Channel of the mixer to seek (same channel passed to #wav64_play) + * @param time_sec Desired time position (in seconds) + * @return The adjusted time in seconds that was actually used for seeking + */ +LIBDRAGON_PREVIEW_API +double wav64_seek(wav64_t *wav, int ch, double time_sec); + /** * @brief Get the (possibly compressed) bitrate of the WAV64 file. * * @param wav Pointer to wav64_t structure - * @return int Bitrate in bits per second + * @return Bitrate in bits per second */ int wav64_get_bitrate(wav64_t *wav); diff --git a/include/xm64.h b/include/xm64.h index 73743732f..f0186d1d0 100644 --- a/include/xm64.h +++ b/include/xm64.h @@ -37,9 +37,11 @@ #ifndef __LIBDRAGON_AUDIO_XM64_H #define __LIBDRAGON_AUDIO_XM64_H + #include #include #include +#include "preview.h" #ifdef __cplusplus extern "C" { @@ -60,10 +62,9 @@ typedef struct waveform_s waveform_t; */ typedef struct xm64player_s { xm_context_t *ctx; ///< libxm context - waveform_t *waves; ///< array of all waveforms (one per XM "sample") - int nwaves; ///< number of waves (XM "samples") - FILE *fh; ///< open handle of XM64 file + int fd; ///< open handle of XM64 file int first_ch; ///< first channel used in the mixer + int stream_ramsz; ///< RAM the mixer sample buffers take (set by #xm64player_play) bool playing; ///< playing flag bool stop_requested; ///< user requested stop playing bool looping; ///< true if the XM is configured to loop @@ -78,9 +79,12 @@ typedef struct xm64player_s { * This function requires the mixer to have been already initialized * (via mixer_init). * + * XM64 files can carry their own embedded samples, or can use an external + * sample library. In the latter case, make sure to call #xm64_set_extsampledir + * to set the directory where the external samples are stored. + * * @param player Pointer to the xm64player_t player structure to use - * @param fn Filename of the XM64 (with filesystem prefix). Currently, - * only files on DFS ("rom:/") are supported. + * @param fn Filename of the XM64 (with filesystem prefix). */ void xm64player_open(xm64player_t *player, const char *fn); @@ -185,6 +189,19 @@ void xm64player_set_effect_callback(xm64player_t *player, void (*cb)(void*, uint */ void xm64player_close(xm64player_t *player); +/** + * @brief Configure the directory where external samples are stored. + * @preview + * + * This function is used to set the directory where the external samples + * are stored. It is only used for XM64 files that use external samples. + * + * @param dir Directory where the external samples are stored. This + * can be on any filesystem, even different from XM64's one. + */ +LIBDRAGON_PREVIEW_API +void xm64_set_extsampledir(const char *dir); + #ifdef __cplusplus } #endif diff --git a/include/yuv.h b/include/yuv.h new file mode 100644 index 000000000..770684521 --- /dev/null +++ b/include/yuv.h @@ -0,0 +1,442 @@ +/** + * @file yuv.h + * @author Giovanni Bajo + * @brief Hardware accelerated YUV conversion + * @ingroup video + */ + +#ifndef __LIBDRAGON_YUV_H +#define __LIBDRAGON_YUV_H + + +/** + * @brief Convert YUV frames to RGB. + * + * This library contains hardware-accelerated function to convert a YUV image + * into a RGB image. The task is mainly performed using RDP, but the RSP can + * also be used to handle parts of it. + * + * It is possible to specify the exact colorspace to use for the conversions. + * Colorspaces are represented using the #yuv_colorspace_t. A few standard + * colorspaces are pre-defined as constants and can be used as-is: + * + * - #YUV_BT601_TV: BT.601 colorspace, limited range (16-235) for CRT TVs. + * - #YUV_BT601_FULL: BT.601 colorspace, full range (0-255) + * - #YUV_BT709_TV: BT.709 colorspace, limited range (16-235) for CRT TVs. + * - #YUV_BT709_FULL: BT.709 colorspace, full range (0-255) + * + * Normally, most encoders default to #YUV_BT601_TV for videos at Nintendo 64 + * resolutions, while #YUV_BT709_FULL is typically the defaults for modern + * HD or 4K videos. + * + * If you have some very specific use case, you can define your own colorspace + * using #yuv_new_colorspace. For testing purposes, #yuv_to_rgb can be used + * to convert a single YUV pixel to RGB using a specified colorspace. + * + * To blit a full frame, you can use #yuv_tex_blit, which is similar to + * #rdpq_tex_blit as it allows to copy an arbitrary sized frame and apply + * transformations to it (typically, scaling or flipping). + * + * To playback a video at maximum performance, it is recommended to use + * #yuv_blitter_t instead. A blitter is an object that can be used to + * perform multiple frame conversions with the same parameters (same input + * size, same output size, same scaling and alignment). It is similar to + * #rdpq_tex_blit in concept, but it precalculates most of the computations + * using a rspq block (see #rspq_block_t for more information), so that + * any time a conversion is needed, it is completely offloaded to the RSP+RDP + * with almost zero CPU overhead. + * + * You can create a #yuv_blitter_t using #yuv_blitter_new (which accepts + * parameters identical to #yuv_tex_blit), or the most handy #yuv_blitter_new_fmv + * which accepts more high-level parameters more optimized for the use case + * of a full-screen full motion video player. + * + */ + +#include +#include +#include "preview.h" +#include "graphics.h" +#include "rdpq_tex.h" + +#ifdef __cplusplus +extern "C" { +#endif + +///@cond +struct rspq_block_s; +typedef struct rspq_block_s rspq_block_t; +///@endcond + +/** + * @brief Initialize the YUV conversion library. + * @preview + */ +LIBDRAGON_PREVIEW_API +void yuv_init(void); + +/** + * @brief Shutdown the YUV conversion library + * @preview + */ +LIBDRAGON_PREVIEW_API +void yuv_close(void); + +/** + * @brief A YUV colorspace + * + * This structure contains the parameters that define a YUV colorspace + * for conversion to and from the RGB space. The "c" parameters are + * used when doing a CPU-based conversion (using #yuv_to_rgb), while + * the "k" parameters are used when doing a RDP-based conversion + * (see all the yuv_draw_frame_* functions). + * + * Most users can simply use one of the predefined colorspaces: #YUV_BT601_TV, + * #YUV_BT601_FULL, #YUV_BT709_TV, #YUV_BT709_FULL. To simplify creating a custom + * colorspace, #yuv_new_colorspace can be used. + * + * When playing back a video, you should specify the colorspace that was used + * to encode the video. Normally, this is available in the video header or + * stream as metadata information. Notice that most video encoders such as + * ffmpeg default to ITU-R BT601 TV Range when encoding low resolution movies + * (non-HD resolutions), so using #YUV_BT601_TV is a good default if the exact + * colorspace is not known. + * + * When encoding a video, it is suggested to configure the encoder to use + * ITU-R BT601 TV Range. In general, colorspaces created in the "TV Range" + * have more color fidelity when displayed on a CRT TV. For PC-only playback + * through emulators, "Full Range" colorspaces offer more color precision. + */ +typedef struct yuv_colorspace_s { + float c0, c1, c2, c3, c4; int y0; + int k0, k1, k2, k3, k4, k5; +} yuv_colorspace_t; + +/** @brief ITU-R BT.601 TV Range colorspace (DEFAULT). + * + * This is the standard colorspace used by low resolution videos such as those + * normally encoded for Nintendo 64. If in doubt, try first always with this + * colorspace. + * + * Created via #yuv_new_colorspace using the following parameters: + * + * * Kr=0.299, Kb=0.114 + * * Y0=16, yrange=219, crange=224 + */ +extern const yuv_colorspace_t YUV_BT601_TV; + +/** @brief ITU-R BT.601 Full Range colorspace. + * + * Created via #yuv_new_colorspace using the following parameters: + * + * * Kr=0.299, Kb=0.114 + * * Y0=0, yrange=256, crange=256 + */ +extern const yuv_colorspace_t YUV_BT601_FULL; + +/** @brief ITU-R BT.709 TV Range colorspace. + * + * Created via #yuv_new_colorspace using the following parameters: + * + * * Kr=0.0722, Kb=0.2126 + * * Y0=16, yrange=219, crange=224 + */ +extern const yuv_colorspace_t YUV_BT709_TV; + +/** + * @brief ITU-R BT.709 Full Range colorspace. + * + * Created via #yuv_new_colorspace using the following parameters: + * + * * Kr=0.0722, Kb=0.2126 + * * Y0=0, yrange=256, crange=256 + */ +extern const yuv_colorspace_t YUV_BT709_FULL; + + +/** + * @brief Calculate coefficients for a new YUV colorspace. + * @preview + * + * This function is mostly for documentation purposes. It can be used to create + * a new colorspace, by calculating its coefficients (stored into #yuv_colorspace_t) + * from the mathematical definition of the colorspace. Most users will not need + * to call this function and just use one of the predefined colorspaces such as + * #YUV_BT601_TV. + * + * A YUV colorspace is defined by three constants normally called Kr, Kg, Kb. + * Since the sum of these three constants must be 1.0, normally only two are + * provided (by convention, Kr and Kb). + * + * Because of technical issues with old analog TVs, it was not possible to + * display the full 8-bit range of the Y,U,V components, so during the conversion + * the range was often restricted a bit. The range for the Y component (as an + * 8-bit unsigned integer) is defined via the minimum allowed value @p y0 and the + * number of possible values @p yrange -- so the last allowed value is y0+yrange-1. + * The range for the U,V components (as 8-bit signed integers) is specified in + * @p crange and is always assumed to be centered around 0 (so the allowed values + * are -crange/2 to crange/2-1). + * + * For old TVs, colorspaces should use the "TV Range" which is defined as + * y0=19, yrange=219, crange=224. + * + * Videos meant to be played back on PC are probably coded using the "Full Range", + * which is y0=0, yrange=256, crange=256. + * + * @param[in] Kr The colorspace coefficient Kr + * @param[in] Kb The colorspace coefficient Kb + * @param[in] y0 The minimum allowed value for the Y component + * @param[in] yrange The number of allowed values for the Y component + * @param[in] crange The number of allowed values for the U,V component + * @return The new colorspace structure + * + * @see #YUV_BT601_TV + * @see #YUV_BT601_FULL + * @see #YUV_BT709_TV + * @see #YUV_BT709_FULL + * + */ +LIBDRAGON_PREVIEW_API +yuv_colorspace_t yuv_new_colorspace(float Kr, float Kb, int y0, int yrange, int crange); + + +/** + * @brief Convert a single YUV pixel into RGB. + * @preview + * + * Convert a single YUV pixel to RGB, using the CPU. This function + * should be used only for non-performance critical tasks. For high-performance + * conversions, see the yuv_draw_frame_* functions that are hardware + * accelerated via the RDP. + * + * Notice that this function is not meant to be bit-exact with the RDP hardware + * accelerated version, but it will return values which are very close. + * + * @param[in] y Y component + * @param[in] u U component + * @param[in] v V component + * @param cs The colorspace to use for the conversion + * + * @return The converted pixel in RGBA format (A is forced to 255). + */ +LIBDRAGON_PREVIEW_API +color_t yuv_to_rgb(uint8_t y, uint8_t u, uint8_t v, const yuv_colorspace_t *cs); + +/** @brief A YUV frame, made of three distinct planes */ +typedef struct yuv_frame_s { + surface_t y; ///< Luminance plane (Y) + surface_t u; ///< Chrominance plane (U) + surface_t v; ///< Chrominance plane (V) +} yuv_frame_t; + +/** @brief YUV blitter zoom configuration */ +typedef enum { + YUV_ZOOM_KEEP_ASPECT, ///< Zoom the frame, keeping frame aspect ratio + YUV_ZOOM_FULL, ///< Zoom the frame, irrespective of aspect ratio + YUV_ZOOM_NONE, ///< Do not zoom the frame to fit the output buffer +} yuv_zoom_t; + +/** @brief YUV blitter output buffer alignment */ +typedef enum { + YUV_ALIGN_CENTER, ///< Align to center of the output buffer + YUV_ALIGN_MIN, ///< Align to left/top of the output buffer + YUV_ALIGN_MAX, ///< Align to right/bottom of the output buffer +} yuv_align_t; + + +/** + * @brief YUV full motion video blitter configuration. + * + * These are the parameters that can be used to configure a YUV blitter via + * #yuv_blitter_new_fmv. They are designed for the use case of a full-screen + * full motion video player, where the video is optionally scaled to fit the screen. + */ +typedef struct yuv_fmv_parms_s { + const yuv_colorspace_t *cs; ///< Color space to use during conversion (default: #YUV_BT601_TV) + yuv_align_t halign; ///< Frame horizontal alignment to the output buffer (default: centered) + yuv_align_t valign; ///< Frame vertical alignment to the output buffer (default: centered) + yuv_zoom_t zoom; ///< Frame zooming algorithm to use (default: keep aspect ratio) + color_t bkg_color; ///< Color to use to clear the rest of the output buffer (default: opaque black) + /** + * @brief Display aspect ratio (DAR) of the video + * + * This should match the aspect ratio of the video file, that for instance + * can be read via #video_get_info (see #video_info_t::aspect_ratio). + * + * If not specified, default to video_width/video_height. + */ + float video_aspect_ratio; + /** + * @brief Aspect ratio of the output display picture + * + * This should match the aspect ratio configured via #display_init, or + * read by #vi_get_aspect_ratio. If not specified, default to 4:3 just + * like #display_init does. + */ + float display_aspect_ratio; + /** + * @brief Enable RDP dithering (magic square) + * + * This is useful when drawing to a 16bpp framebuffer, to avoid banding + * artifacts. If possible, use in conjunction with dedithering at the VI + * level (see #vi_set_dedither, or pass #FILTERS_RESAMPLE_ANTIALIAS_DEDITHER + * to #display_init). + */ + bool enable_dithering; +} yuv_fmv_parms_t; + +/** + * @brief An optimized YUV blitter, meant for drawing multiple frames. + * + * This structure represents a YUV blitter, which is an engine capable of + * drawing multiple YUV frames onto a RGB target surface. + * + * The blitter is created by #yuv_blitter_new or #yuv_blitter_new_fmv, + * providing all parameters that describe how to perform the blitting. At + * creation time, the blitting operation is recorded into a rspq block, so + * that the blitting itself (performed by #yuv_blitter_run) uses almost zero + * CPU time. + * + * Once a blitter is not used anymore, remember to call #yuv_blitter_free to + * release the memory. + */ +typedef struct yuv_blitter_s { + rspq_block_t *block; ///< RSPQ block containing the blitting operation +} yuv_blitter_t; + + +/** + * @brief Create a YUV blitter optimized for rendering multiple frames with + * @preview + * some possible transformation. + * + * This function is similar to #yuv_blitter_new_fmv but initializes the + * blitter using the same interface of #yuv_tex_blit or #rdpq_tex_blit. The + * interface allows to handle on-the-fly arbitrary transformations of the + * blitter (including scaling and rotations) and also cropping. It is indeed + * a superset of what is possible through #yuv_blitter_new_fmv, but its API + * might be a bit harder to use for people that just want to do a full-motion + * video player. + * + * In general, refer to #rdpq_tex_blit for more in-depth documentation + * related to @p x0 , @p y0 , and @p parms . + * + * The blitter initialized by this function must be freed with #yuv_blitter_free + * to release all allocated memory. + * + * @param video_width Width of the video in pixels + * @param video_height Height of the video in pixels + * @param x0 X coordinate on the framebuffer where to draw the surface + * @param y0 Y coordinate on the framebuffer where to draw the surface + * @param parms Parameters for the blit operation (or NULL for default) + * @param cs Colorspace to use for the conversion (or NULL for #YUV_BT601_TV) + * @return An initialized blitter instance. + * + * @see #yuv_blitter_new_fmv + * @see #yuv_blitter_run + */ +LIBDRAGON_PREVIEW_API +yuv_blitter_t yuv_blitter_new(int video_width, int video_height, + float x0, float y0, const rdpq_blitparms_t *parms, const yuv_colorspace_t *cs); + +/** + * @brief Create a YUV blitter optimized for FMV drawing (full screen movie player) + * @preview + * + * This function creates a YUV blitter, using a configuration that is suited + * for full motion videos. By default (passing NULL as @p parms ), the blitter + * will draw each frame centered on the screen, and zooming it while maintaining + * its aspect ratio. Moreover, areas outside of the video will be filled with + * the black color. This is a good default for a full screen video player. + * + * By configuring @p parms , it is possible to tune the behavior of the player + * in several details: color space, alignment of the frame, type of zoom, + * and fill color. + * + * The blitter initialized by this function must be freed with #yuv_blitter_free + * to release all allocated memory. + * + * @param video_width Width of the video in pixels + * @param video_height Height of the video in pixels + * @param screen_width Width of the screen in pixels + * @param screen_height Height of the screen in pixels + * @param parms Optional parameters (can be NULL) + * @return An initialized blitter instance. + * + * @see #yuv_blitter_new + * @see #yuv_blitter_run + */ +LIBDRAGON_PREVIEW_API +yuv_blitter_t yuv_blitter_new_fmv(int video_width, int video_height, + int screen_width, int screen_height, const yuv_fmv_parms_t *parms); + + +/** + * @brief Perform a YUV blit using a blitter, with the specified surfaces + * @preview + * + * This function performs blitting of a YUV frame (converting it into RGB). + * The source frame is expected to be split into 3 planes. The conversion + * will be performed by a mix of RSP and RDP, and will be drawn to the currently + * attached surface (see #rdpq_attach). + * + * The blitter is configured at creation time with parameters that describe + * where to draw into the buffer, whether to perform a zoom, etc. + * + * @param blitter Blitter created by #yuv_blitter_new_fmv or #yuv_blitter_new + * @param frame YUV frame to blit + */ +LIBDRAGON_PREVIEW_API +void yuv_blitter_run(yuv_blitter_t *blitter, yuv_frame_t *frame); + +/** + * @brief Free the memory allocated by a blitter + * @preview + * + * This function release the memory allocated on a #yuv_blitter_t instance. + * After calling this function, the blitter instance cannot be used anymore + * and must be initialized again. + * + * @param blitter Blitter to free + */ +LIBDRAGON_PREVIEW_API +void yuv_blitter_free(yuv_blitter_t *blitter); + +/** + * @brief Blit a 3-planes YUV frame into the current RDP framebuffer. + * @preview + * + * This function is similar to #rdpq_tex_blit, but it allows to blit + * a YUV frame split into 3 planes. This is faster than first merging the + * 3 planes into a single buffer (as required by #FMT_YUV16) and then blit it. + * + * This is an all-in-one function that avoids creating a #yuv_blitter_t instance, + * using it and then freeing it. On the other hand, it performs a lot of work + * on the CPU which the blitter does only one time (at creation time). Unless you + * only need to convert one frame, you should consider using the blitter + * for improved speed. + * + * For more information on how to use this function, see #rdpq_tex_blit. + * + * @param frame YUV frame to blit + * @param x0 X coordinate where to blit the frame + * @param y0 Y coordinate where to blit the frame + * @param parms Optional blitting parameters (see #rdpq_blitparms_t) + * @param cs Optional colorspace to use for the conversion. If NULL, + * the default is #YUV_BT601_TV. + * + * @see #rdpq_tex_blit + * @see #yuv_blitter_t + * @see #yuv_blitter_new + * @see #yuv_blitter_new_fmv + */ +LIBDRAGON_PREVIEW_API +void yuv_tex_blit(yuv_frame_t *frame, float x0, float y0, + const rdpq_blitparms_t *parms, const yuv_colorspace_t *cs); + + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/libdragon.version b/libdragon.version new file mode 100644 index 000000000..a73e59d5e --- /dev/null +++ b/libdragon.version @@ -0,0 +1,6 @@ +{ + "branch": "preview", + "hash": "$Format:%H$", + "commit-date": "$Format:%cs$", + "dirty": false +} diff --git a/n64.ld b/n64.ld index 3b40b9b07..39f0113a0 100644 --- a/n64.ld +++ b/n64.ld @@ -57,6 +57,9 @@ SECTIONS { *(.rodata.*) KEEP(*(keep.rodata.*)) *(.gnu.linkonce.r.*) + . = ALIGN(4); + __tdata_align = .; + LONG (ALIGNOF(.tdata)); . = ALIGN(8); } @@ -77,7 +80,9 @@ SECTIONS { __wrap___do_global_ctors and enables it via the --wrap linker option. When linking with ld, we should use __do_global_ctors, which is the default if you don't provide the --wrap option. */ - KEEP(*(.ctors)) + KEEP(*(EXCLUDE_FILE (*crtend.o) .ctors)) + KEEP(*(SORT(.ctors.*))) + KEEP(*crtend.o(.ctors)) /* Similarly we should have a; LONG(0) @@ -106,7 +111,29 @@ SECTIONS { *(.gnu.linkonce.d.*) . = ALIGN(8); } - + + .tdata : { + __tls_base = .; + __tdata_start = .; + *(.tdata) + *(.tdata.*) + *(.gnu.linkonce.td.*) + __tdata_end = .; + } + + .tbss : { + __tbss_start = .; + *(.tbss) + *(.tbss.*) + *(.gnu.linkonce.tb.*) + . = ALIGN(8); + __tbss_end = .; + __tls_end = .; + } + + /* Fix dot for TBSS Sections */ + . = .+(__tbss_end-__tbss_start); + /* Small data START */ .sdata : { @@ -150,6 +177,9 @@ SECTIONS { *(.bss*) *(.gnu.linkonce.b.*) *(COMMON) + . = ALIGN(8); + __th_tdata_copy = .; + . = .+(__tdata_end-__tdata_start); . = ALIGN(8); __bss_end = .; } diff --git a/n64.mk b/n64.mk index 4024c610b..5d8f39cc5 100644 --- a/n64.mk +++ b/n64.mk @@ -1,21 +1,29 @@ BUILD_DIR ?= . SOURCE_DIR ?= . +# Preview API policy (must be set before including this file): +# 0 — using a preview API is a compile-time error (default) +# 1 — preview APIs are usable but produce a compiler warning +# 2 — preview APIs are fully unlocked (no diagnostics) +LIBDRAGON_PREVIEW ?= 2 + # Override this if your project uses a different directory for your DFS filesystem root N64_MKDFS_ROOT ?= filesystem -N64_ROM_TITLE = "Made with libdragon" # Override this with the name of your game or project -N64_ROM_CATEGORY = # Set an N64 Media Category code in the ROM header (N, D, C, E, Z) -N64_ROM_SAVETYPE = # Supported savetypes: none eeprom4k eeprom16 sram256k sram768k sram1m flashram -N64_ROM_RTC = # Set to true to enable the Joybus Real-Time Clock -N64_ROM_REGIONFREE = # Set to true to allow booting on any console region -N64_ROM_REGION = # Set to a region code (emulators will boot on a specific console region) +N64_ROM_TITLE ?= "Made with libdragon" # Override this with the name of your game or project +N64_ROM_CATEGORY ?= N # Set an N64 Media Category code in the ROM header (N, D, C, E, Z) +N64_ROM_SAVETYPE ?= none # Supported savetypes: none eeprom4k eeprom16k sram256k sram768k sram1m flashram +N64_ROM_EXPANSIONPAK ?= unspecified # Supported expansion pak types: unspecified unused recommended required +N64_ROM_RTC ?= # Set to enable the Joybus Real-Time Clock +N64_ROM_REGIONFREE ?= 1 # Set to allow booting on any console region +N64_ROM_REGION ?= # Set to a region code (emulators will boot on a specific console region) N64_ROM_ELFCOMPRESS ?= 1 # Set compression level of ELF file in ROM N64_ROM_DSOCOMPRESS ?= 1 # Set compression level of DSOs file in ROM -N64_ROM_CONTROLLER1 = # Sets the type of Controller 1 in the Advanced Homebrew Header. This could influence emulator behaviour such as Ares' -N64_ROM_CONTROLLER2 = # Sets the type of Controller 2 in the Advanced Homebrew Header. This could influence emulator behaviour such as Ares' -N64_ROM_CONTROLLER3 = # Sets the type of Controller 3 in the Advanced Homebrew Header. This could influence emulator behaviour such as Ares' -N64_ROM_CONTROLLER4 = # Sets the type of Controller 4 in the Advanced Homebrew Header. This could influence emulator behaviour such as Ares' +N64_ROM_CONTROLLER1 ?= # Sets the type of Controller 1 in the Advanced Homebrew Header. This could influence emulator behaviour such as Ares' +N64_ROM_CONTROLLER2 ?= # Sets the type of Controller 2 in the Advanced Homebrew Header. This could influence emulator behaviour such as Ares' +N64_ROM_CONTROLLER3 ?= # Sets the type of Controller 3 in the Advanced Homebrew Header. This could influence emulator behaviour such as Ares' +N64_ROM_CONTROLLER4 ?= # Sets the type of Controller 4 in the Advanced Homebrew Header. This could influence emulator behaviour such as Ares' +N64_ROM_METADATA ?= # Path to a metadata INI file to embed in the ROM. If set, invokes n64metadata. # Override this to use a different file prefix for the debug symbols. This is # useful when building multiple projects in the same directory and you can set @@ -23,7 +31,7 @@ N64_ROM_CONTROLLER4 = # Sets the type of Controller 4 in the Advanced Homebrew H # .PHONY: tiny3d # tiny3d: # $(MAKE) -C $(T3D_INST) N64_BACKTRACE_FILE_PREFIX=tiny3d -N64_BACKTRACE_FILE_PREFIX= +N64_BACKTRACE_FILE_PREFIX ?= # Override this to use a toolchain installed separately from libdragon N64_GCCPREFIX ?= $(N64_INST) @@ -60,13 +68,16 @@ N64_TOOL = $(N64_BINDIR)/n64tool N64_SYM = $(N64_BINDIR)/n64sym N64_ELFCOMPRESS = $(N64_BINDIR)/n64elfcompress N64_AUDIOCONV = $(N64_BINDIR)/audioconv64 +N64_VIDEOCONV = $(N64_BINDIR)/videoconv64 N64_MKSPRITE = $(N64_BINDIR)/mksprite N64_MKFONT = $(N64_BINDIR)/mkfont +N64_MKMODEL = $(N64_BINDIR)/mkmodel +N64_METADATA = $(N64_BINDIR)/n64metadata N64_DSO = $(N64_BINDIR)/n64dso N64_DSOEXTERN = $(N64_BINDIR)/n64dso-extern N64_DSOMSYM = $(N64_BINDIR)/n64dso-msym -N64_C_AND_CXX_FLAGS = -march=vr4300 -mtune=vr4300 -mabi=o64 -I$(N64_INCLUDEDIR) +N64_C_AND_CXX_FLAGS = -march=vr4300 -mtune=vr4300 -mabi=o64 -I$(N64_INCLUDEDIR)/newlib_overrides -I$(N64_INCLUDEDIR) -include ktls.h N64_C_AND_CXX_FLAGS += -falign-functions=32 # NOTE: if you change this, also change backtrace() in backtrace.c N64_C_AND_CXX_FLAGS += -ffunction-sections -fdata-sections -g -ffile-prefix-map="$(CURDIR)"=$(N64_BACKTRACE_FILE_PREFIX) N64_C_AND_CXX_FLAGS += -ffast-math -ftrapping-math -fno-associative-math @@ -79,18 +90,35 @@ N64_ASFLAGS = -mtune=vr4300 -march=vr4300 -mabi=o64 -Wa,--fatal-warnings -I$(N64 N64_RSPASFLAGS = -march=mips1 -mabi=32 -Wa,--fatal-warnings -I$(N64_INCLUDEDIR) N64_LDFLAGS = -g -L$(N64_LIBDIR) -ldragon -lm -ldragonsys -Tn64.ld --gc-sections --wrap __do_global_ctors N64_DSOLDFLAGS = --emit-relocs --unresolved-symbols=ignore-all --nmagic -T$(N64_LIBDIR)/dso.ld +ifneq ($(filter 1 2,$(LIBDRAGON_PREVIEW)),) +N64_C_AND_CXX_FLAGS += -DLIBDRAGON_PREVIEW=$(LIBDRAGON_PREVIEW) +N64_ASFLAGS += -DLIBDRAGON_PREVIEW=$(LIBDRAGON_PREVIEW) +N64_RSPASFLAGS += -DLIBDRAGON_PREVIEW=$(LIBDRAGON_PREVIEW) +endif -N64_TOOLFLAGS = --title $(N64_ROM_TITLE) +N64_TOOLFLAGS = --toc +N64_TOOLFLAGS += --title $(N64_ROM_TITLE) N64_TOOLFLAGS += $(if $(N64_ROM_HEADER),--header $(N64_ROM_HEADER)) N64_TOOLFLAGS += $(if $(N64_ROM_CATEGORY),--category $(N64_ROM_CATEGORY)) N64_TOOLFLAGS += $(if $(N64_ROM_REGION),--region $(N64_ROM_REGION)) N64_ED64ROMCONFIGFLAGS = $(if $(N64_ROM_SAVETYPE),--savetype $(N64_ROM_SAVETYPE)) +N64_ED64ROMCONFIGFLAGS += $(if $(N64_ROM_EXPANSIONPAK),--expansionpak $(N64_ROM_EXPANSIONPAK)) N64_ED64ROMCONFIGFLAGS += $(if $(N64_ROM_RTC),--rtc) N64_ED64ROMCONFIGFLAGS += $(if $(N64_ROM_REGIONFREE),--regionfree) N64_ED64ROMCONFIGFLAGS += $(if $(N64_ROM_CONTROLLER1),--controller1 $(N64_ROM_CONTROLLER1)) N64_ED64ROMCONFIGFLAGS += $(if $(N64_ROM_CONTROLLER2),--controller2 $(N64_ROM_CONTROLLER2)) N64_ED64ROMCONFIGFLAGS += $(if $(N64_ROM_CONTROLLER3),--controller3 $(N64_ROM_CONTROLLER3)) N64_ED64ROMCONFIGFLAGS += $(if $(N64_ROM_CONTROLLER4),--controller4 $(N64_ROM_CONTROLLER4)) +N64_METADATAFLAGS = $(if $(V),-v) + +# If metadata is used, disable padding to avoid double padding (n64tool + n64metadata). +# n64metadata will handle the final 16 KiB padding. +ifneq ($(N64_ROM_METADATA),) +N64_TOOLFLAGS += --padding 0 +endif + +# Add *.version files to the rompak +N64_TOOLFILES = $(wildcard $(N64_INCLUDEDIR)/*.version) ifeq ($(D),1) CFLAGS+=-g3 @@ -118,25 +146,25 @@ RSPASFLAGS+=-MMD %.z64: LDFLAGS+=$(N64_LDFLAGS) %.z64: $(BUILD_DIR)/%.elf @echo " [Z64] $@" - $(N64_SYM) $< $<.sym + $(N64_SYM) --all $< $<.sym cp $< $<.stripped $(N64_STRIP) -s $<.stripped $(N64_ELFCOMPRESS) -o $(dir $<) -c $(N64_ROM_ELFCOMPRESS) $<.stripped @rm -f $@ - DFS_FILE="$(filter %.dfs, $^)"; \ - if [ -z "$$DFS_FILE" ]; then \ - $(N64_TOOL) $(N64_TOOLFLAGS) --toc --output $@ --align 256 $<.stripped --align 8 $<.sym --align 8; \ - else \ - MSYM_FILE="$(filter %.msym, $^)"; \ - if [ -z "$$MSYM_FILE" ]; then \ - $(N64_TOOL) $(N64_TOOLFLAGS) --toc --output $@ --align 256 $<.stripped --align 8 $<.sym --align 16 "$$DFS_FILE"; \ - else \ - $(N64_TOOL) $(N64_TOOLFLAGS) --toc --output $@ --align 256 $<.stripped --align 8 $<.sym --align 8 "$$MSYM_FILE" --align 16 "$$DFS_FILE"; \ - fi \ - fi + $(N64_TOOL) $(N64_TOOLFLAGS) --output $(BUILD_DIR)/$@.tmp \ + --align 256 $<.stripped \ + $<.sym \ + $(filter %.dfs, $^) \ + $(filter %.msym, $^) \ + $(N64_TOOLFILES) if [ ! -z "$(strip $(N64_ED64ROMCONFIGFLAGS))" ]; then \ - $(N64_ED64ROMCONFIG) $(N64_ED64ROMCONFIGFLAGS) $@; \ + $(N64_ED64ROMCONFIG) $(N64_ED64ROMCONFIGFLAGS) $(BUILD_DIR)/$@.tmp; \ + fi + if [ ! -z "$(N64_ROM_METADATA)" ]; then \ + echo " [METADATA] $@"; \ + $(N64_METADATA) $(N64_METADATAFLAGS) $(BUILD_DIR)/$@.tmp $(N64_ROM_METADATA); \ fi + @mv $(BUILD_DIR)/$@.tmp $@ %.v64: %.z64 @echo " [V64] $@" @@ -158,27 +186,36 @@ $(BUILD_DIR)/%.o: $(SOURCE_DIR)/%.S SYMPREFIX="$(subst .,_,$(subst /,_,$(basename $@)))"; \ TEXTSECTION="$(basename $@).text"; \ DATASECTION="$(basename $@).data"; \ + METASECTION="$(basename $@).meta"; \ BINARY="$(basename $@).elf"; \ echo " [RSP] $<"; \ - $(N64_CC) $(RSPASFLAGS) -L$(N64_LIBDIR) -nostartfiles -Wl,-Trsp.ld -Wl,--gc-sections -Wl,-Map=$(BUILD_DIR)/$(notdir $(basename $@)).map -o $@ $<; \ + $(N64_CC) $(RSPASFLAGS) -L$(N64_LIBDIR) -nostartfiles -Wl,-Trsp.ld -Wl,--gc-sections -Wl,-Map=$(BUILD_DIR)/$(notdir $(basename $@)).map,--cref -o $@ $<; \ mv "$@" $$BINARY; \ $(N64_OBJCOPY) -O binary -j .text $$BINARY $$TEXTSECTION.bin; \ $(N64_OBJCOPY) -O binary -j .data $$BINARY $$DATASECTION.bin; \ + $(N64_OBJCOPY) -O binary -j .meta $$BINARY $$METASECTION.bin --set-section-flags .meta=alloc,load; \ + [ -s $$METASECTION.bin ] || printf '\0' > $$METASECTION.bin; \ $(N64_OBJCOPY) -I binary -O elf32-bigmips -B mips4300 \ --redefine-sym _binary_$${SYMPREFIX}_text_bin_start=$${FILENAME}_text_start \ --redefine-sym _binary_$${SYMPREFIX}_text_bin_end=$${FILENAME}_text_end \ --redefine-sym _binary_$${SYMPREFIX}_text_bin_size=$${FILENAME}_text_size \ - --set-section-alignment .data=8 \ + --set-section-alignment .data=16 \ --rename-section .text=.data $$TEXTSECTION.bin $$TEXTSECTION.o; \ $(N64_OBJCOPY) -I binary -O elf32-bigmips -B mips4300 \ --redefine-sym _binary_$${SYMPREFIX}_data_bin_start=$${FILENAME}_data_start \ --redefine-sym _binary_$${SYMPREFIX}_data_bin_end=$${FILENAME}_data_end \ --redefine-sym _binary_$${SYMPREFIX}_data_bin_size=$${FILENAME}_data_size \ - --set-section-alignment .data=8 \ + --set-section-alignment .data=16 \ --rename-section .text=.data $$DATASECTION.bin $$DATASECTION.o; \ + $(N64_OBJCOPY) -I binary -O elf32-bigmips -B mips4300 \ + --redefine-sym _binary_$${SYMPREFIX}_meta_bin_start=$${FILENAME}_meta_start \ + --redefine-sym _binary_$${SYMPREFIX}_meta_bin_end=$${FILENAME}_meta_end \ + --redefine-sym _binary_$${SYMPREFIX}_meta_bin_size=$${FILENAME}_meta_size \ + --set-section-alignment .data=16 \ + --rename-section .text=.data $$METASECTION.bin $$METASECTION.o; \ $(N64_SIZE) -G $$BINARY; \ - $(N64_LD) -relocatable $$TEXTSECTION.o $$DATASECTION.o -o $@; \ - rm $$TEXTSECTION.bin $$DATASECTION.bin $$TEXTSECTION.o $$DATASECTION.o; \ + $(N64_LD) -relocatable $$TEXTSECTION.o $$DATASECTION.o $$METASECTION.o -o $@; \ + rm $$TEXTSECTION.bin $$DATASECTION.bin $$METASECTION.bin $$TEXTSECTION.o $$DATASECTION.o $$METASECTION.o; \ else \ echo " [AS] $<"; \ $(CC) -c $(ASFLAGS) -o $@ $<; \ @@ -201,11 +238,11 @@ $(BUILD_DIR)/%.o: $(SOURCE_DIR)/%.cpp # between ld when it comes to global ctors dtors. Also see __do_global_ctors EXTERNS_FILE="$(filter %.externs, $^)"; \ if [ -z "$$EXTERNS_FILE" ]; then \ - $(CXX) -o $@ $(filter %.o, $^) $(filter-out $(N64_LIBDIR)/libdragon.a $(N64_LIBDIR)/libdragonsys.a, $(filter %.a, $^)) \ - -lc -mabi=o64 $(patsubst %,-Wl$(COMMA)%,$(LDFLAGS)) -Wl,-Map=$(BUILD_DIR)/$(notdir $(basename $@)).map; \ + $(CXX) $(N64_C_AND_CXX_FLAGS) -o $@ $(filter %.o, $^) $(filter-out $(N64_LIBDIR)/libdragon.a $(N64_LIBDIR)/libdragonsys.a, $(filter %.a, $^)) \ + -lc -mabi=o64 $(patsubst %,-Wl$(COMMA)%,$(LDFLAGS)) -Wl,-Map=$(BUILD_DIR)/$(notdir $(basename $@)).map,--cref; \ else \ - $(CXX) -o $@ $(filter %.o, $^) $(filter-out $(N64_LIBDIR)/libdragon.a $(N64_LIBDIR)/libdragonsys.a, $(filter %.a, $^)) \ - -lc -mabi=o64 $(patsubst %,-Wl$(COMMA)%,$(LDFLAGS)) -Wl,-T"$$EXTERNS_FILE" -Wl,-Map=$(BUILD_DIR)/$(notdir $(basename $@)).map; \ + $(CXX) $(N64_C_AND_CXX_FLAGS) -o $@ $(filter %.o, $^) $(filter-out $(N64_LIBDIR)/libdragon.a $(N64_LIBDIR)/libdragonsys.a, $(filter %.a, $^)) \ + -lc -mabi=o64 $(patsubst %,-Wl$(COMMA)%,$(LDFLAGS)) -Wl,-T"$$EXTERNS_FILE" -Wl,-Map=$(BUILD_DIR)/$(notdir $(basename $@)).map,--cref; \ fi $(N64_SIZE) -G $@ @@ -215,8 +252,8 @@ $(BUILD_DIR)/%.o: $(SOURCE_DIR)/%.cpp %.dso: CXX=$(N64_CXX) %.dso: AS=$(N64_AS) %.dso: LD=$(N64_LD) -%.dso: CFLAGS+=$(N64_CFLAGS) -mno-gpopt -DN64_DSO $(DSO_CFLAGS) -%.dso: CXXFLAGS+=$(N64_CXXFLAGS) -mno-gpopt -DN64_DSO $(DSO_CXXFLAGS) +%.dso: CFLAGS+=$(N64_CFLAGS) -G 0 -DN64_DSO $(DSO_CFLAGS) +%.dso: CXXFLAGS+=$(N64_CXXFLAGS) -G 0 -DN64_DSO $(DSO_CXXFLAGS) %.dso: ASFLAGS+=$(N64_ASFLAGS) %.dso: RSPASFLAGS+=$(N64_RSPASFLAGS) @@ -228,16 +265,27 @@ $(BUILD_DIR)/%.o: $(SOURCE_DIR)/%.cpp $(N64_LD) $(N64_DSOLDFLAGS) -Map=$(basename $(DSO_ELF)).map -o $(DSO_ELF) $(filter %.o, $^) $(N64_SIZE) -G $(DSO_ELF) $(N64_DSO) -o $(dir $@) -c $(N64_ROM_DSOCOMPRESS) $(DSO_ELF) - $(N64_SYM) $(DSO_ELF) $@.sym + $(N64_SYM) --all $(DSO_ELF) $@.sym %.externs: @echo " [DSOEXTERN] $@" $(N64_DSOEXTERN) -o $@ $^ -%.msym: %.elf +%.msym: @echo " [MSYM] $@" - $(N64_DSOMSYM) $< $@ - + EXTERNS_FILE="$(filter %.externs, $^)"; \ + INPUT_FILE="$(filter %.elf, $^)"; \ + if [ -z "$$EXTERNS_FILE" ]; then \ + $(N64_DSOMSYM) "$$INPUT_FILE" $@; \ + else \ + $(N64_DSOMSYM) -e "$$EXTERNS_FILE" "$$INPUT_FILE" $@; \ + fi + +# If a recipe fails, delete the target it was building. Otherwise a partially +# written file would be left behind, and the next build would consider it +# up-to-date and never retry the failed step. +.DELETE_ON_ERROR: + ifneq ($(V),1) .SILENT: endif diff --git a/rsp.ld b/rsp.ld index 67921ac7c..1153aa0ec 100644 --- a/rsp.ld +++ b/rsp.ld @@ -63,5 +63,10 @@ SECTIONS } } > ram_data AT > rom_dmem + .meta : { + KEEP(*(.meta)) + *(.meta.*) + } + /DISCARD/ : { *(.MIPS.abiflags) } } diff --git a/scripts/build_mgfx.sh b/scripts/build_mgfx.sh new file mode 100644 index 000000000..fb154a59b --- /dev/null +++ b/scripts/build_mgfx.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# +# This script invokes RSPL to compile all variants of the mgfx vertex shader. + +# Bash strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/ +set -euo pipefail +IFS=$'\n\t' + +if [[ -z ${RSPL_INST-} ]]; then + echo RSPL_INST environment variable is not defined + echo Please set RSPL_INST to point to the RSPL root directory + exit 1 +fi + +RSPL=${RSPL_INST}/dist/cli.mjs +MGFX_DIR=src/magma/ +MGFX_RSPL=${MGFX_DIR}/rsp_mgfx.rspl + +node ${RSPL} ${MGFX_RSPL} --magma --reorder --opt-time=60 --opt-worker=16 -o ${MGFX_DIR}/rsp_mgfx.S +node ${RSPL} ${MGFX_RSPL} --magma --reorder --opt-time=60 --opt-worker=16 -D ENABLE_ENV_MAP -o ${MGFX_DIR}/rsp_mgfx_env.S diff --git a/scripts/fix_commit_authors.py b/scripts/fix_commit_authors.py new file mode 100644 index 000000000..b644e6216 --- /dev/null +++ b/scripts/fix_commit_authors.py @@ -0,0 +1,502 @@ +#!/usr/bin/env python3 +""" +This script should be run after cherry-picking or squashing commits from a +source branch (eg: preview) to a target branch (eg: trunk) to preserve proper +authorship attribution. + +It analyzes commits in a given range to determine the correct authorship based on +the lines modified or added. It compares changes against the upstream branch using +`git blame` to identify the original authors of the code. + +The script can automatically rewrite git history (using `git rebase`) to: +1. Set the primary author of a commit to the person who wrote the majority of the lines. +2. Add `Co-authored-by` trailers to the commit message for other significant contributors. + +It handles file renames and supports a dry-run mode for previewing changes without modifying +the repository. Useful for preserving attribution when cherry-picking or squashing commits. +""" +import argparse +import os +import re +import shlex +import subprocess +import sys +import tempfile +from dataclasses import dataclass +from difflib import SequenceMatcher +from typing import Dict, List, Optional, Tuple + + +@dataclass +class AddedLine: + file_path: str + head_line_no: int # 1-based in HEAD version of the file + content: str + old_path: Optional[str] = None # path before rename (if any) + + +@dataclass +class CommitDiff: + commit_sha: str + parent_sha: Optional[str] + added_lines: List[AddedLine] + path_renames: Dict[str, str] # old_path -> new_path + + +def eprint(msg: str) -> None: + sys.stderr.write(msg + "\n") + + +def run_git(args: List[str], cwd: Optional[str] = None, check: bool = True) -> subprocess.CompletedProcess: + cmd = ["git"] + args + # Decode with UTF-8 and replace invalid sequences to be robust against binary outputs + return subprocess.run( + cmd, + cwd=cwd, + check=check, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding="utf-8", + errors="replace", + ) + + +def ensure_clean_worktree() -> None: + out = run_git(["status", "--porcelain", "--untracked-files=no"]).stdout.strip() + if out: + eprint("Working tree not clean (ignoring untracked files). Please commit/stash changes before proceeding.") + sys.exit(1) + + +def is_merge_commit(commit: str) -> bool: + out = run_git(["rev-list", "--parents", "-n", "1", commit]).stdout.strip() + parts = out.split() + return len(parts) > 2 + + +def get_commit_parent(commit: str) -> Optional[str]: + out = run_git(["rev-list", "--parents", "-n", "1", commit]).stdout.strip() + parts = out.split() + if len(parts) >= 2: + return parts[1] + return None + + +def get_commit_message(commit: str) -> str: + return run_git(["log", "-1", "--pretty=%B", commit]).stdout + + +def set_commit_author_and_message(name: str, email: str, new_message: Optional[str]) -> None: + env = os.environ.copy() + author_flag = ["--author", f"{name} <{email}>"] + if new_message is None: + run_git(["commit", "--amend", "--no-edit", *author_flag]) + else: + with tempfile.NamedTemporaryFile("w", delete=False) as tf: + tf.write(new_message) + tf.flush() + temp_path = tf.name + try: + run_git(["commit", "--amend", *author_flag, "-F", temp_path]) + finally: + try: + os.unlink(temp_path) + except OSError: + pass + + +def list_commits_in_range(commit_range: str) -> List[str]: + # Oldest first + out = run_git(["rev-list", "--reverse", commit_range]).stdout + commits = [l.strip() for l in out.splitlines() if l.strip()] + return commits + + +def parse_renames_for_commit(parent: str, commit: str) -> Dict[str, str]: + # Map old_path -> new_path + out = run_git(["diff", "--name-status", "-M", f"{parent}", f"{commit}"]).stdout + renames: Dict[str, str] = {} + for line in out.splitlines(): + # Example: R100\told\tnew + if not line: + continue + status_and_paths = line.split("\t") + if not status_and_paths: + continue + status = status_and_paths[0] + if status.startswith("R") and len(status_and_paths) == 3: + old_path = status_and_paths[1] + new_path = status_and_paths[2] + renames[old_path] = new_path + return renames + + +HUNK_HEADER_RE = re.compile(r"^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,(\d+))?\s+@@") + + +def collect_added_lines_for_commit(commit: str) -> CommitDiff: + parent = get_commit_parent(commit) + added_lines: List[AddedLine] = [] + renames = parse_renames_for_commit(parent, commit) if parent else {} + + if parent is None: + return CommitDiff(commit, parent, added_lines, renames) + + # Diff for the whole commit with zero context to isolate added lines + diff_out = run_git(["diff", "--unified=0", "--no-color", "-M", f"{parent}", f"{commit}"]).stdout + current_file: Optional[str] = None + old_file: Optional[str] = None + head_line_cursor = 0 + in_hunk = False + + for raw_line in diff_out.splitlines(): + line = raw_line.rstrip("\n") + if line.startswith("diff --git "): + current_file = None + old_file = None + in_hunk = False + continue + if line.startswith("+++ "): + # format: +++ b/path or +++ /dev/null + path = line[4:].strip() + if path.startswith("b/"): + current_file = path[2:] + else: + current_file = None + continue + if line.startswith("--- "): + path = line[4:].strip() + if path.startswith("a/"): + old_file = path[2:] + else: + old_file = None + continue + if line.startswith("Binary files "): + # Skip binary diffs + current_file = None + old_file = None + in_hunk = False + continue + m = HUNK_HEADER_RE.match(line) + if m: + start = int(m.group(1)) + count = int(m.group(2) or "1") + head_line_cursor = start + in_hunk = True + continue + if in_hunk and current_file: + if line.startswith("+") and not line.startswith("+++"): + added_lines.append(AddedLine(file_path=current_file, head_line_no=head_line_cursor, content=line[1:], old_path=old_file)) + head_line_cursor += 1 + elif line.startswith("-") and not line.startswith("---"): + # Deletions advance only original side, not head_line_cursor + continue + else: + # Context or end of hunk lines advance cursor appropriately + if not (line.startswith("+") or line.startswith("-")): + head_line_cursor += 1 + + return CommitDiff(commit, parent, added_lines, renames) + + +def git_show_blob(rev: str, path: str) -> Optional[str]: + try: + return run_git(["show", f"{rev}:{path}"]).stdout + except subprocess.CalledProcessError: + return None + + +def blame_porcelain(upstream: str, path: str) -> Optional[List[Tuple[str, str]]]: + try: + out = run_git(["blame", "-e", "-w", "--line-porcelain", upstream, "--", path]).stdout + except subprocess.CalledProcessError: + return None + authors: List[Tuple[str, str]] = [] # (name, email) + current_author: Optional[str] = None + current_email: Optional[str] = None + for line in out.splitlines(): + if not line: + continue + if line.startswith("author "): + current_author = line[len("author "):].strip() + elif line.startswith("author-mail "): + mail = line[len("author-mail "):].strip() + # format: + if mail.startswith("<") and mail.endswith(">"): + mail = mail[1:-1] + current_email = mail + elif line.startswith("\t"): + # Source line follows; we should have author and email + name = current_author or "" + email = (current_email or "").lower() + authors.append((name, email)) + current_author = None + current_email = None + return authors + + +def build_line_mapping(upstream_text: str, head_text: str) -> Dict[int, int]: + """ + Return mapping from head line number (1-based) to upstream line number (1-based) + for lines that are exactly equal according to SequenceMatcher. Fallback to + unique exact-content search for unmapped lines. + """ + upstream_lines = upstream_text.splitlines() + head_lines = head_text.splitlines() + + sm = SequenceMatcher(None, upstream_lines, head_lines, autojunk=False) + mapping: Dict[int, int] = {} + for a0, b0, size in sm.get_matching_blocks(): + for k in range(size): + upstream_idx = a0 + k # 0-based + head_idx = b0 + k + mapping[head_idx + 1] = upstream_idx + 1 + + # Fallback for unmapped lines: single-occurrence exact match search + content_to_upstream_indices: Dict[str, List[int]] = {} + for i, txt in enumerate(upstream_lines): + content_to_upstream_indices.setdefault(txt, []).append(i + 1) + + for head_idx, txt in enumerate(head_lines, start=1): + if head_idx in mapping: + continue + candidates = content_to_upstream_indices.get(txt) + if candidates and len(candidates) == 1: + mapping[head_idx] = candidates[0] + + return mapping + + +def select_authors(author_to_count: Dict[Tuple[str, str], int], threshold: float) -> Tuple[Optional[Tuple[str, str]], List[Tuple[str, str]]]: + if not author_to_count: + return None, [] + total = sum(author_to_count.values()) + # Primary: max count; tie-breaker by email lexicographically for determinism + primary = max(author_to_count.items(), key=lambda kv: (kv[1], kv[0][1]))[0] + coauthors: List[Tuple[str, str]] = [] + for author, count in author_to_count.items(): + if author == primary: + continue + if total > 0 and (count / total) >= threshold: + coauthors.append(author) + # Stable order for coauthors: by count desc, then email + coauthors.sort(key=lambda a: (-author_to_count[a], a[1])) + return primary, coauthors + + +def append_coauthors_to_message(message: str, coauthors: List[Tuple[str, str]]) -> str: + existing = set() + for line in message.splitlines(): + m = re.match(r"^Co-authored-by:\s*(.+)\s*<([^>]+)>\s*$", line.strip(), re.IGNORECASE) + if m: + name = m.group(1).strip() + email = m.group(2).strip().lower() + existing.add((name, email)) + to_add = [(n, e) for (n, e) in coauthors if (n, e) not in existing] + if not to_add: + return message + trailer_lines = [f"Co-authored-by: {name} <{email}>" for (name, email) in to_add] + if not message.endswith("\n"): + message += "\n" + if not message.endswith("\n\n"): + message += "\n" + message += "\n".join(trailer_lines) + "\n" + return message + + +def analyze_head_commit(upstream: str, threshold: float, verbose: bool = False) -> Tuple[Dict[Tuple[str, str], int], int]: + head = "HEAD" + if is_merge_commit(head): + if verbose: + eprint("Skipping merge commit at HEAD") + return {}, 0 + commit_diff = collect_added_lines_for_commit(head) + author_to_count: Dict[Tuple[str, str], int] = {} + total_counted = 0 + + # Group added lines by file path for efficiency + path_to_lines: Dict[str, List[AddedLine]] = {} + for al in commit_diff.added_lines: + path_to_lines.setdefault(al.file_path, []).append(al) + + for path, lines in path_to_lines.items(): + # Obtain HEAD version of file and upstream version + head_blob = git_show_blob(head, path) + if head_blob is None: + continue + # Prefer new path; if missing upstream, try old path if any + upstream_blob = git_show_blob(upstream, path) + if upstream_blob is None and lines and lines[0].old_path: + upstream_blob = git_show_blob(upstream, lines[0].old_path) # try old path + upstream_path_for_blame = lines[0].old_path + else: + upstream_path_for_blame = path + if upstream_blob is None: + if verbose: + eprint(f"Upstream file not found for {path}, skipping") + continue + + # Build mapping head->upstream + mapping = build_line_mapping(upstream_blob, head_blob) + blame = blame_porcelain(upstream, upstream_path_for_blame) + if blame is None: + if verbose: + eprint(f"git blame failed for {upstream}:{upstream_path_for_blame}, skipping") + continue + + for al in lines: + upstream_line = mapping.get(al.head_line_no) + if upstream_line is None: + # Try fallback: if the exact content appears uniquely in upstream + # Note: build_line_mapping already attempted unique text fallback + continue + if 1 <= upstream_line <= len(blame): + name, email = blame[upstream_line - 1] + if not name or not email: + continue + key = (name, email.lower()) + author_to_count[key] = author_to_count.get(key, 0) + 1 + total_counted += 1 + + return author_to_count, total_counted + + +def orchestrate_range_or_rebase(args: argparse.Namespace) -> None: + # If dry-run: analyze each commit and log only + if args.dry_run: + commits = list_commits_in_range(args.range) + if not commits: + eprint(f"No commits in range {args.range}") + return + for c in commits: + if is_merge_commit(c): + eprint(f"[skip] merge {c}") + continue + # Temporarily checkout the commit to analyze as HEAD? Avoid changing state. + # Instead, diff and blob reading can be done by specifying explicitly. + # For simplicity in dry-run, we cherry-pick the approach used for HEAD by temporarily using worktree-less git show/blame. + # Reuse analyze_head_commit by checking out is complex; So implement a lightweight per-commit analysis inline: + parent = get_commit_parent(c) + if not parent: + continue + commit_diff = collect_added_lines_for_commit(c) + author_to_count: Dict[Tuple[str, str], int] = {} + path_to_lines: Dict[str, List[AddedLine]] = {} + for al in commit_diff.added_lines: + path_to_lines.setdefault(al.file_path, []).append(al) + total_counted = 0 + for path, lines in path_to_lines.items(): + head_blob = git_show_blob(c, path) + if head_blob is None: + continue + upstream_blob = git_show_blob(args.upstream, path) + if upstream_blob is None and lines and lines[0].old_path: + upstream_blob = git_show_blob(args.upstream, lines[0].old_path) + upstream_path_for_blame = lines[0].old_path + else: + upstream_path_for_blame = path + if upstream_blob is None: + continue + mapping = build_line_mapping(upstream_blob, head_blob) + blame = blame_porcelain(args.upstream, upstream_path_for_blame) + if blame is None: + continue + for al in lines: + upstream_line = mapping.get(al.head_line_no) + if upstream_line is None: + continue + if 1 <= upstream_line <= len(blame): + name, email = blame[upstream_line - 1] + if not name or not email: + continue + key = (name, email.lower()) + author_to_count[key] = author_to_count.get(key, 0) + 1 + total_counted += 1 + + primary, coauthors = select_authors(author_to_count, args.threshold) + if not primary: + eprint(f"{c[:12]}: no mappable lines") + continue + total = sum(author_to_count.values()) + details = ", ".join([f"{n} <{e}>: {author_to_count[(n,e)]}" for (n,e) in sorted(author_to_count.keys(), key=lambda a: (-author_to_count[a], a[1]))]) + eprint(f"{c[:12]}: primary={primary[0]} <{primary[1]}> coauthors={len(coauthors)}/{total} [{details}]") + return + + # Modify history via rebase with --exec + ensure_clean_worktree() + + # Determine base for rebase + base = None + if ".." in args.range: + base = args.range.split("..", 1)[0] + else: + base = "trunk" + + exec_cmd_parts = [ + sys.executable, + os.path.relpath(__file__), + "--amend-current", + "-u", + args.upstream, + "-t", + str(args.threshold), + ] + if args.verbose: + exec_cmd_parts.append("--verbose") + exec_cmd = " ".join(shlex.quote(p) for p in exec_cmd_parts) + + rebase_args = ["rebase", "--reapply-cherry-picks", base, "--exec", exec_cmd] + if args.rebase_merges: + # Place flag early + rebase_args = ["rebase", "--rebase-merges", "--reapply-cherry-picks", base, "--exec", exec_cmd] + + eprint("Starting rebase to fix commit authors (this will rewrite history)...") + try: + # We do not capture stdout/stderr to allow user to interact if conflicts arise + subprocess.run(["git"] + rebase_args, check=True) + except subprocess.CalledProcessError as e: + eprint("Rebase failed. Resolve conflicts, then run 'git rebase --continue' or abort with 'git rebase --abort'.") + sys.exit(e.returncode) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Fix commit authors by blaming modified lines against an upstream branch.") + parser.add_argument("--range", "-r", default="trunk..HEAD", help="Commitish or range to operate on (default: trunk..HEAD)") + parser.add_argument("--upstream", "-u", default="gfx-rdp", help="Upstream branch to blame against (default: gfx-rdp)") + parser.add_argument("--threshold", "-t", type=float, default=0.10, help="Co-author threshold as fraction of total (default: 0.10)") + parser.add_argument("--dry-run", action="store_true", help="Analyze and log only; do not modify commits") + parser.add_argument("--verbose", action="store_true", help="Verbose logging to stderr") + parser.add_argument("--amend-current", action="store_true", help="Amend only the current commit (HEAD); used under rebase --exec") + parser.add_argument("--rebase-merges", action="store_true", help="Use git rebase --rebase-merges to preserve merges") + + args = parser.parse_args() + + if args.amend_current: + # Analyze HEAD and optionally amend + author_to_count, total_counted = analyze_head_commit(args.upstream, args.threshold, verbose=args.verbose) + if not author_to_count: + eprint("HEAD: no mappable lines; leaving author unchanged") + return + primary, coauthors = select_authors(author_to_count, args.threshold) + total = sum(author_to_count.values()) + details = ", ".join([f"{n} <{e}>: {author_to_count[(n,e)]}" for (n,e) in sorted(author_to_count.keys(), key=lambda a: (-author_to_count[a], a[1]))]) + eprint(f"HEAD: primary={primary[0]} <{primary[1]}> coauthors={len(coauthors)}/{total} [{details}]") + if args.dry_run: + return + # Update commit message with co-authors + msg = get_commit_message("HEAD") + new_msg = append_coauthors_to_message(msg, coauthors) + if primary is None: + return + set_commit_author_and_message(primary[0], primary[1], new_msg if new_msg != msg else None) + return + + # Orchestrate over range via rebase or dry-run iteration + orchestrate_range_or_rebase(args) + + +if __name__ == "__main__": + main() + + diff --git a/scripts/push-arm64-docker-image.sh b/scripts/push-arm64-docker-image.sh new file mode 100644 index 000000000..c31b46042 --- /dev/null +++ b/scripts/push-arm64-docker-image.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# +# This script is used to push the ARM64 Docker image of the toolchain to the GitHub +# Container Registry. It is required until we manage to change the CI to automatically +# build the image for ARM64 as well. + + +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR/.." + +# === Configuration === +OWNER="dragonminded" +IMAGE="libdragon" +REGISTRY="ghcr.io/${OWNER}/${IMAGE}" +TAG="latest" + +# === Utility functions === + +error() { + echo "❌ $*" >&2 + exit 1 +} + +info() { + echo "👉 $*" +} + +# === Pre-flight checks === + +# 1. Check that Docker is running +docker info >/dev/null 2>&1 || error "Docker is not running. Please start Docker first." + +# 2. Check that Buildx is available +if ! docker buildx version >/dev/null 2>&1; then + error "Docker Buildx is not available. Install a recent version of Docker Desktop or run: brew install docker-buildx" +fi + +# 3. Check authentication to ghcr.io +info "Checking authentication to ghcr.io..." +if ! docker system info | grep -q "ghcr.io"; then + info "No active login found for ghcr.io, verifying access..." +fi + +# 4. Verify actual access (test pull) +if ! docker pull "${REGISTRY}:${TAG}" >/dev/null 2>&1; then + info "Unable to pull ${REGISTRY}:${TAG} — verifying login..." + if ! docker login ghcr.io >/dev/null 2>&1; then + error "You are not authenticated to ghcr.io. Please log in with: echo \$CR_PAT | docker login ghcr.io -u --password-stdin" + fi + info "Login successful, retrying test pull..." + docker pull "${REGISTRY}:${TAG}" >/dev/null 2>&1 || info "Test pull failed (this may be normal if the image doesn't exist yet)." +fi + +info "✅ Docker authentication to ghcr.io verified." + +# === Build ARM64 image === +info "Building ARM64 image from local Dockerfile..." +docker buildx create --use --name multiarch-builder >/dev/null 2>&1 || docker buildx use multiarch-builder +docker buildx inspect --bootstrap >/dev/null + +docker buildx build \ + --platform linux/arm64 \ + -t "${REGISTRY}:${TAG}-arm64" \ + -f Dockerfile \ + --load . + +# === Fetch existing x86_64 image === +info "Pulling existing x86_64 image..." +docker pull "${REGISTRY}:${TAG}-amd64" || docker pull "${REGISTRY}:${TAG}" || { + error "Unable to find existing x86 image on GHCR (required to create multi-arch manifest)." +} + +# === Create and push multi-arch manifest === +info "Creating and pushing multi-architecture manifest..." +docker manifest create "${REGISTRY}:${TAG}" \ + --amend "${REGISTRY}:${TAG}-amd64" \ + --amend "${REGISTRY}:${TAG}-arm64" + +docker manifest push "${REGISTRY}:${TAG}" + +info "🎉 Done! Multi-platform image pushed to ${REGISTRY}:${TAG}" diff --git a/scripts/test_update_authors.py b/scripts/test_update_authors.py new file mode 100644 index 000000000..97c99989a --- /dev/null +++ b/scripts/test_update_authors.py @@ -0,0 +1,823 @@ +#!/usr/bin/env python3 + +import unittest +import tempfile +import os +import shutil +import subprocess +from pathlib import Path +from unittest.mock import patch, MagicMock +import sys + +# Add the current directory to the path so we can import update_authors +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import update_authors + + +class TestUpdateHeaders(unittest.TestCase): + def setUp(self): + """Set up test environment.""" + self.test_dir = tempfile.mkdtemp() + self.original_cwd = os.getcwd() + os.chdir(self.test_dir) + + # Initialize git repository for testing + subprocess.run(['git', 'init'], check=True, capture_output=True) + subprocess.run(['git', 'config', 'user.name', 'Test User'], check=True, capture_output=True) + subprocess.run(['git', 'config', 'user.email', 'test@example.com'], check=True, capture_output=True) + + def tearDown(self): + """Clean up test environment.""" + os.chdir(self.original_cwd) + shutil.rmtree(self.test_dir) + + def create_test_file(self, filename, content): + """Create a test file with given content and commit it to git.""" + filepath = Path(filename) + filepath.parent.mkdir(parents=True, exist_ok=True) + with open(filepath, 'w') as f: + f.write(content) + + subprocess.run(['git', 'add', str(filepath)], check=True, capture_output=True) + subprocess.run(['git', 'commit', '-m', f'Add {filename}'], check=True, capture_output=True) + return filepath + + def test_normalize_author_github_masked_email(self): + """Test GitHub masked email normalization.""" + name, email = update_authors.normalize_author("SpookyIluha", "127010686+SpookyIluha@users.noreply.github.com") + self.assertEqual(name, "SpookyIluha") + self.assertEqual(email, "https://github.com/SpookyIluha") + + def test_normalize_author_regular_email(self): + """Test regular email normalization.""" + name, email = update_authors.normalize_author("John Doe", "john@example.com") + self.assertEqual(name, "John Doe") + self.assertEqual(email, "john@example.com") + + def test_normalize_author_author_map(self): + """Test author map normalization.""" + name, email = update_authors.normalize_author("rasky", "some@email.com") + self.assertEqual(name, "Giovanni Bajo") + self.assertEqual(email, "giovannibajo@gmail.com") + + def test_is_assembly_file(self): + """Test assembly file detection.""" + self.assertTrue(update_authors.is_assembly_file(Path("test.s"))) + self.assertTrue(update_authors.is_assembly_file(Path("test.inc"))) + self.assertFalse(update_authors.is_assembly_file(Path("test.c"))) + self.assertFalse(update_authors.is_assembly_file(Path("test.h"))) + + def test_is_source_file(self): + """Test source file detection.""" + self.assertTrue(update_authors.is_source_file(Path("test.c"))) + self.assertTrue(update_authors.is_source_file(Path("test.cpp"))) + self.assertTrue(update_authors.is_source_file(Path("test.h"))) + self.assertTrue(update_authors.is_source_file(Path("test.hpp"))) + self.assertTrue(update_authors.is_source_file(Path("test.s"))) + self.assertFalse(update_authors.is_source_file(Path("test.txt"))) + self.assertFalse(update_authors.is_source_file(Path("test.md"))) + + def test_is_skipped_path(self): + """Test path skipping logic.""" + # Test with a file that should be skipped + with patch.object(update_authors, 'SKIP_PATHS', ['vendor*', '*.md']): + # Mock os.path.isdir to return False for our test paths + with patch('os.path.isdir', return_value=False): + self.assertTrue(update_authors.is_skipped_path('vendor/something.c')) + self.assertTrue(update_authors.is_skipped_path('README.md')) + self.assertFalse(update_authors.is_skipped_path('src/main.c')) + + def test_extract_existing_brief(self): + """Test extraction of existing @brief tags.""" + header_content = """ +/** + * @file test.c + * @brief This is a test file + * @author Test User + */ +""" + brief = update_authors.extract_existing_brief(header_content) + self.assertEqual(brief, "This is a test file") + + # Test with no @brief + header_content = """ +/** + * @file test.c + * @author Test User + */ +""" + brief = update_authors.extract_existing_brief(header_content) + self.assertIsNone(brief) + + def test_find_file_level_header(self): + """Test finding existing file-level headers.""" + content = """ +/** + * @file test.c + * @author Test User + */ +int main() { + return 0; +} +""" + start, end, header = update_authors.find_file_level_header(content) + self.assertEqual(start, 1) + self.assertEqual(end, 5) + self.assertIsNotNone(header) + if header: + self.assertIn("@file test.c", header) + self.assertIn("@author Test User", header) + + # Test with no header + content = """ +int main() { + return 0; +} +""" + start, end, header = update_authors.find_file_level_header(content) + self.assertIsNone(start) + self.assertIsNone(end) + self.assertIsNone(header) + + def test_create_doxygen_header(self): + """Test Doxygen header creation.""" + authors = [("Test User", "test@example.com"), ("Another User", "another@example.com")] + header = update_authors.create_doxygen_header("test.c", authors) + + self.assertIn("@file test.c", header) + self.assertIn("@author Test User ", header) + self.assertIn("@author Another User ", header) + self.assertTrue(header.endswith(" */\n")) + + def test_create_doxygen_header_with_brief(self): + """Test Doxygen header creation with existing brief.""" + authors = [("Test User", "test@example.com")] + header = update_authors.create_doxygen_header("test.c", authors, "This is a test file") + + self.assertIn("@file test.c", header) + self.assertIn("@author Test User ", header) + self.assertIn("@brief This is a test file", header) + + def test_create_assembly_header(self): + """Test assembly header creation.""" + authors = [("Test User", "test@example.com"), ("Another User", "another@example.com")] + header = update_authors.create_assembly_header("test.s", authors) + + self.assertIn("############################################################", header) + self.assertIn("# test.s", header) + self.assertIn("# Authors: Test User ", header) + self.assertIn("# Another User ", header) + + def test_update_existing_header(self): + """Test updating existing headers.""" + header_content = """ +/** + * @file oldname.c + * @author Old User + * @brief Old description + * @version 1.0 + */ +""" + authors = [("New User", "new@example.com")] + updated = update_authors.update_existing_header(header_content, "newname.c", authors) + + self.assertIn("@file newname.c", updated) + self.assertIn("@author New User ", updated) + self.assertIn("@brief Old description", updated) + self.assertIn("@version 1.0", updated) + self.assertNotIn("@author Old User", updated) + + def test_process_file_no_existing_header_c(self): + """Test processing C file with no existing header.""" + content = """ +int main() { + return 0; +} +""" + filepath = self.create_test_file("test.c", content) + def mock_check_output(args, **kwargs): + if 'log' in args: + return "Test User " + else: + return "author Test User\nauthor-mail " + with patch('subprocess.check_output', side_effect=mock_check_output): + result = update_authors.process_file(filepath) + self.assertTrue(result) + with open(filepath, 'r') as f: + new_content = f.read() + self.assertIn("/**", new_content) + self.assertIn("@file test.c", new_content) + self.assertIn("@author Test User ", new_content) + + def test_process_file_existing_header_c(self): + """Test processing C file with existing header.""" + header_content = """ +/** + * @file test.c + * @author Old User + * @brief Old description + */ +""" + content = header_content + "int main() {\n return 0;\n}\n" + filepath = self.create_test_file("test.c", content) + def mock_check_output(args, **kwargs): + if 'log' in args: + return "New User " + else: + return "author New User\nauthor-mail " + with patch('subprocess.check_output', side_effect=mock_check_output): + result = update_authors.process_file(filepath) + self.assertTrue(result) + with open(filepath, 'r') as f: + new_content = f.read() + self.assertIn("@file test.c", new_content) + self.assertIn("@author New User ", new_content) + self.assertIn("@brief Old description", new_content) + self.assertNotIn("@author Old User", new_content) + + def test_process_file_assembly_no_header(self): + """Test processing assembly file with no existing header.""" + content = """ + .text + .globl main +main: + jr $ra +""" + filepath = self.create_test_file("test.s", content) + def mock_check_output(args, **kwargs): + if 'log' in args: + return "Test User " + else: + # Each line in the file gets a pair + return "author Test User\nauthor-mail \n" * 5 + with patch('subprocess.check_output', side_effect=mock_check_output): + result = update_authors.process_file(filepath) + self.assertTrue(result) + with open(filepath, 'r') as f: + new_content = f.read() + self.assertIn("# Authors: Test User ", new_content) + + def test_process_file_assembly_existing_header(self): + """Test processing assembly file with existing header.""" + content = """ +############################################################ +# Bla bla existing text 1 +# Bla bla existing text 2 +# Bla bla existing text 3 +# +# +# +# Authors: Old User +############################################################ + + .text + .globl main +main: + jr $ra +""" + filepath = self.create_test_file("test.s", content) + def mock_check_output(args, **kwargs): + if 'log' in args: + return "New User " + else: + return "author New User\nauthor-mail \n" * 5 + with patch('subprocess.check_output', side_effect=mock_check_output): + result = update_authors.process_file(filepath) + self.assertTrue(result) + with open(filepath, 'r') as f: + new_content = f.read() + self.assertIn("# Authors: New User ", new_content) + self.assertNotIn("# Authors: Old User", new_content) + + def test_process_file_assembly_existing_header_no_authors(self): + """Test processing assembly file with existing header but no authors section.""" + content = """ +############################################################ +# test.s +# +# Some other comment +# +# +############################################################ + + .text + .globl main +main: + jr $ra +""" + filepath = self.create_test_file("test.s", content) + def mock_check_output(args, **kwargs): + if 'log' in args: + return "Test User " + else: + return "author Test User\nauthor-mail \n" * 5 + with patch('subprocess.check_output', side_effect=mock_check_output): + result = update_authors.process_file(filepath) + self.assertTrue(result) + with open(filepath, 'r') as f: + new_content = f.read() + self.assertIn("# Authors: Test User ", new_content) + + def test_process_file_assembly_multiple_sharps_blocks(self): + """Test that only the first sharps block is updated in assembly files with multiple blocks.""" + content = """ +############################################################ +# test.s +# +# +# +# Authors: Old User +############################################################ + + .text + .globl main +main: + jr $ra + +############################################################ +# This is a second block +# Authors: Should Stay +# +############################################################ +""" + filepath = self.create_test_file("test.s", content) + def mock_check_output(args, **kwargs): + if 'log' in args: + return "New User " + else: + return "author New User\nauthor-mail \n" * 5 + with patch('subprocess.check_output', side_effect=mock_check_output): + result = update_authors.process_file(filepath) + self.assertTrue(result) + with open(filepath, 'r') as f: + new_content = f.read() + self.assertIn("# Authors: New User ", new_content) + self.assertNotIn("# Authors: Old User ", new_content) + self.assertIn("# Authors: Should Stay ", new_content) + self.assertIn("# This is a second block", new_content) + + def test_process_file_assembly_tab_indented_sharps(self): + """Test that author lines in assembly headers preserve tab indentation of the sharp block.""" + content = """ ############################################################ + # + # Some description + # + # + ############################################################ + +.text +main: + jr $ra +""" + filepath = self.create_test_file("test.s", content) + def mock_check_output(args, **kwargs): + if 'log' in args: + return "New User " + else: + return "author New User\nauthor-mail \n" * 5 + with patch('subprocess.check_output', side_effect=mock_check_output): + result = update_authors.process_file(filepath) + self.assertTrue(result) + with open(filepath, 'r') as f: + new_content = f.read() + self.assertIn("\t# Authors: New User ", new_content) + + def test_process_file_original_obj_map_c(self): + """Test processing the original obj_map.c file content.""" + content = """/** + * This is a very simple hash map that uses open adressing (linear probing). + * The hash function is the identity for now, since it uses integer keys. + */ + +#include \"obj_map.h\" + +void obj_map_new(obj_map_t *map) +{ + // Implementation here +} +""" + filepath = self.create_test_file("obj_map.c", content) + def mock_check_output(args, **kwargs): + if 'log' in args: + return "Test User " + else: + return "author Test User\nauthor-mail \n" * 10 + with patch('subprocess.check_output', side_effect=mock_check_output): + result = update_authors.process_file(filepath) + self.assertTrue(result) + with open(filepath, 'r') as f: + new_content = f.read() + lines = new_content.split('\n') + self.assertEqual(lines[0].strip(), "/**") + self.assertEqual(lines[1].lstrip(), "* @file obj_map.c") + self.assertEqual(lines[2].lstrip(), "* @author Test User ") + self.assertEqual(lines[3].lstrip(), "*") + self.assertIn("This is a very simple hash map", lines[4]) + self.assertIn("The hash function is the identity for now, since it uses integer keys", new_content) + self.assertIn("#include \"obj_map.h\"", new_content) + self.assertIn("void obj_map_new(obj_map_t *map)", new_content) + + def test_process_file_skip_vendored(self): + """Test that vendored files are skipped.""" + content = "int main() { return 0; }" + filepath = self.create_test_file("vendor/test.c", content) + + with patch.object(update_authors, 'SKIP_PATHS', ['vendor*']): + result = update_authors.process_file(filepath) + self.assertFalse(result) + + def test_process_file_skip_untracked(self): + """Test that untracked files are skipped.""" + filepath = Path("untracked.c") + with open(filepath, 'w') as f: + f.write("int main() { return 0; }") + + result = update_authors.process_file(filepath) + self.assertFalse(result) + + def test_get_authors_mock(self): + """Test get_authors with mocked git commands.""" + filepath = Path("test.c") + + # Mock git log output (first committer) + mock_log_output = "Another User \nTest User " + # Mock git blame output + mock_blame_output = """ +author Test User +author-mail +author Test User +author-mail +author Another User +author-mail +""" + def mock_check_output(args, **kwargs): + if 'log' in args: + return mock_log_output + else: + return mock_blame_output + with patch('subprocess.check_output', side_effect=mock_check_output): + authors = update_authors.get_authors(filepath) + self.assertEqual(len(authors), 2) + # Test User is the first committer + self.assertEqual(authors[0][0], "Test User") + self.assertEqual(authors[0][1], "test@example.com") + # Another User should be second (alphabetically, but only one left) + self.assertEqual(authors[1][0], "Another User") + self.assertEqual(authors[1][1], "another@example.com") + + def test_get_authors_with_github_masked_emails(self): + """Test get_authors with GitHub masked emails.""" + filepath = Path("test.c") + + # Mock git log output + mock_log_output = "SpookyIluha <127010686+SpookyIluha@users.noreply.github.com>" + + # Mock git blame output + mock_blame_output = """ +author SpookyIluha +author-mail <127010686+SpookyIluha@users.noreply.github.com> +""" + + def mock_check_output(args, **kwargs): + if 'log' in args: + return mock_log_output + else: + return mock_blame_output + + with patch('subprocess.check_output', side_effect=mock_check_output): + authors = update_authors.get_authors(filepath) + # Should return normalized authors with GitHub URLs + self.assertEqual(len(authors), 1) + self.assertEqual(authors[0][0], "SpookyIluha") + self.assertEqual(authors[0][1], "https://github.com/SpookyIluha") + + def test_edge_case_empty_file(self): + """Test processing an empty file.""" + filepath = self.create_test_file("empty.c", "") + + with patch.object(update_authors, 'get_authors') as mock_get_authors: + mock_get_authors.return_value = [("Test User", "test@example.com")] + + result = update_authors.process_file(filepath) + self.assertTrue(result) + + with open(filepath, 'r') as f: + new_content = f.read() + + self.assertIn("/**", new_content) + self.assertIn("@file empty.c", new_content) + + def test_edge_case_file_with_only_comments(self): + """Test processing a file with only comments.""" + content = """ +// This is a comment +/* Another comment */ +""" + filepath = self.create_test_file("comments.c", content) + + with patch.object(update_authors, 'get_authors') as mock_get_authors: + mock_get_authors.return_value = [("Test User", "test@example.com")] + + result = update_authors.process_file(filepath) + self.assertTrue(result) + + with open(filepath, 'r') as f: + new_content = f.read() + + self.assertIn("/**", new_content) + self.assertIn("@file comments.c", new_content) + self.assertIn("// This is a comment", new_content) + + def test_edge_case_malformed_header(self): + """Test processing a file with malformed header.""" + content = """ +/** + * @file test.c + * @author Test User + * This is malformed (missing @) +int main() { + return 0; +} +""" + filepath = self.create_test_file("malformed.c", content) + def mock_check_output(args, **kwargs): + if 'log' in args: + return "New User " + else: + return "author New User\nauthor-mail " + with patch('subprocess.check_output', side_effect=mock_check_output): + result = update_authors.process_file(filepath) + self.assertTrue(result) + with open(filepath, 'r') as f: + new_content = f.read() + # Should still process the file and add proper header + self.assertIn("@file malformed.c", new_content) + self.assertIn("@author New User ", new_content) + + def test_get_authors_alphabetical_ordering(self): + """Test that authors are sorted alphabetically.""" + def mock_check_output(args, **kwargs): + if 'blame' in args: + return """author Test User +author-mail +author Another User +author-mail +author Test User +author-mail """ + elif 'log' in args: + return "Test User " + return "" + + with patch('subprocess.check_output', side_effect=mock_check_output): + authors = update_authors.get_authors_for_file(Path("test.c")) + # Should be sorted alphabetically, with first committer first + self.assertEqual(authors[0][0], "Test User") # First committer + self.assertEqual(authors[1][0], "Another User") # Alphabetically first + + def test_idempotency_c_file(self): + """Test that processing a C file multiple times doesn't change the content.""" + # Create a test C file with existing header + test_content = '''/** + * @file test.c + * @author Test Author + * + * This is a test file. + */ + +#include + +int main() { + return 0; +} +''' + + filepath = self.create_test_file("test.c", test_content) + + def mock_check_output(args, **kwargs): + if 'blame' in args: + return """author Test Author +author-mail +author Test Author +author-mail """ + elif 'log' in args: + return "Test Author " + return "" + + with patch('subprocess.check_output', side_effect=mock_check_output): + # Process the file once + update_authors.process_file(filepath) + content1 = filepath.read_text() + + # Process the file again + update_authors.process_file(filepath) + content2 = filepath.read_text() + + # Content should be identical + self.assertEqual(content1, content2, "C file content changed after second processing") + + def test_idempotency_assembly_file(self): + """Test that processing an assembly file multiple times doesn't change the content.""" + # Create a test assembly file with existing header + test_content = '''############################################################ +# test.S +# +# This is a test assembly file. +# +# Authors: Test Author +############################################################ + + .text + .globl main +main: + jr ra + nop +''' + + filepath = self.create_test_file("test.S", test_content) + + def mock_check_output(args, **kwargs): + if 'blame' in args: + return """author Test Author +author-mail +author Test Author +author-mail """ + elif 'log' in args: + return "Test Author " + return "" + + with patch('subprocess.check_output', side_effect=mock_check_output): + # Process the file once + update_authors.process_file(filepath) + content1 = filepath.read_text() + + # Process the file again + update_authors.process_file(filepath) + content2 = filepath.read_text() + + # Content should be identical + self.assertEqual(content1, content2, "Assembly file content changed after second processing") + + def test_idempotency_c_file_with_multiple_authors(self): + """Test idempotency with multiple authors.""" + # Create a test C file with existing header and multiple authors + test_content = '''/** + * @file test.c + * @author Author One + * @author Author Two + * + * This is a test file with multiple authors. + */ + +#include + +int main() { + return 0; +} +''' + + filepath = self.create_test_file("test.c", test_content) + + def mock_check_output(args, **kwargs): + if 'blame' in args: + return """author Author One +author-mail +author Author Two +author-mail +author Author One +author-mail """ + elif 'log' in args: + return "Author One " + return "" + + with patch('subprocess.check_output', side_effect=mock_check_output): + # Process the file once + update_authors.process_file(filepath) + content1 = filepath.read_text() + + # Process the file again + update_authors.process_file(filepath) + content2 = filepath.read_text() + + # Content should be identical + self.assertEqual(content1, content2, "C file with multiple authors changed after second processing") + + def test_idempotency_assembly_file_with_description(self): + """Test idempotency for assembly file with description before authors.""" + # Create a test assembly file with description before authors + test_content = '''############################################################ +# test.S +# +# This is a test assembly file with description. +# It has multiple lines of description. +# +# Authors: Test Author +############################################################ + + .text + .globl main +main: + jr ra + nop +''' + + filepath = self.create_test_file("test.S", test_content) + + def mock_check_output(args, **kwargs): + if 'blame' in args: + return """author Test Author +author-mail +author Test Author +author-mail """ + elif 'log' in args: + return "Test Author " + return "" + + with patch('subprocess.check_output', side_effect=mock_check_output): + # Process the file once + update_authors.process_file(filepath) + content1 = filepath.read_text() + + # Process the file again + update_authors.process_file(filepath) + content2 = filepath.read_text() + + # Content should be identical + self.assertEqual(content1, content2, "Assembly file with description changed after second processing") + + def test_assembly_header_single_blank_line_before_authors(self): + """Test that there is always exactly one blank line before authors in assembly headers.""" + # Case 1: Only blank lines before authors + test_content_blank = '''############################################################ +# test.S +# +# +# +# Authors: Test Author +############################################################ + + .text + .globl main +main: + jr ra + nop +''' + filepath_blank = self.create_test_file("test.S", test_content_blank) + def mock_check_output(args, **kwargs): + if 'blame' in args: + return "author Test Author\nauthor-mail " + elif 'log' in args: + return "Test Author " + return "" + with patch('subprocess.check_output', side_effect=mock_check_output): + update_authors.process_file(filepath_blank) + content = filepath_blank.read_text() + lines = content.splitlines() + # Find the authors line + for idx, line in enumerate(lines): + if 'Authors:' in line: + # There must be exactly one blank line (i.e., a line with only #) before authors + self.assertTrue(idx > 0) + self.assertEqual(lines[idx-1].strip(), '#') + # And the line before that must not be blank + if idx > 1: + self.assertNotEqual(lines[idx-2].strip(), '#') + break + else: + self.fail('Authors line not found') + + # Case 2: Descriptive content and multiple blank lines before authors + test_content_desc = '''############################################################ +# test.S +# +# This is a test assembly file. +# +# +# Authors: Test Author +############################################################ + + .text + .globl main +main: + jr ra + nop +''' + filepath_desc = self.create_test_file("test.S", test_content_desc) + with patch('subprocess.check_output', side_effect=mock_check_output): + update_authors.process_file(filepath_desc) + content = filepath_desc.read_text() + lines = content.splitlines() + for idx, line in enumerate(lines): + if 'Authors:' in line: + # There must be exactly one blank line (i.e., a line with only #) before authors + self.assertTrue(idx > 0) + self.assertEqual(lines[idx-1].strip(), '#') + # And the line before that must not be blank + if idx > 1: + self.assertNotEqual(lines[idx-2].strip(), '#') + break + else: + self.fail('Authors line not found') + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/scripts/update_authors.py b/scripts/update_authors.py new file mode 100644 index 000000000..325600c3d --- /dev/null +++ b/scripts/update_authors.py @@ -0,0 +1,641 @@ +#!/usr/bin/env python3 +""" +Script to add or update Doxygen author headers in libdragon C/C++/header files. + +This script is meant to be run on the preview branch, where the whole history +of file changes can be inspected. Backport to trunks are always squashed so +most of the history is lost there, and this script wouldn't be able to reconstruct +a list of major authors of each file. +""" + +import os +import re +import sys +import subprocess +import multiprocessing +from pathlib import Path +import fnmatch +from collections import Counter + +# List of files or directories to skip (vendored/third-party) +SKIP_PATHS = [ + 'src/fatfs', + 'src/audio/opus', + 'src/audio/libxm', + 'src/video/pl_mpeg', + 'src/video/h264_decoder', + 'src/libcart', + 'src/usb.c', + 'src/rdpq/rdpq_font_builtin.c', + 'include/usb.h', + 'tools/audioconv64/libsamplerate', + 'tools/audioconv64/vadpcm', + 'tools/audioconv64/lzh5_compress.*', + 'tools/audioconv64/dr_*', + 'tools/common/apultra', + 'tools/common/incbin.h', + 'tools/common/lodepng.*', + 'tools/common/lz4', + 'tools/common/shrinkler', + 'tools/common/mips_elf.h', + 'tools/common/stb_ds.h', + 'tools/common/subprocess.h', + 'tools/mkfont/freetype', + 'tools/mkfont/phf.*', + 'tools/mkfont/rect_pack', + 'tools/mkfont/crc64.c', + 'tools/mkmaterial/json.hpp', + 'tools/mkmodel/cgltf.h', + 'tools/mksprite/exoquant.*', + + 'tools/ipl3.h', + # Add more as needed +] + +# Author mapping to normalize names and emails +AUTHOR_MAP = { + # Name variations -> canonical (name, email) + 'Giovanni Bajo': ('Giovanni Bajo', 'giovannibajo@gmail.com'), + 'rasky': ('Giovanni Bajo', 'giovannibajo@gmail.com'), + 'Shaun Taylor': ('Jennifer Taylor', 'dragonminded@dragonminded.com'), + # Add more mappings as needed +} + +def normalize_author(name, email): + """Normalize author name and email using the author map.""" + # Check if the name is in our mapping + if name in AUTHOR_MAP: + return AUTHOR_MAP[name] + + # Check if the email is a GitHub masked email (e.g., 127010686+SpookyIluha@users.noreply.github.com) + github_match = re.match(r'\d+\+([^@]+)@users\.noreply\.github\.com', email) + if github_match: + username = github_match.group(1) + # Return the name with a GitHub user page link instead of the masked email + return (name, f"https://github.com/{username}") + + # Check if the email matches any known author + for canonical_name, canonical_email in AUTHOR_MAP.values(): + if email == canonical_email: + return (canonical_name, canonical_email) + + # Return as-is if no mapping found + return (name, email) + +def is_git_tracked(filepath): + """Check if a file is tracked by git.""" + try: + result = subprocess.run([ + 'git', 'ls-files', '--error-unmatch', str(filepath) + ], capture_output=True, text=True) + return result.returncode == 0 + except Exception: + return False + +def is_skipped_path(path): + """Return True if the path matches any skip pattern.""" + for skip in SKIP_PATHS: + # Support glob patterns + if fnmatch.fnmatch(path, skip): + return True + # Support directory skipping + if os.path.isdir(skip) and os.path.commonpath([os.path.abspath(path), os.path.abspath(skip)]) == os.path.abspath(skip): + return True + # Support direct file match + if os.path.abspath(path) == os.path.abspath(skip): + return True + return False + +def is_assembly_file(filepath): + """Check if file is an assembly file based on extension.""" + return filepath.suffix.lower() in ['.s', '.inc'] + +def get_authors_for_file(filepath): + """Get authors for a single file using git blame and git log.""" + try: + # Get first committer (chronologically) - use normal order and take the last line + log = subprocess.check_output([ + 'git', 'log', '--follow', '--format=%an <%ae>', str(filepath) + ], encoding='utf-8', errors='replace') + log_lines = log.splitlines() + first_committer_line = log_lines[-1] if log_lines else None + if first_committer_line and '<' in first_committer_line and first_committer_line.endswith('>'): + name, email = first_committer_line.rsplit(' <', 1) + email = email.rstrip('>') + first_author = normalize_author(name.strip(), email.strip()) + else: + first_author = None + + # Get line-by-line blame for the file + blame = subprocess.check_output([ + 'git', 'blame', '--line-porcelain', str(filepath) + ], encoding='utf-8', errors='replace') + if isinstance(blame, bytes): + blame = blame.decode('utf-8', errors='replace') + authors = [] + names = [] + for line in blame.splitlines(): + if line.startswith('author-mail '): + authors.append(line[12:].strip()) + elif line.startswith('author '): + names.append(line[7:].strip()) + author_pairs = list(zip(names, authors)) + # Filter out uncommitted entries + filtered_pairs = [] + for name, email in author_pairs: + email = email.strip('<>') + if name == "Not Committed Yet" and email == "not.committed.yet": + continue + filtered_pairs.append((name, email)) + # Normalize + normalized_pairs = [normalize_author(name, email) for name, email in filtered_pairs] + counter = Counter(normalized_pairs) + total = sum(counter.values()) + # Apply 20% rule + major_authors = [a for a, c in counter.items() if c / total >= 0.2] + # Always include the first committer at the top + authors_list = [] + if first_author: + authors_list.append(first_author) + # Add other major authors, sorted alphabetically, excluding first_author if present + other_authors = [a for a in major_authors if a != first_author] + other_authors.sort(key=lambda x: x[0].lower()) + authors_list.extend(other_authors) + # Ensure unique authors (should already be unique, but just in case) + seen = set() + unique_authors = [] + for author in authors_list: + if author not in seen: + seen.add(author) + unique_authors.append(author) + return unique_authors + except Exception: + return [] + +def extract_existing_brief(header_content): + """Extract the first @brief tag from a file-level comment block.""" + lines = header_content.split('\n') + for line in lines: + stripped = line.strip() + if '@brief' in stripped: + brief_match = re.search(r'@brief\s+(.+)', stripped) + if brief_match: + return brief_match.group(1).strip() + return None + +def find_file_level_header(content): + """Find existing file-level Doxygen header block.""" + lines = content.split('\n') + i = 0 + + # Skip empty lines at the beginning + while i < len(lines) and lines[i].strip() == '': + i += 1 + + # Check if the first non-empty line starts a Doxygen comment block + if i < len(lines) and (lines[i].strip().startswith('/**') or lines[i].strip().startswith('/*!')): + start_line = i + i += 1 + + # Find the end of the comment block + while i < len(lines): + stripped = lines[i].strip() + if stripped == '*/': + return start_line, i + 1, '\n'.join(lines[start_line:i+1]) + i += 1 + + return None, None, None + +def create_assembly_header(filename, authors): + """Create assembly header with 60 sharps.""" + header_lines = [ + "#" * 60, + f"# {filename}", + "#", + ] + + # Add authors on multiple lines + for i, author_info in enumerate(authors): + name, email = author_info + if i == 0: + header_lines.append(f"# Authors: {name} <{email}>") + else: + header_lines.append(f"# {name} <{email}>") + + header_lines.extend([ + "#", + "#" * 60, + "" + ]) + return '\n'.join(header_lines) + +def create_doxygen_header(filename, authors, existing_brief=None): + """Create Doxygen header for C/C++ files.""" + header_lines = [ + "/**", + f" * @file {filename}", + ] + for i, author_info in enumerate(authors): + name, email = author_info + header_lines.append(f" * @author {name} <{email}>") + if existing_brief: + header_lines.append(f" * @brief {existing_brief}") + header_lines.append(" */\n") + return '\n'.join(header_lines) + +def update_existing_header(header_content, filename, authors): + """Update @file and @author tags in an existing header while preserving all other content.""" + lines = header_content.split('\n') + updated_lines = [] + + # Start with the opening comment line + updated_lines.append('/**') + + # Add @file tag at the beginning + updated_lines.append(f" * @file {filename}") + + # Add @author tags + for author_info in authors: + name, email = author_info + updated_lines.append(f" * @author {name} <{email}>") + + # Extract and preserve existing descriptive content (skip @file, @author tags, but keep @brief) + descriptive_lines = [] + for line in lines: + stripped = line.strip() + if (stripped.startswith('* @file') or + stripped.startswith('* @author') or + stripped == '*/' or + stripped == '/**' or + stripped.startswith('/**')): + # Skip existing @file/@author tags and comment markers + continue + elif stripped.startswith('*'): + # Keep descriptive content (lines starting with * including empty lines) + descriptive_lines.append(line) + + # Add a blank line only if there are descriptive lines and the first one is not empty + if descriptive_lines: + first_desc = descriptive_lines[0].strip() + if first_desc.startswith('*') and not first_desc.startswith('* @') and first_desc != '*': + updated_lines.append(' *') + + # Add the descriptive content + updated_lines.extend(descriptive_lines) + + # Add the closing comment line + updated_lines.append(" */") + + return '\n'.join(updated_lines) + +def process_file(filepath, authors=None): + """Process a single file to add/update Doxygen headers.""" + try: + # Skip vendored files and directories + if is_skipped_path(str(filepath)): + return False + + # Skip untracked git files + if not is_git_tracked(filepath): + return False + + # Read file content + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + except UnicodeDecodeError: + print(f" Skipping {filepath} (encoding issue)") + return False + + # Get authors if not provided + if authors is None: + authors = get_authors_for_file(filepath) + + # Handle assembly files + if is_assembly_file(filepath): + lines = content.split('\n') + # Skip empty lines at the beginning + i = 0 + while i < len(lines) and lines[i].strip() == '': + i += 1 + + # Detect top-level sharps block (from first sharps line to next sharps line) + if i < len(lines) and lines[i].lstrip().startswith('#') and len(lines[i].lstrip()) >= 10 and set(lines[i].lstrip()) == {'#'}: + # Get indentation from the opening sharps line + indent = lines[i][:len(lines[i]) - len(lines[i].lstrip())] + # Find the closing sharps line + closing_idx = None + for j in range(i+1, len(lines)): + l = lines[j].lstrip() + if l.startswith('#') and len(l) >= 10 and set(l) == {'#'}: + closing_idx = j + break + if closing_idx is not None: + # Build new block: preserve all lines except author lines, insert new authors before closing sharps + block_lines = [] + # Add any empty lines at the beginning + block_lines.extend(lines[:i]) + # Opening sharps line + block_lines.append(lines[i]) + # All lines up to (but not including) closing sharps + pre_author_lines = [] + has_content_before_authors = False + for j in range(i+1, closing_idx): + l = lines[j].lstrip() + if l.startswith('# Authors:') or (l.startswith('#') and ' <' in l and '@' in l and not l.startswith('# Bla bla')): + continue # skip old author lines + # Check for actual description (not empty, not filename, not just '#') + if l.startswith('#'): + content = l[1:].strip() + if content and content != filepath.name: + has_content_before_authors = True + pre_author_lines.append(lines[j]) + # Remove trailing blank lines before authors + while pre_author_lines and pre_author_lines[-1].lstrip() == '#': + pre_author_lines.pop() + block_lines.extend(pre_author_lines) + # Always add exactly one blank line before authors + block_lines.append(f'{indent}#') + + for k, author_info in enumerate(authors): + name, email = author_info + if k == 0: + block_lines.append(f"{indent}# Authors: {name} <{email}>") + else: + block_lines.append(f"{indent}# {name} <{email}>") + # Add the closing sharps line + block_lines.append(lines[closing_idx]) + # Add the rest of the file + new_lines = block_lines + lines[closing_idx+1:] + new_content = '\n'.join(new_lines) + else: + # No closing sharps, fallback to previous logic + new_header = create_assembly_header(filepath.name, authors) + new_content = new_header + content + else: + # No top-level sharps block, add full header + new_header = create_assembly_header(filepath.name, authors) + new_content = new_header + content + with open(filepath, 'w', encoding='utf-8') as f: + f.write(new_content) + return True + + # Handle C/C++ files + # Check if there's already a file-level header + header_start, header_end, existing_header = find_file_level_header(content) + + # Update existing header or create new one + if header_start is not None and header_end is not None: + # Update existing header while preserving all content + updated_header = update_existing_header(existing_header, filepath.name, authors) + lines = content.split('\n') + new_content = updated_header + '\n' + '\n'.join(lines[header_end:]) + else: + # Extract existing brief if available + existing_brief = None + if existing_header: + existing_brief = extract_existing_brief(existing_header) + + # Create new header + new_header = create_doxygen_header(filepath.name, authors, existing_brief) + new_content = new_header + content + + # Write back + with open(filepath, 'w', encoding='utf-8') as f: + f.write(new_content) + + return True + +def process_file_wrapper(filepath): + """Wrapper function for multiprocessing that returns success status.""" + try: + return process_file(filepath) + except Exception as e: + print(f"Error processing {filepath}: {e}") + return False + +def main(): + """Main function to process files or directories.""" + if len(sys.argv) < 2: + print("Usage: python3 update_authors.py [ ...]") + print(" python3 update_authors.py # Process all C/C++/header/assembly files in the repo") + sys.exit(1) + + # Get list of files to process + files_to_process = [] + + if len(sys.argv) == 2 and sys.argv[1] == '.': + # Process all C/C++/header/assembly files in the repo + for root, dirs, files in os.walk('.'): + # Skip .git directory + if '.git' in dirs: + dirs.remove('.git') + # Skip build directories + if 'build' in dirs: + dirs.remove('build') + if 'bin' in dirs: + dirs.remove('bin') + + for file in files: + filepath = Path(root) / file + if is_source_file(filepath): + files_to_process.append(filepath) + else: + # Process specified files/directories + for arg in sys.argv[1:]: + path = Path(arg) + if path.is_file(): + if is_source_file(path): + files_to_process.append(path) + else: + print(f"Skipping {path} (not a source file)") + elif path.is_dir(): + # Recursively find all source files in directory + for root, dirs, files in os.walk(path): + # Skip .git directory + if '.git' in dirs: + dirs.remove('.git') + # Skip build directories + if 'build' in dirs: + dirs.remove('build') + if 'bin' in dirs: + dirs.remove('bin') + + for file in files: + filepath = Path(root) / file + if is_source_file(filepath): + files_to_process.append(filepath) + else: + print(f"Warning: {path} does not exist") + + if not files_to_process: + print("No source files found to process") + return + + print(f"Processing {len(files_to_process)} files...") + + # Filter out untracked files before getting authors + tracked_files = [fp for fp in files_to_process if is_git_tracked(fp)] + untracked_count = len(files_to_process) - len(tracked_files) + + if untracked_count > 0: + print(f"Skipping {untracked_count} untracked files...") + + # Always process all tracked files (do not skip files with existing headers) + print("Process files...") + + # Use multiprocessing to process files in parallel + num_processes = max(1, min(multiprocessing.cpu_count(), len(tracked_files))) + + with multiprocessing.Pool(processes=num_processes) as pool: + # Use map to process files in parallel + results = pool.map(process_file_wrapper, tracked_files) + + # Print summary + processed = sum(1 for result in results if result) + print(f"Updated {processed} files") + +def is_source_file(filepath): + """Check if file is a source file that should be processed.""" + # C/C++/header files + if filepath.suffix.lower() in ['.c', '.cpp', '.h', '.hpp']: + return True + # Assembly files + if filepath.suffix.lower() in ['.s', '.inc']: + return True + return False + +def get_authors(filepath): + """Wrapper for testability: get authors for a single file.""" + return get_authors_for_file(filepath) + +def has_proper_header(filepath): + """Check if a file already has a proper Doxygen or assembly header.""" + try: + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + except (UnicodeDecodeError, FileNotFoundError): + return False + + lines = content.split('\n') + i = 0 + + # Skip empty lines at the beginning + while i < len(lines) and lines[i].strip() == '': + i += 1 + + if i >= len(lines): + return False + + # Check for Doxygen header + if lines[i].strip().startswith('/**') or lines[i].strip().startswith('/*!'): + # Look for @file and @author tags + has_file = False + has_author = False + for line in lines[i:]: + stripped = line.strip() + if stripped == '*/': + break + if '@file' in stripped: + has_file = True + if '@author' in stripped: + has_author = True + return has_file and has_author + + # Check for assembly header + if is_assembly_file(filepath): + if lines[i].lstrip().startswith('#') and len(lines[i].lstrip()) >= 10 and set(lines[i].lstrip()) == {'#'}: + # Look for Authors line in the first sharps block + for j in range(i+1, len(lines)): + l = lines[j].lstrip() + if l.startswith('#') and len(l) >= 10 and set(l) == {'#'}: + break # End of first block + if l.startswith('# Authors:'): + return True + + return False + +def extract_existing_authors(header_content): + """Extract existing @author tags from a Doxygen header.""" + authors = [] + lines = header_content.split('\n') + for line in lines: + stripped = line.strip() + if stripped.startswith('* @author '): + # Extract author info from "@author Name " format + author_part = stripped[10:] # Remove "* @author " + # Parse name and email + if ' <' in author_part and author_part.endswith('>'): + name_part, email_part = author_part.rsplit(' <', 1) + email = email_part.rstrip('>') + authors.append((name_part.strip(), email)) + else: + # Just name without email + authors.append((author_part.strip(), '')) + return authors + +def extract_existing_assembly_authors(content): + """Extract existing authors from assembly header.""" + authors = [] + lines = content.split('\n') + i = 0 + + # Skip empty lines at the beginning + while i < len(lines) and lines[i].strip() == '': + i += 1 + + # Look for the first sharps block + if i < len(lines) and lines[i].lstrip().startswith('#') and len(lines[i].lstrip()) >= 10 and set(lines[i].lstrip()) == {'#'}: + # Find the closing sharps line + for j in range(i+1, len(lines)): + l = lines[j].lstrip() + if l.startswith('#') and len(l) >= 10 and set(l) == {'#'}: + break # End of first block + if l.startswith('# Authors:'): + # Parse the first author line + author_line = l[10:].strip() # Remove "# Authors: " + if ' <' in author_line and author_line.endswith('>'): + name_part, email_part = author_line.rsplit(' <', 1) + email = email_part.rstrip('>') + authors.append((name_part.strip(), email)) + else: + authors.append((author_line.strip(), '')) + # Look for continuation lines + for k in range(j+1, len(lines)): + l2 = lines[k].lstrip() + if l2.startswith('#') and len(l2) >= 10 and set(l2) == {'#'}: + break # End of block + if l2.startswith('# ') and ' <' in l2 and '@' in l2: + # Continuation author line + author_line = l2[11:].strip() # Remove "# " + if ' <' in author_line and author_line.endswith('>'): + name_part, email_part = author_line.rsplit(' <', 1) + email = email_part.rstrip('>') + authors.append((name_part.strip(), email)) + else: + authors.append((author_line.strip(), '')) + break + + return authors + +def authors_match(existing_authors, detected_authors): + """Check if existing authors match detected authors (order-independent).""" + if len(existing_authors) != len(detected_authors): + return False + + # Normalize both lists for comparison + existing_set = set() + detected_set = set() + + for name, email in existing_authors: + # Normalize existing authors + normalized_name, normalized_email = normalize_author(name, email) + existing_set.add((normalized_name, normalized_email)) + + for name, email in detected_authors: + # Detected authors are already normalized + detected_set.add((name, email)) + + return existing_set == detected_set + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/GL/array.c b/src/GL/array.c new file mode 100644 index 000000000..f7ffce03a --- /dev/null +++ b/src/GL/array.c @@ -0,0 +1,461 @@ +/** + * @file array.c + * @author Dennis Heinze + * @brief OpenGL array and attribute pointer management for vertex data. + */ +#include "gl_internal.h" +#include "debug.h" +#include + +extern gl_state_t *state; + +typedef struct { + GLboolean et, ec, en; + GLint st, sc, sv; + GLenum tc; + GLuint pc, pn, pv; + GLsizei s; +} gl_interleaved_array_t; + +#define ILA_F (sizeof(GLfloat)) +#define ILA_C (sizeof(GLubyte) * 4) + +static const gl_interleaved_array_t interleaved_arrays[] = { + /* GL_V2F */ { .et = false, .ec = false, .en = false, .sv = 2, .pv = 0, .s = 2*ILA_F }, + /* GL_V3F */ { .et = false, .ec = false, .en = false, .sv = 3, .pv = 0, .s = 3*ILA_F }, + /* GL_C4UB_V2F */ { .et = false, .ec = true, .en = false, .sc = 4, .sv = 2, .tc = GL_UNSIGNED_BYTE, .pc = 0, .pv = ILA_C, .s = ILA_C + 2*ILA_F }, + /* GL_C4UB_V3F */ { .et = false, .ec = true, .en = false, .sc = 4, .sv = 3, .tc = GL_UNSIGNED_BYTE, .pc = 0, .pv = ILA_C, .s = ILA_C + 3*ILA_F }, + /* GL_C3F_V3F */ { .et = false, .ec = true, .en = false, .sc = 3, .sv = 3, .tc = GL_FLOAT, .pc = 0, .pv = 3*ILA_F, .s = 6*ILA_F }, + /* GL_N3F_V3F */ { .et = false, .ec = false, .en = true, .sv = 3, .pn = 0, .pv = 3*ILA_F, .s = 6*ILA_F }, + /* GL_C4F_N3F_V3F */ { .et = false, .ec = true, .en = true, .sc = 4, .sv = 3, .tc = GL_FLOAT, .pc = 0, .pn = 4*ILA_F, .pv = 7*ILA_F, .s = 10*ILA_F }, + /* GL_T2F_V3F */ { .et = true, .ec = false, .en = false, .st = 2, .sv = 3, .pv = 2*ILA_F, .s = 5*ILA_F }, + /* GL_T4F_V4F */ { .et = true, .ec = false, .en = false, .st = 4, .sv = 4, .pv = 4*ILA_F, .s = 8*ILA_F }, + /* GL_T2F_C4UB_V3F */ { .et = true, .ec = true, .en = false, .st = 2, .sc = 4, .sv = 3, .tc = GL_UNSIGNED_BYTE, .pc = 2*ILA_F, .pv = ILA_C + 2*ILA_F, .s = ILA_C + 5*ILA_F }, + /* GL_T2F_C3F_V3F */ { .et = true, .ec = true, .en = false, .st = 2, .sc = 3, .sv = 3, .tc = GL_FLOAT, .pc = 2*ILA_F, .pv = 5*ILA_F, .s = 8*ILA_F }, + /* GL_T2F_N3F_V3F */ { .et = true, .ec = false, .en = true, .st = 2, .sv = 3, .pn = 2*ILA_F, .pv = 5*ILA_F, .s = 8*ILA_F }, + /* GL_T2F_C4F_N3F_V3F */ { .et = true, .ec = true, .en = true, .st = 2, .sc = 4, .sv = 3, .tc = GL_FLOAT, .pc = 2*ILA_F, .pn = 6*ILA_F, .pv = 9*ILA_F, .s = 12*ILA_F }, + /* GL_T4F_C4F_N3F_V4F */ { .et = true, .ec = true, .en = true, .st = 4, .sc = 4, .sv = 4, .tc = GL_FLOAT, .pc = 4*ILA_F, .pn = 8*ILA_F, .pv = 11*ILA_F, .s = 15*ILA_F }, +}; + +extern const cpu_read_attrib_func cpu_read_funcs[ATTRIB_COUNT][ATTRIB_TYPE_COUNT]; +extern const rsp_read_attrib_func rsp_read_funcs[ATTRIB_COUNT][ATTRIB_TYPE_COUNT]; + +gl_array_type_t gl_array_type_from_enum(GLenum array) +{ + switch (array) { + case GL_VERTEX_ARRAY: + return ATTRIB_VERTEX; + case GL_TEXTURE_COORD_ARRAY: + return ATTRIB_TEXCOORD; + case GL_NORMAL_ARRAY: + return ATTRIB_NORMAL; + case GL_COLOR_ARRAY: + return ATTRIB_COLOR; + case GL_MATRIX_INDEX_ARRAY_ARB: + return ATTRIB_MTX_INDEX; + default: + return -1; + } +} + +void gl_update_array(gl_array_t *array, gl_array_type_t array_type) +{ + uint32_t size_shift = 0; + + switch (array->type) { + case GL_BYTE: + case GL_UNSIGNED_BYTE: + size_shift = 0; + break; + case GL_SHORT: + case GL_UNSIGNED_SHORT: + case GL_HALF_FIXED_N64: + size_shift = 1; + break; + case GL_INT: + case GL_UNSIGNED_INT: + case GL_FLOAT: + size_shift = 2; + break; + case GL_DOUBLE: + size_shift = 3; + break; + } + + array->final_stride = array->stride == 0 ? array->size << size_shift : array->stride; + + uint32_t func_index = gl_type_to_index(array->type); + array->cpu_read_func = cpu_read_funcs[array_type][func_index]; + array->rsp_read_func = rsp_read_funcs[array_type][func_index]; + + assertf(array->cpu_read_func != NULL, "CPU read function is missing"); + assertf(array->rsp_read_func != NULL, "RSP read function is missing"); +} + +void gl_update_array_pointer(gl_array_t *array) +{ + if (array->binding != NULL) { + array->final_pointer = array->binding->storage.data + (uint32_t)array->pointer; + } else { + array->final_pointer = array->pointer; + } +} + +void gl_update_array_pointers(gl_array_object_t *obj) +{ + for (uint32_t i = 0; i < ATTRIB_COUNT; i++) + { + gl_update_array_pointer(&obj->arrays[i]); + } +} + +void gl_array_object_init(gl_array_object_t *obj) +{ + obj->arrays[ATTRIB_VERTEX].size = 4; + obj->arrays[ATTRIB_VERTEX].type = GL_FLOAT; + obj->arrays[ATTRIB_COLOR].size = 4; + obj->arrays[ATTRIB_COLOR].type = GL_FLOAT; + obj->arrays[ATTRIB_COLOR].normalize = true; + obj->arrays[ATTRIB_TEXCOORD].size = 4; + obj->arrays[ATTRIB_TEXCOORD].type = GL_FLOAT; + obj->arrays[ATTRIB_NORMAL].size = 3; + obj->arrays[ATTRIB_NORMAL].type = GL_FLOAT; + obj->arrays[ATTRIB_NORMAL].normalize = true; + obj->arrays[ATTRIB_MTX_INDEX].size = 0; + obj->arrays[ATTRIB_MTX_INDEX].type = GL_UNSIGNED_BYTE; + + for (uint32_t i = 0; i < ATTRIB_COUNT; i++) + { + gl_update_array(&obj->arrays[i], i); + } +} + +void gl_array_init() +{ + gl_array_object_init(&state->default_array_object); + state->array_object = &state->default_array_object; +} + +void gl_set_array(gl_array_type_t array_type, GLint size, GLenum type, GLsizei stride, const GLvoid *pointer) +{ + if (stride < 0) { + gl_set_error(GL_INVALID_VALUE, "Stride must not be negative"); + return; + } + + // From the spec (https://registry.khronos.org/OpenGL/extensions/ARB/ARB_vertex_array_object.txt): + // An INVALID_OPERATION error is generated if any of the *Pointer commands + // specifying the location and organization of vertex data are called while + // a non-zero vertex array object is bound, zero is bound to the + // ARRAY_BUFFER buffer object, and the pointer is not NULL[fn]. + // [fn: This error makes it impossible to create a vertex array + // object containing client array pointers.] + if (state->array_object != &state->default_array_object && state->array_buffer == NULL && pointer != NULL) { + gl_set_error(GL_INVALID_OPERATION, "Vertex array objects can only be used in conjunction with vertex buffer objects"); + return; + } + + gl_array_t *array = &state->array_object->arrays[array_type]; + + array->size = size; + array->type = type; + array->stride = stride; + array->pointer = pointer; + array->binding = state->array_buffer; + + gl_update_array(array, array_type); +} + +void glVertexPointer(GLint size, GLenum type, GLsizei stride, const GLvoid *pointer) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (size) { + case 2: + case 3: + case 4: + break; + default: + gl_set_error(GL_INVALID_VALUE, "Size must be 2, 3 or 4"); + return; + } + + switch (type) { + case GL_SHORT: + case GL_INT: + case GL_FLOAT: + case GL_DOUBLE: + case GL_HALF_FIXED_N64: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid vertex data type", type); + return; + } + + gl_set_array(ATTRIB_VERTEX, size, type, stride, pointer); +} + +void glTexCoordPointer(GLint size, GLenum type, GLsizei stride, const GLvoid *pointer) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (size) { + case 1: + case 2: + case 3: + case 4: + break; + default: + gl_set_error(GL_INVALID_VALUE, "Size must be 1, 2, 3 or 4"); + return; + } + + switch (type) { + case GL_SHORT: + case GL_INT: + case GL_FLOAT: + case GL_DOUBLE: + case GL_HALF_FIXED_N64: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid texture coordinate data type", type); + return; + } + + gl_set_array(ATTRIB_TEXCOORD, size, type, stride, pointer); +} + +void glNormalPointer(GLenum type, GLsizei stride, const GLvoid *pointer) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (type) { + case GL_BYTE: + case GL_SHORT: + case GL_INT: + case GL_FLOAT: + case GL_DOUBLE: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid normal data type", type); + return; + } + + gl_set_array(ATTRIB_NORMAL, 3, type, stride, pointer); +} + +void glColorPointer(GLint size, GLenum type, GLsizei stride, const GLvoid *pointer) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (size) { + case 3: + case 4: + break; + default: + gl_set_error(GL_INVALID_VALUE, "Size must be 3 or 4"); + return; + } + + switch (type) { + case GL_BYTE: + case GL_UNSIGNED_BYTE: + case GL_SHORT: + case GL_UNSIGNED_SHORT: + case GL_INT: + case GL_UNSIGNED_INT: + case GL_FLOAT: + case GL_DOUBLE: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid color data type", type); + return; + } + + gl_set_array(ATTRIB_COLOR, size, type, stride, pointer); +} + +void glMatrixIndexPointerARB(GLint size, GLenum type, GLsizei stride, const GLvoid *pointer) +{ + if (!gl_ensure_no_begin_end()) return; + + if (size < 1 || size > VERTEX_UNIT_COUNT) { + gl_set_error(GL_INVALID_VALUE, "Size must be 1"); + return; + } + + switch (type) { + case GL_UNSIGNED_BYTE: + case GL_UNSIGNED_SHORT: + case GL_UNSIGNED_INT: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid matrix index data type", type); + return; + } + + gl_set_array(ATTRIB_MTX_INDEX, size, type, stride, pointer); +} + +void gl_set_array_enabled(gl_array_type_t array_type, bool enabled) +{ + gl_array_t *array = &state->array_object->arrays[array_type]; + array->enabled = enabled; +} + +void glEnableClientState(GLenum array) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (array) { + case GL_VERTEX_ARRAY: + case GL_TEXTURE_COORD_ARRAY: + case GL_NORMAL_ARRAY: + case GL_COLOR_ARRAY: + case GL_MATRIX_INDEX_ARRAY_ARB: + gl_set_array_enabled(gl_array_type_from_enum(array), true); + break; + case GL_EDGE_FLAG_ARRAY: + case GL_INDEX_ARRAY: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid client state", array); + break; + } +} +void glDisableClientState(GLenum array) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (array) { + case GL_VERTEX_ARRAY: + case GL_TEXTURE_COORD_ARRAY: + case GL_NORMAL_ARRAY: + case GL_COLOR_ARRAY: + case GL_MATRIX_INDEX_ARRAY_ARB: + gl_set_array_enabled(gl_array_type_from_enum(array), false); + break; + case GL_EDGE_FLAG_ARRAY: + case GL_INDEX_ARRAY: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid client state", array); + break; + } +} + +void glInterleavedArrays(GLenum format, GLsizei stride, const GLvoid *pointer) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (format) { + case GL_V2F: + case GL_V3F: + case GL_C4UB_V2F: + case GL_C4UB_V3F: + case GL_C3F_V3F: + case GL_N3F_V3F: + case GL_C4F_N3F_V3F: + case GL_T2F_V3F: + case GL_T4F_V4F: + case GL_T2F_C4UB_V3F: + case GL_T2F_C3F_V3F: + case GL_T2F_N3F_V3F: + case GL_T2F_C4F_N3F_V3F: + case GL_T4F_C4F_N3F_V4F: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid interleaved array format", format); + return; + } + + const gl_interleaved_array_t *a = &interleaved_arrays[format - GL_V2F]; + + if (stride == 0) { + stride = a->s; + } + + if (a->et) { + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glTexCoordPointer(a->st, GL_FLOAT, stride, pointer); + } else { + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + } + + if (a->ec) { + glEnableClientState(GL_COLOR_ARRAY); + glColorPointer(a->sc, a->tc, stride, pointer + a->pc); + } else { + glDisableClientState(GL_COLOR_ARRAY); + } + + if (a->en) { + glEnableClientState(GL_NORMAL_ARRAY); + glNormalPointer(GL_FLOAT, stride, pointer + a->pn); + } else { + glDisableClientState(GL_NORMAL_ARRAY); + } + + glEnableClientState(GL_VERTEX_ARRAY); + glVertexPointer(a->sv, GL_FLOAT, stride, pointer + a->pv); +} + +void glGenVertexArrays(GLsizei n, GLuint *arrays) +{ + if (!gl_ensure_no_begin_end()) return; + + for (GLsizei i = 0; i < n; i++) + { + gl_array_object_t *new_obj = calloc(sizeof(gl_array_object_t), 1); + assertf(new_obj, "Out of memory"); + gl_array_object_init(new_obj); + arrays[i] = (GLuint)new_obj; + } +} + +void glDeleteVertexArrays(GLsizei n, const GLuint *arrays) +{ + if (!gl_ensure_no_begin_end()) return; + + for (GLsizei i = 0; i < n; i++) + { + assertf(arrays[i] == 0 || is_valid_object_id(arrays[i]), + "Not a valid array object: %#lx. Make sure to allocate IDs via glGenVertexArray", arrays[i]); + + gl_array_object_t *obj = (gl_array_object_t*)arrays[i]; + if (obj == NULL) { + continue; + } + + if (obj == state->array_object) { + glBindVertexArray(0); + } + + free(obj); + } +} + +void glBindVertexArray(GLuint array) +{ + if (!gl_ensure_no_begin_end()) return; + assertf(array == 0 || is_valid_object_id(array), + "Not a valid array object: %#lx. Make sure to allocate IDs via glGenVertexArray", array); + + gl_array_object_t *obj = (gl_array_object_t*)array; + + if (obj == NULL) { + obj = &state->default_array_object; + } + + state->array_object = obj; +} + +GLboolean glIsVertexArray(GLuint array) +{ + if (!gl_ensure_no_begin_end()) return 0; + + // FIXME: This doesn't actually guarantee that it's a valid array object, but just uses the heuristic of + // "is it somewhere in the heap memory?". This way we can at least rule out arbitrarily chosen integer constants, + // which used to be valid array IDs in legacy OpenGL. + return is_valid_object_id(array); +} diff --git a/src/GL/buffer.c b/src/GL/buffer.c new file mode 100644 index 000000000..186666074 --- /dev/null +++ b/src/GL/buffer.c @@ -0,0 +1,319 @@ +/** + * @file buffer.c + * @author Dennis Heinze + * @brief OpenGL buffer object management and data transfer. + */ +#include "gl_internal.h" +#include +#include +#include + +extern gl_state_t *state; + + +GLboolean glIsBufferARB(GLuint buffer) +{ + // FIXME: This doesn't actually guarantee that it's a valid buffer object, but just uses the heuristic of + // "is it somewhere in the heap memory?". This way we can at least rule out arbitrarily chosen integer constants, + // which used to be valid buffer IDs in legacy OpenGL. + return is_valid_object_id(buffer); +} + +void glBindBufferARB(GLenum target, GLuint buffer) +{ + if (!gl_ensure_no_begin_end()) return; + assertf(buffer == 0 || is_valid_object_id(buffer), + "Not a valid buffer object: %#lx. Make sure to allocate IDs via glGenBuffersARB", buffer); + + gl_buffer_object_t *obj = (gl_buffer_object_t*)buffer; + + switch (target) { + case GL_ARRAY_BUFFER_ARB: + state->array_buffer = obj; + break; + case GL_ELEMENT_ARRAY_BUFFER_ARB: + state->element_array_buffer = obj; + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid buffer target", target); + return; + } +} + +void gl_unbind_buffer(gl_buffer_object_t *obj, gl_buffer_object_t **binding) +{ + if (*binding == obj) { + *binding = NULL; + } +} + +void glDeleteBuffersARB(GLsizei n, const GLuint *buffers) +{ + if (!gl_ensure_no_begin_end()) return; + + for (GLsizei i = 0; i < n; i++) + { + assertf(buffers[i] == 0 || is_valid_object_id(buffers[i]), + "Not a valid buffer object: %#lx. Make sure to allocate IDs via glGenBuffersARB", buffers[i]); + + gl_buffer_object_t *obj = (gl_buffer_object_t*)buffers[i]; + if (obj == NULL) { + continue; + } + + gl_unbind_buffer(obj, &state->array_buffer); + gl_unbind_buffer(obj, &state->element_array_buffer); + + for (uint32_t a = 0; a < ATTRIB_COUNT; a++) + { + // FIXME: From the spec: + // (2) What happens when a buffer object that is attached to a non-current + // VAO is deleted? + // RESOLUTION: Nothing (though a reference count may be decremented). + // A buffer object that is deleted while attached to a non-current VAO + // is treated just like a buffer object bound to another context (or to + // a current VAO in another context). + gl_unbind_buffer(obj, &state->array_object->arrays[a].binding); + } + + // TODO: keep alive until no longer in use + + if (obj->storage.data != NULL) + { + free_uncached(obj->storage.data); + } + + free(obj); + } +} + +void glGenBuffersARB(GLsizei n, GLuint *buffers) +{ + if (!gl_ensure_no_begin_end()) return; + + for (GLsizei i = 0; i < n; i++) + { + gl_buffer_object_t *new_obj = calloc(sizeof(gl_buffer_object_t), 1); + assertf(new_obj, "Out of memory"); + new_obj->usage = GL_STATIC_DRAW_ARB; + new_obj->access = GL_READ_WRITE_ARB; + buffers[i] = (GLuint)new_obj; + } +} + +bool gl_get_buffer_object(GLenum target, gl_buffer_object_t **obj) +{ + switch (target) { + case GL_ARRAY_BUFFER_ARB: + *obj = state->array_buffer; + break; + case GL_ELEMENT_ARRAY_BUFFER_ARB: + *obj = state->element_array_buffer; + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid buffer target", target); + return false; + } + + if (*obj == NULL) { + gl_set_error(GL_INVALID_OPERATION, "No buffer object is currently bound"); + return false; + } + + return true; +} + +void glBufferDataARB(GLenum target, GLsizeiptrARB size, const GLvoid *data, GLenum usage) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_buffer_object_t *obj = NULL; + if (!gl_get_buffer_object(target, &obj)) { + return; + } + + switch (usage) { + case GL_STREAM_DRAW_ARB: + case GL_STREAM_READ_ARB: + case GL_STREAM_COPY_ARB: + case GL_STATIC_DRAW_ARB: + case GL_STATIC_READ_ARB: + case GL_STATIC_COPY_ARB: + case GL_DYNAMIC_DRAW_ARB: + case GL_DYNAMIC_READ_ARB: + case GL_DYNAMIC_COPY_ARB: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid buffer usage", usage); + return; + } + + if (size < 0) { + gl_set_error(GL_INVALID_VALUE, "Size must not be negative"); + return; + } + + if (!gl_storage_resize(&obj->storage, size)) { + gl_set_error(GL_OUT_OF_MEMORY, "Failed to allocate buffer storage"); + return; + } + + if (data != NULL) { + memcpy(obj->storage.data, data, size); + } + + obj->usage = usage; + obj->access = GL_READ_WRITE_ARB; + obj->mapped = false; + obj->pointer = NULL; +} + +void glBufferSubDataARB(GLenum target, GLintptrARB offset, GLsizeiptrARB size, const GLvoid *data) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_buffer_object_t *obj = NULL; + if (!gl_get_buffer_object(target, &obj)) { + return; + } + + if (obj->mapped) { + gl_set_error(GL_INVALID_OPERATION, "The buffer object is currently mapped"); + return; + } + + if (offset < 0) { + gl_set_error(GL_INVALID_VALUE, "Offset must not be negative"); + return; + } + + if ((offset >= obj->storage.size) || (offset + size > obj->storage.size)) { + gl_set_error(GL_INVALID_VALUE, "Offset and size define a memory region that is beyond the buffer storage"); + return; + } + + memcpy(obj->storage.data + offset, data, size); +} + +void glGetBufferSubDataARB(GLenum target, GLintptrARB offset, GLsizeiptrARB size, GLvoid *data) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_buffer_object_t *obj = NULL; + if (!gl_get_buffer_object(target, &obj)) { + return; + } + + if (obj->mapped) { + gl_set_error(GL_INVALID_OPERATION, "The buffer object is currently mapped"); + return; + } + + if (offset < 0) { + gl_set_error(GL_INVALID_VALUE, "Offset must not be negative"); + return; + } + + if ((offset >= obj->storage.size) || (offset + size > obj->storage.size)) { + gl_set_error(GL_INVALID_VALUE, "Offset and size define a memory region that is beyond the buffer storage"); + return; + } + + memcpy(data, obj->storage.data + offset, size); +} + +GLvoid * glMapBufferARB(GLenum target, GLenum access) +{ + if (!gl_ensure_no_begin_end()) return 0; + + gl_buffer_object_t *obj = NULL; + if (!gl_get_buffer_object(target, &obj)) { + return NULL; + } + + switch (access) { + case GL_READ_ONLY_ARB: + case GL_WRITE_ONLY_ARB: + case GL_READ_WRITE_ARB: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid buffer access", access); + return NULL; + } + + if (obj->mapped) { + gl_set_error(GL_INVALID_OPERATION, "The buffer object is already mapped"); + return NULL; + } + + obj->access = access; + obj->mapped = true; + obj->pointer = obj->storage.data; + + return obj->pointer; +} + +GLboolean glUnmapBufferARB(GLenum target) +{ + if (!gl_ensure_no_begin_end()) return 0; + + gl_buffer_object_t *obj = NULL; + if (!gl_get_buffer_object(target, &obj)) { + return GL_FALSE; + } + + if (!obj->mapped) { + gl_set_error(GL_INVALID_OPERATION, "The buffer object has not been mapped"); + return GL_FALSE; + } + + obj->mapped = false; + obj->pointer = NULL; + + return GL_TRUE; +} + +void glGetBufferParameterivARB(GLenum target, GLenum pname, GLint *params) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_buffer_object_t *obj = NULL; + if (!gl_get_buffer_object(target, &obj)) { + return; + } + + switch (pname) { + case GL_BUFFER_SIZE_ARB: + *params = obj->storage.size; + break; + case GL_BUFFER_USAGE_ARB: + *params = obj->usage; + break; + case GL_BUFFER_ACCESS_ARB: + *params = obj->access; + break; + case GL_BUFFER_MAPPED_ARB: + *params = obj->mapped; + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid buffer parameter", pname); + return; + } +} + +void glGetBufferPointervARB(GLenum target, GLenum pname, GLvoid **params) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_buffer_object_t *obj = NULL; + if (!gl_get_buffer_object(target, &obj)) { + return; + } + + if (pname != GL_BUFFER_MAP_POINTER_ARB) { + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid buffer pointer", pname); + return; + } + + *params = obj->pointer; +} diff --git a/src/GL/cpu_pipeline.c b/src/GL/cpu_pipeline.c new file mode 100644 index 000000000..189a8e78b --- /dev/null +++ b/src/GL/cpu_pipeline.c @@ -0,0 +1,886 @@ +/** + * @file cpu_pipeline.c + * @author Dennis Heinze + * @brief OpenGL CPU-based rendering pipeline implementation. + */ +#include "gl_internal.h" +#include "rdpq_rect.h" + +extern gl_state_t *state; + +static const float clip_planes[CLIPPING_PLANE_COUNT][4] = { + { 1, 0, 0, GUARD_BAND_FACTOR }, + { 0, 1, 0, GUARD_BAND_FACTOR }, + { 0, 0, 1, 1 }, + { 1, 0, 0, -GUARD_BAND_FACTOR }, + { 0, 1, 0, -GUARD_BAND_FACTOR }, + { 0, 0, 1, -1 }, +}; + +static void read_i8(GLfloat *dst, const int8_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) dst[i] = src[i]; +} + +static void read_i16(GLfloat *dst, const int16u_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) dst[i] = src[i]; +} + +static void read_i32(GLfloat *dst, const int32u_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) dst[i] = src[i]; +} + +static void read_u8n(GLfloat *dst, const uint8_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) dst[i] = U8_TO_FLOAT(src[i]); +} + +static void read_i8n(GLfloat *dst, const int8_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) dst[i] = I8_TO_FLOAT(src[i]); +} + +static void read_u16n(GLfloat *dst, const uint16u_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) dst[i] = U16_TO_FLOAT(src[i]); +} + +static void read_i16n(GLfloat *dst, const int16u_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) dst[i] = I16_TO_FLOAT(src[i]); +} + +static void read_u32n(GLfloat *dst, const uint32u_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) dst[i] = U32_TO_FLOAT(src[i]); +} + +static void read_i32n(GLfloat *dst, const int32u_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) dst[i] = I32_TO_FLOAT(src[i]); +} + +static void read_f32(GLfloat *dst, const floatu *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) dst[i] = src[i]; +} + +static void read_f64(GLfloat *dst, const doubleu *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) dst[i] = src[i]; +} + +static void read_x16_vtx(GLfloat *dst, const int16u_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) dst[i] = src[i] * state->vertex_halfx_precision.to_float_factor; +} + +static void read_x16_tex(GLfloat *dst, const int16u_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) dst[i] = src[i] * state->texcoord_halfx_precision.to_float_factor; +} + +static void read_u8_i(GLubyte *dst, const uint8_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) dst[i] = src[i]; +} + +static void read_u16_i(GLubyte *dst, const uint16u_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) dst[i] = src[i]; +} + +static void read_u32_i(GLubyte *dst, const uint32u_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) dst[i] = src[i]; +} + +const cpu_read_attrib_func cpu_read_funcs[ATTRIB_COUNT][ATTRIB_TYPE_COUNT] = { + { + (cpu_read_attrib_func)read_i8, + NULL, + (cpu_read_attrib_func)read_i16, + NULL, + (cpu_read_attrib_func)read_i32, + NULL, + (cpu_read_attrib_func)read_f32, + (cpu_read_attrib_func)read_f64, + (cpu_read_attrib_func)read_x16_vtx, + }, + { + (cpu_read_attrib_func)read_i8n, + (cpu_read_attrib_func)read_u8n, + (cpu_read_attrib_func)read_i16n, + (cpu_read_attrib_func)read_u16n, + (cpu_read_attrib_func)read_i32n, + (cpu_read_attrib_func)read_u32n, + (cpu_read_attrib_func)read_f32, + (cpu_read_attrib_func)read_f64, + NULL, + }, + { + (cpu_read_attrib_func)read_i8, + NULL, + (cpu_read_attrib_func)read_i16, + NULL, + (cpu_read_attrib_func)read_i32, + NULL, + (cpu_read_attrib_func)read_f32, + (cpu_read_attrib_func)read_f64, + (cpu_read_attrib_func)read_x16_tex, + }, + { + (cpu_read_attrib_func)read_i8n, + NULL, + (cpu_read_attrib_func)read_i16n, + NULL, + (cpu_read_attrib_func)read_i32n, + NULL, + (cpu_read_attrib_func)read_f32, + (cpu_read_attrib_func)read_f64, + NULL, + }, + { + NULL, + (cpu_read_attrib_func)read_u8_i, + NULL, + (cpu_read_attrib_func)read_u16_i, + NULL, + (cpu_read_attrib_func)read_u32_i, + NULL, + NULL, + NULL, + }, +}; + +static void gl_clip_triangle(); +static void gl_clip_line(); +static void gl_clip_point(); + +static void gl_init_cpu_pipe() +{ + gl_texture_object_t *tex_obj = gl_get_active_texture(); + if (tex_obj != NULL && gl_tex_is_complete(tex_obj)) { + state->prim_texture = true; + state->prim_mipmaps = gl_tex_get_levels(tex_obj); + state->prim_tex_width = tex_obj->srv_object->levels[0].width; + state->prim_tex_height = tex_obj->srv_object->levels[0].height; + state->prim_bilinear = tex_obj->srv_object->mag_filter == GL_LINEAR || + tex_obj->srv_object->min_filter == GL_LINEAR || + tex_obj->srv_object->min_filter == GL_LINEAR_MIPMAP_NEAREST || + tex_obj->srv_object->min_filter == GL_LINEAR_MIPMAP_LINEAR; + } else { + state->prim_texture = false; + state->prim_mipmaps = 0; + state->prim_tex_width = 0; + state->prim_tex_height = 0; + state->prim_bilinear = false; + } + + state->trifmt = (rdpq_trifmt_t){ + .pos_offset = VTX_SCREEN_POS_OFFSET, + .shade_offset = VTX_SHADE_OFFSET, + .shade_flat = state->shade_model == GL_FLAT, + .tex_offset = state->prim_texture ? VTX_TEXCOORD_OFFSET : -1, + .tex_mipmaps = state->prim_mipmaps, + .z_offset = state->depth_test ? VTX_DEPTH_OFFSET : -1, + }; + + gl_update_matrix_targets(); +} + +static float dot_product4(const float *a, const float *b) +{ + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]; +} + +static float lerp(float a, float b, float t) +{ + return a + (b - a) * t; +} + +static uint8_t gl_get_clip_codes(GLfloat *pos, GLfloat *ref) +{ + // This corresponds to vcl + vch on RSP + uint8_t codes = 0; + for (uint32_t i = 0; i < 3; i++) + { + if (pos[i] < - ref[i]) { + codes |= 1 << i; + } else if (pos[i] > ref[i]) { + codes |= 1 << (i + 3); + } + } + return codes; +} + +static gl_matrix_target_t* gl_get_matrix_target(uint8_t mtx_index) +{ + if (state->matrix_palette_enabled) { + return &state->palette_matrix_targets[mtx_index]; + } + + return &state->default_matrix_target; +} + +static void gl_vertex_pre_tr(uint8_t cache_index) +{ + gl_vtx_t *v = &state->vertex_cache[cache_index]; + + memcpy(&v->obj_attributes, &state->current_attributes, sizeof(gl_obj_attributes_t)); + + gl_matrix_target_t* mtx_target = gl_get_matrix_target(v->obj_attributes.mtx_index[0]); + gl_matrix_mult(v->cs_pos, &mtx_target->mvp, v->obj_attributes.position); + +#if 0 + debugf("VTX ID: %d\n", id); + debugf(" OBJ: %8.2f %8.2f %8.2f %8.2f\n", v->obj_pos[0], v->obj_pos[1],v->obj_pos[2], v->obj_pos[3]); + debugf(" [%08lx %08lx %08lx %08lx]\n", + fx16(OBJ_SCALE*v->obj_pos[0]), fx16(OBJ_SCALE*v->obj_pos[1]), fx16(OBJ_SCALE*v->obj_pos[2]), fx16(OBJ_SCALE*v->obj_pos[3])); + debugf(" CSPOS: %8.2f %8.2f %8.2f %8.2f\n", v->cs_pos[0], v->cs_pos[1], v->cs_pos[2], v->cs_pos[3]); + debugf(" [%08lx %08lx %08lx %08lx]\n", fx16(OBJ_SCALE*v->cs_pos[0]), fx16(OBJ_SCALE*v->cs_pos[1]), fx16(OBJ_SCALE*v->cs_pos[2]), fx16(OBJ_SCALE*v->cs_pos[3])); +#endif + + GLfloat tr_ref[] = { + v->cs_pos[3], + v->cs_pos[3], + v->cs_pos[3] + }; + + v->tr_code = gl_get_clip_codes(v->cs_pos, tr_ref); + v->t_l_applied = false; +} + +static void gl_calc_texture_coord(GLfloat *dest, const GLfloat *input, uint32_t coord_index, const gl_tex_gen_t *gen, const GLfloat *obj_pos, const GLfloat *eye_pos, const GLfloat *eye_normal) +{ + if (!gen->enabled) { + dest[coord_index] = input[coord_index]; + return; + } + + switch (gen->mode) { + case GL_EYE_LINEAR: + dest[coord_index] = eye_pos[0] * gen->eye_plane[0] + + eye_pos[1] * gen->eye_plane[1] + + eye_pos[2] * gen->eye_plane[2] + + eye_pos[3] * gen->eye_plane[3]; + break; + case GL_OBJECT_LINEAR: + dest[coord_index] = obj_pos[0] * gen->object_plane[0] + + obj_pos[1] * gen->object_plane[1] + + obj_pos[2] * gen->object_plane[2] + + obj_pos[3] * gen->object_plane[3]; + break; + case GL_SPHERE_MAP: + GLfloat norm_eye_pos[3]; + gl_normalize(norm_eye_pos, eye_pos); + GLfloat d2 = 2.0f * dot_product3(norm_eye_pos, eye_normal); + GLfloat r[3] = { + norm_eye_pos[0] - eye_normal[0] * d2, + norm_eye_pos[1] - eye_normal[1] * d2, + norm_eye_pos[2] - eye_normal[2] * d2 + 1.0f, + }; + GLfloat m = 1.0f / (2.0f * sqrtf(dot_product3(r, r))); + dest[coord_index] = r[coord_index] * m + 0.5f; + break; + } +} + +static void gl_calc_texture_coords(GLfloat *dest, const GLfloat *input, const GLfloat *obj_pos, const GLfloat *eye_pos, const GLfloat *eye_normal) +{ + GLfloat tmp[TEX_COORD_COUNT]; + GLfloat result[TEX_COORD_COUNT]; + + for (uint32_t i = 0; i < TEX_GEN_COUNT; i++) + { + gl_calc_texture_coord(tmp, input, i, &state->tex_gen[i], obj_pos, eye_pos, eye_normal); + } + + // TODO: skip matrix multiplication if it is the identity + gl_matrix_mult(result, gl_matrix_stack_get_matrix(&state->texture_stack), tmp); + + GLfloat inv_q = 1.0f / result[3]; + + dest[0] = result[0] * inv_q; + dest[1] = result[1] * inv_q; +} + +static void gl_vertex_calc_clip_code(gl_vtx_t *v) +{ + GLfloat clip_ref[] = { + v->cs_pos[3] * GUARD_BAND_FACTOR, + v->cs_pos[3] * GUARD_BAND_FACTOR, + v->cs_pos[3] + }; + + v->clip_code = gl_get_clip_codes(v->cs_pos, clip_ref); +} + +static void gl_vertex_calc_screenspace(gl_vtx_t *v) +{ + v->inv_w = v->cs_pos[3] != 0.0f ? 1.0f / v->cs_pos[3] : 0x7FFF; + + v->screen_pos[0] = v->cs_pos[0] * v->inv_w * state->current_viewport.scale[0] + state->current_viewport.offset[0]; + v->screen_pos[1] = v->cs_pos[1] * v->inv_w * state->current_viewport.scale[1] + state->current_viewport.offset[1]; + + v->depth = v->cs_pos[2] * v->inv_w * state->current_viewport.scale[2] + state->current_viewport.offset[2]; +} + +static void gl_vertex_t_l(gl_vtx_t *vtx) +{ + gl_matrix_target_t* mtx_target = gl_get_matrix_target(vtx->obj_attributes.mtx_index[0]); + gl_matrix_t *mv = gl_matrix_stack_get_matrix(mtx_target->mv_stack); + + GLfloat eye_pos[4]; + GLfloat eye_normal[3]; + + if (state->lighting || state->fog || state->prim_texture) { + gl_matrix_mult(eye_pos, mv, vtx->obj_attributes.position); + } + + if (state->lighting || state->prim_texture) { + // TODO: use inverse transpose matrix + gl_matrix_mult3x3(eye_normal, mv, vtx->obj_attributes.normal); + + if (state->normalize) { + gl_normalize(eye_normal, eye_normal); + } + } + + if (state->lighting) { + gl_perform_lighting(vtx->shade, vtx->obj_attributes.color, eye_pos, eye_normal, &state->material); + } else { + memcpy(vtx->shade, vtx->obj_attributes.color, sizeof(GLfloat) * 4); + } + + if (state->fog) { + vtx->shade[3] = (state->fog_offset - fabsf(eye_pos[2])) * state->fog_factor; + } + + vtx->shade[0] = CLAMP01(vtx->shade[0]); + vtx->shade[1] = CLAMP01(vtx->shade[1]); + vtx->shade[2] = CLAMP01(vtx->shade[2]); + vtx->shade[3] = CLAMP01(vtx->shade[3]); + + if (state->prim_texture) { + gl_calc_texture_coords(vtx->texcoord, vtx->obj_attributes.texcoord, vtx->obj_attributes.position, eye_pos, eye_normal); + + vtx->texcoord[0] = vtx->texcoord[0] * state->prim_tex_width; + vtx->texcoord[1] = (state->tex_flip_t ? 1.f - vtx->texcoord[1] : vtx->texcoord[1]) * state->prim_tex_height; + + if (state->prim_bilinear) { + vtx->texcoord[0] -= 0.5f; + vtx->texcoord[1] -= 0.5f; + } + } + + gl_vertex_calc_screenspace(vtx); + gl_vertex_calc_clip_code(vtx); +} + +static gl_vtx_t * gl_get_screen_vtx(uint8_t cache_index) +{ + gl_vtx_t *vtx = &state->vertex_cache[cache_index]; + + if (!vtx->t_l_applied) { + // If there was a cache miss, perform T&L + gl_vertex_t_l(vtx); + vtx->t_l_applied = true; + } + + return vtx; +} + +static void gl_draw_primitive(const uint8_t *indices) +{ + uint8_t tr_codes = 0xFF; + for (uint8_t i = 0; i < state->prim_size; i++) + { + tr_codes &= state->vertex_cache[indices[i]].tr_code; + } + + // Trivial rejection + if (tr_codes) { + return; + } + + for (uint8_t i = 0; i < state->prim_size; i++) + { + state->primitive_vertices[i] = gl_get_screen_vtx(indices[i]); + #if 0 + gl_vtx_t *v = state->primitive_vertices[i]; + debugf("VTX %d:\n", i); + debugf(" cpos: (%.4f, %.4f, %.4f, %.4f) [%08lx, %08lx, %08lx, %08lx]\n", + v->cs_pos[0],v->cs_pos[1],v->cs_pos[2],v->cs_pos[3], + fx16(v->cs_pos[0]*65536), fx16(v->cs_pos[1]*65536), + fx16(v->cs_pos[2]*65536), fx16(v->cs_pos[3]*65536)); + debugf(" screen: (%.2f, %.2f) [%08lx, %08lx]\n", + v->screen_pos[0], v->screen_pos[1], + (uint32_t)(int32_t)(v->screen_pos[0] * 4), + (uint32_t)(int32_t)(v->screen_pos[1] * 4)); + if (state->prim_texture) { + debugf(" tex: (%.2f, %.2f) [%08lx, %08lx]\n", + v->texcoord[0], v->texcoord[1], + (uint32_t)(int32_t)(v->texcoord[0] * 32), + (uint32_t)(int32_t)(v->texcoord[1] * 32)); + rdpq_debug_log(true); + state->cull_face = 0; + } + #endif + } + + switch (state->prim_size) { + case 1: + gl_clip_point(); + break; + case 2: + gl_clip_line(); + break; + case 3: + gl_clip_triangle(); + break; + } +} + +static void gl_draw_point(gl_vtx_t *v0) +{ + GLfloat half_size = state->point_size * 0.5f; + GLfloat p0[2] = { v0->screen_pos[0] - half_size, v0->screen_pos[1] - half_size }; + GLfloat p1[2] = { p0[0] + state->point_size, p0[1] + state->point_size }; + + rdpq_set_prim_color(RGBA32( + FLOAT_TO_U8(v0->shade[0]), + FLOAT_TO_U8(v0->shade[1]), + FLOAT_TO_U8(v0->shade[2]), + FLOAT_TO_U8(v0->shade[3]) + )); + + if (state->depth_test) { + rdpq_set_prim_depth_raw(v0->depth * 0x7FFF, 0); + } + + if (state->prim_texture) { + rdpq_texture_rectangle_scaled(0, p0[0], p0[1], p1[0], p1[1], v0->texcoord[0]/32.f, v0->texcoord[1]/32.f, v0->texcoord[0]/32.f+1, v0->texcoord[0]/32.f+1); + } else { + rdpq_fill_rectangle(p0[0], p0[1], p1[0], p1[1]); + } +} + +static void gl_draw_line(gl_vtx_t *v0, gl_vtx_t *v1) +{ + GLfloat perp[2] = { v0->screen_pos[1] - v1->screen_pos[1], v1->screen_pos[0] - v0->screen_pos[0] }; + GLfloat mag = sqrtf(perp[0]*perp[0] + perp[1]*perp[1]); + if (mag == 0.0f) return; + + GLfloat width_factor = (state->line_width * 0.5f) / mag; + perp[0] *= width_factor; + perp[1] *= width_factor; + + gl_vtx_t line_vertices[4]; + + line_vertices[0].screen_pos[0] = v0->screen_pos[0] + perp[0]; + line_vertices[0].screen_pos[1] = v0->screen_pos[1] + perp[1]; + line_vertices[1].screen_pos[0] = v0->screen_pos[0] - perp[0]; + line_vertices[1].screen_pos[1] = v0->screen_pos[1] - perp[1]; + + line_vertices[2].screen_pos[0] = v1->screen_pos[0] + perp[0]; + line_vertices[2].screen_pos[1] = v1->screen_pos[1] + perp[1]; + line_vertices[3].screen_pos[0] = v1->screen_pos[0] - perp[0]; + line_vertices[3].screen_pos[1] = v1->screen_pos[1] - perp[1]; + + if (state->shade_model == GL_FLAT) { + memcpy(line_vertices[0].shade, v1->shade, sizeof(float) * 4); + memcpy(line_vertices[1].shade, v1->shade, sizeof(float) * 4); + } else { + memcpy(line_vertices[0].shade, v0->shade, sizeof(float) * 4); + memcpy(line_vertices[1].shade, v0->shade, sizeof(float) * 4); + } + + memcpy(line_vertices[2].shade, v1->shade, sizeof(float) * 4); + memcpy(line_vertices[3].shade, v1->shade, sizeof(float) * 4); + + if (state->prim_texture) { + memcpy(line_vertices[0].texcoord, v0->texcoord, sizeof(float) * 3); + memcpy(line_vertices[1].texcoord, v0->texcoord, sizeof(float) * 3); + memcpy(line_vertices[2].texcoord, v1->texcoord, sizeof(float) * 3); + memcpy(line_vertices[3].texcoord, v1->texcoord, sizeof(float) * 3); + } + + if (state->depth_test) { + line_vertices[0].depth = v0->depth; + line_vertices[1].depth = v0->depth; + line_vertices[2].depth = v1->depth; + line_vertices[3].depth = v1->depth; + } + + rdpq_triangle(&state->trifmt, (const float*)&line_vertices[0], (const float*)&line_vertices[1], (const float*)&line_vertices[2]); + rdpq_triangle(&state->trifmt, (const float*)&line_vertices[1], (const float*)&line_vertices[2], (const float*)&line_vertices[3]); +} + +static void gl_draw_triangle(gl_vtx_t *v0, gl_vtx_t *v1, gl_vtx_t *v2) +{ + rdpq_triangle(&state->trifmt, (const float*)v2, (const float*)v0, (const float*)v1); +} + +static void gl_cull_triangle(gl_vtx_t *v0, gl_vtx_t *v1, gl_vtx_t *v2) +{ + if (state->cull_face) + { + if (state->cull_face_mode == GL_FRONT_AND_BACK) { + return; + } + + float winding = v0->screen_pos[0] * (v1->screen_pos[1] - v2->screen_pos[1]) + + v1->screen_pos[0] * (v2->screen_pos[1] - v0->screen_pos[1]) + + v2->screen_pos[0] * (v0->screen_pos[1] - v1->screen_pos[1]); + + bool is_front = (state->front_face == GL_CCW) ^ (winding > 0.0f); + GLenum face = is_front ? GL_FRONT : GL_BACK; + + if (state->cull_face_mode == face) { + return; + } + } + + if (state->shade_model == GL_FLAT) { + memcpy(v2->shade, state->flat_color, sizeof(state->flat_color)); + } + + switch (state->polygon_mode) { + case GL_POINT: + gl_draw_point(v0); + gl_draw_point(v1); + gl_draw_point(v2); + break; + case GL_LINE: + gl_draw_line(v0, v1); + gl_draw_line(v1, v2); + gl_draw_line(v2, v0); + break; + case GL_FILL: + gl_draw_triangle(v0, v1, v2); + break; + } +} + +static void gl_intersect_line_plane(gl_vtx_t *intersection, const gl_vtx_t *p0, const gl_vtx_t *p1, const float *clip_plane) +{ + float d0 = dot_product4(p0->cs_pos, clip_plane); + float d1 = dot_product4(p1->cs_pos, clip_plane); + + float a = d0 / (d0 - d1); + + assertf(a >= 0.f && a <= 1.f, "invalid a: %f", a); + + intersection->cs_pos[0] = lerp(p0->cs_pos[0], p1->cs_pos[0], a); + intersection->cs_pos[1] = lerp(p0->cs_pos[1], p1->cs_pos[1], a); + intersection->cs_pos[2] = lerp(p0->cs_pos[2], p1->cs_pos[2], a); + intersection->cs_pos[3] = lerp(p0->cs_pos[3], p1->cs_pos[3], a); + + intersection->shade[0] = lerp(p0->shade[0], p1->shade[0], a); + intersection->shade[1] = lerp(p0->shade[1], p1->shade[1], a); + intersection->shade[2] = lerp(p0->shade[2], p1->shade[2], a); + intersection->shade[3] = lerp(p0->shade[3], p1->shade[3], a); + + intersection->texcoord[0] = lerp(p0->texcoord[0], p1->texcoord[0], a); + intersection->texcoord[1] = lerp(p0->texcoord[1], p1->texcoord[1], a); + + gl_vertex_calc_clip_code(intersection); +} + +static void gl_clip_triangle() +{ + gl_vtx_t *v0 = state->primitive_vertices[0]; + gl_vtx_t *v1 = state->primitive_vertices[1]; + gl_vtx_t *v2 = state->primitive_vertices[2]; + + // Flat shading + if (state->shade_model == GL_FLAT) { + memcpy(state->flat_color, v2->shade, sizeof(state->flat_color)); + } + + uint8_t any_clip = v0->clip_code | v1->clip_code | v2->clip_code; + + if (!any_clip) { + gl_cull_triangle(v0, v1, v2); + return; + } + + // Polygon clipping using the Sutherland-Hodgman algorithm + // See https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm + + // Intersection points are stored in the clipping cache + gl_vtx_t clipping_cache[CLIPPING_CACHE_SIZE]; + uint32_t cache_used = 0; + + gl_clipping_list_t lists[2]; + + gl_clipping_list_t *in_list = &lists[0]; + gl_clipping_list_t *out_list = &lists[1]; + + out_list->vertices[0] = v0; + out_list->vertices[1] = v1; + out_list->vertices[2] = v2; + out_list->count = 3; + + for (uint32_t c = 0; c < CLIPPING_PLANE_COUNT; c++) + { + // If nothing clips this plane, skip it entirely + if ((any_clip & (1<count = 0; + + for (uint32_t i = 0; i < in_list->count; i++) + { + uint32_t prev_index = (i + in_list->count - 1) % in_list->count; + + gl_vtx_t *cur_point = in_list->vertices[i]; + gl_vtx_t *prev_point = in_list->vertices[prev_index]; + + bool cur_inside = (cur_point->clip_code & (1<clip_code & (1<vertices[out_list->count] = intersection; + out_list->count++; + } + + if (cur_inside) { + out_list->vertices[out_list->count] = cur_point; + out_list->count++; + } else { + // If the point is in the clipping cache, remember it as unused + uint32_t diff = cur_point - clipping_cache; + if (diff >= 0 && diff < CLIPPING_CACHE_SIZE) { + cache_used &= ~(1<count; i++) + { + gl_vertex_calc_screenspace(out_list->vertices[i]); + + if (i > 1) { + gl_cull_triangle(out_list->vertices[0], out_list->vertices[i-1], out_list->vertices[i]); + } + } +} + +static void gl_clip_line() +{ + gl_vtx_t *v0 = state->primitive_vertices[0]; + gl_vtx_t *v1 = state->primitive_vertices[1]; + gl_vtx_t vertex_cache[2]; + + uint8_t any_clip = v0->clip_code | v1->clip_code; + + uint8_t clipped = 0; + if (any_clip) { + + for (uint32_t c = 0; c < CLIPPING_PLANE_COUNT; c++) + { + // If nothing clips this plane, skip it entirely + if ((any_clip & (1<clip_code & (1<clip_code & (1<primitive_vertices[0]; + gl_draw_point(v0); +} + +static void submit_vertex(uint32_t cache_index) +{ + uint8_t indices[3]; + if (gl_prim_assembly(cache_index, indices)) + { + gl_draw_primitive(indices); + } +} + +static void draw_vertex_from_arrays(const gl_array_t *arrays, uint32_t id, uint32_t index) +{ + uint8_t cache_index; + if (gl_get_cache_index(id, &cache_index)) + { + gl_load_attribs(arrays, index); + gl_vertex_pre_tr(cache_index); + } + + submit_vertex(cache_index); +} + +static void gl_cpu_begin() +{ + gl_init_cpu_pipe(); +} + +static void gl_cpu_end() +{ + if (state->primitive_mode == GL_LINE_LOOP) { + // Close line loop + state->prim_indices[0] = state->prim_indices[1]; + state->prim_indices[1] = state->locked_vertex; + + gl_draw_primitive(state->prim_indices); + } + + gl_set_current_color(state->current_attributes.color); + gl_set_current_texcoords(state->current_attributes.texcoord); + gl_set_current_normal(state->current_attributes.normal); + gl_set_current_mtx_index(state->current_attributes.mtx_index); +} + +void gl_read_attrib(gl_array_type_t array_type, const void *value, GLenum type, uint32_t size) +{ + cpu_read_attrib_func read_func = cpu_read_funcs[array_type][gl_type_to_index(type)]; + void *dst = gl_get_attrib_pointer(&state->current_attributes, array_type); + read_func(dst, value, size); + if (array_type != ATTRIB_MTX_INDEX) { + gl_fill_attrib_defaults(array_type, size); + } +} + +static void gl_cpu_vertex(const void *value, GLenum type, uint32_t size) +{ + uint8_t cache_index; + if (gl_get_cache_index(next_prim_id(), &cache_index)) { + + gl_fill_attrib_defaults(ATTRIB_VERTEX, size); + gl_read_attrib(ATTRIB_VERTEX, value, type, size); + gl_vertex_pre_tr(cache_index); + } + + submit_vertex(cache_index); +} + +static void gl_cpu_color(const void *value, GLenum type, uint32_t size) +{ + gl_read_attrib(ATTRIB_COLOR, value, type, size); +} + +static void gl_cpu_tex_coord(const void *value, GLenum type, uint32_t size) +{ + gl_read_attrib(ATTRIB_TEXCOORD, value, type, size); +} + +static void gl_cpu_normal(const void *value, GLenum type, uint32_t size) +{ + gl_read_attrib(ATTRIB_NORMAL, value, type, size); +} + +static void gl_cpu_mtx_index(const void *value, GLenum type, uint32_t size) +{ + gl_read_attrib(ATTRIB_MTX_INDEX, value, type, size); +} + +static void gl_cpu_array_element(uint32_t index) +{ + gl_fill_all_attrib_defaults(state->array_object->arrays); + draw_vertex_from_arrays(state->array_object->arrays, index, index); +} + +static void gl_cpu_draw_arrays(uint32_t first, uint32_t count) +{ + gl_fill_all_attrib_defaults(state->array_object->arrays); + + if (state->array_object->arrays[ATTRIB_VERTEX].enabled) { + for (uint32_t i = 0; i < count; i++) + { + draw_vertex_from_arrays(state->array_object->arrays, next_prim_id(), first + i); + } + } else { + // If the vertex array is disabled, nothing is drawn. However, all other attributes are still applied. + // So in effect, we just need to load the last set of attributes. + gl_load_attribs(state->array_object->arrays, first + count - 1); + } +} + +static void gl_cpu_draw_elements(uint32_t count, const void* indices, read_index_func read_index) +{ + gl_fill_all_attrib_defaults(state->array_object->arrays); + + if (state->array_object->arrays[ATTRIB_VERTEX].enabled) { + for (uint32_t i = 0; i < count; i++) + { + uint32_t index = read_index(indices, i); + draw_vertex_from_arrays(state->array_object->arrays, index, index); + } + } else { + // If the vertex array is disabled, nothing is drawn. However, all other attributes are still applied. + // So in effect, we just need to load the last set of attributes. + gl_load_attribs(state->array_object->arrays, read_index(indices, count - 1)); + } +} + +const gl_pipeline_t gl_cpu_pipeline = (gl_pipeline_t) { + .begin = gl_cpu_begin, + .end = gl_cpu_end, + .vertex = gl_cpu_vertex, + .color = gl_cpu_color, + .tex_coord = gl_cpu_tex_coord, + .normal = gl_cpu_normal, + .mtx_index = gl_cpu_mtx_index, + .array_element = gl_cpu_array_element, + .draw_arrays = gl_cpu_draw_arrays, + .draw_elements = gl_cpu_draw_elements, +}; diff --git a/src/GL/gl.c b/src/GL/gl.c new file mode 100644 index 000000000..249eb72ad --- /dev/null +++ b/src/GL/gl.c @@ -0,0 +1,557 @@ +/** + * @file gl.c + * @author Dennis Heinze + * @brief OpenGL core initialization and state management. + */ +#include "GL/gl.h" +#include "rdpq.h" +#include "rdpq_rect.h" +#include "rdpq_mode.h" +#include "rdpq_debug.h" +#include "rspq.h" +#include "display.h" +#include "rdp.h" +#include "utils.h" +#include +#include +#include +#include "gl_internal.h" + +DEFINE_RSP_UCODE(rsp_gl); +DEFINE_RSP_UCODE(rsp_gl_pipeline); + +uint32_t gl_overlay_id; +uint32_t glp_overlay_id; +uint32_t gl_rsp_state; + +gl_state_t *state; + +uint32_t gl_get_type_size(GLenum type) +{ + switch (type) { + case GL_BYTE: + return sizeof(GLbyte); + case GL_UNSIGNED_BYTE: + return sizeof(GLubyte); + case GL_SHORT: + return sizeof(GLshort); + case GL_UNSIGNED_SHORT: + return sizeof(GLushort); + case GL_INT: + return sizeof(GLint); + case GL_UNSIGNED_INT: + return sizeof(GLuint); + case GL_FLOAT: + return sizeof(GLfloat); + case GL_DOUBLE: + return sizeof(GLdouble); + case GL_HALF_FIXED_N64: + return sizeof(GLhalfxN64); + default: + return 0; + } +} + +void gl_init() +{ + rdpq_init(); + + state = calloc(1, sizeof(gl_state_t)); + assertf(state, "Out of memory"); + + gl_texture_init(); + + gl_server_state_t *server_state = UncachedAddr(rspq_overlay_get_state(&rsp_gl)); + memset(server_state, 0, sizeof(gl_server_state_t)); + + memcpy(&server_state->bound_textures[0], state->default_textures[0].srv_object, sizeof(gl_srv_texture_object_t)); + memcpy(&server_state->bound_textures[1], state->default_textures[1].srv_object, sizeof(gl_srv_texture_object_t)); + server_state->texture_ids[0] = PhysicalAddr(state->default_textures[0].srv_object); + server_state->texture_ids[1] = PhysicalAddr(state->default_textures[1].srv_object); + + server_state->color[0] = 0x7FFF; + server_state->color[1] = 0x7FFF; + server_state->color[2] = 0x7FFF; + server_state->color[3] = 0x7FFF; + server_state->tex_coords[3] = 1 << 5; + server_state->normal[2] = 0x7F; + + server_state->point_size = 1 << 2; + server_state->line_width = 1 << 2; + server_state->polygon_mode = GL_FILL; + + server_state->tex_gen.mode[0] = GL_EYE_LINEAR; + server_state->tex_gen.mode[1] = GL_EYE_LINEAR; + server_state->tex_gen.mode[2] = GL_EYE_LINEAR; + server_state->tex_gen.mode[3] = GL_EYE_LINEAR; + + server_state->tex_gen.mode_const[0] = GL_OBJECT_LINEAR; + server_state->tex_gen.mode_const[1] = GL_EYE_LINEAR; + server_state->tex_gen.mode_const[2] = GL_SPHERE_MAP; + + server_state->tex_gen.integer[0][0][0] = 1; + server_state->tex_gen.integer[0][1][0] = 1; + + server_state->tex_gen.integer[1][0][1] = 1; + server_state->tex_gen.integer[1][1][1] = 1; + + state->matrix_stacks[0] = malloc_uncached(sizeof(gl_matrix_srv_t) * MODELVIEW_STACK_SIZE); + state->matrix_stacks[1] = malloc_uncached(sizeof(gl_matrix_srv_t) * PROJECTION_STACK_SIZE); + state->matrix_stacks[2] = malloc_uncached(sizeof(gl_matrix_srv_t) * TEXTURE_STACK_SIZE); + state->matrix_palette = malloc_uncached(sizeof(gl_matrix_srv_t) * MATRIX_PALETTE_SIZE * 2); // Double size for mvp-matrices + assertf(state->matrix_stacks[0] && state->matrix_stacks[1] && state->matrix_stacks[2] && state->matrix_palette, + "Out of memory"); + + server_state->matrix_pointers[0] = PhysicalAddr(state->matrix_stacks[0]); + server_state->matrix_pointers[1] = PhysicalAddr(state->matrix_stacks[1]); + server_state->matrix_pointers[2] = PhysicalAddr(state->matrix_stacks[2]); + server_state->matrix_pointers[3] = PhysicalAddr(state->matrix_palette); + server_state->matrix_pointers[4] = PhysicalAddr(state->matrix_palette + MATRIX_PALETTE_SIZE); + server_state->loaded_mtx_index[0] = -1; + server_state->loaded_mtx_index[1] = -1; + + server_state->flags |= FLAG_FINAL_MTX_DIRTY; + + server_state->mat_ambient[0] = 0x1999; // 0.2 + server_state->mat_ambient[1] = 0x1999; // 0.2 + server_state->mat_ambient[2] = 0x1999; // 0.2 + server_state->mat_ambient[3] = 0x7FFF; // 1.0 + server_state->mat_diffuse[0] = 0x6666; // 0.8 + server_state->mat_diffuse[1] = 0x6666; // 0.8 + server_state->mat_diffuse[2] = 0x6666; // 0.8 + server_state->mat_diffuse[3] = 0x7FFF; // 1.0 + server_state->mat_specular[3] = 0x7FFF; // 1.0 + server_state->mat_emissive[3] = 0x7FFF; // 1.0 + server_state->mat_color_target[0] = 1; + server_state->mat_color_target[1] = 1; + + for (uint32_t i = 0; i < LIGHT_COUNT; i++) + { + server_state->lights.position[i][2] = 0x7FFF; // 1.0 + server_state->lights.ambient[i][3] = 0x7FFF; // 1.0 + server_state->lights.diffuse[i][3] = 0x7FFF; // 1.0 + server_state->lights.attenuation_frac[i][0] = 1 << 15; // 1.0 + } + + server_state->light_ambient[0] = 0x1999; // 0.2 + server_state->light_ambient[1] = 0x1999; // 0.2 + server_state->light_ambient[2] = 0x1999; // 0.2 + server_state->light_ambient[3] = 0x7FFF; // 1.0 + + server_state->dither_mode = DITHER_SQUARE_SQUARE << (SOM_ALPHADITHER_SHIFT - 32); + + gl_overlay_id = rspq_overlay_register(&rsp_gl); + glp_overlay_id = rspq_overlay_register(&rsp_gl_pipeline); + gl_rsp_state = PhysicalAddr(rspq_overlay_get_state(&rsp_gl)); + + gl_matrix_init(); + gl_lighting_init(); + gl_rendermode_init(); + gl_array_init(); + gl_primitive_init(); + gl_pixel_init(); + gl_list_init(); + + glDepthRange(0, 1); + glClearDepth(1.0); + glCullFace(GL_BACK); + glFrontFace(GL_CCW); +} + +void gl_close() +{ + rspq_wait(); + + gl_list_close(); + gl_primitive_close(); + gl_texture_close(); + rspq_overlay_unregister(gl_overlay_id); + rspq_overlay_unregister(glp_overlay_id); + + // FIXME: some of the above do deferred deletions, others don't. + // So we need another rspq_wait. + rspq_wait(); + + free_uncached(state->matrix_stacks[0]); + free_uncached(state->matrix_stacks[1]); + free_uncached(state->matrix_stacks[2]); + free_uncached(state->matrix_palette); + + free(state); +} + +void gl_reset_uploaded_texture() +{ + gl_set_word(GL_UPDATE_NONE, offsetof(gl_server_state_t, uploaded_tex), 0); +} + +void gl_context_begin() +{ + const surface_t *old_color_buffer = state->color_buffer; + + state->color_buffer = rdpq_get_attached(); + assertf(state->color_buffer, "GL: Tried to begin rendering without framebuffer attached"); + + uint32_t width = state->color_buffer->width; + uint32_t height = state->color_buffer->height; + + if (old_color_buffer == NULL || old_color_buffer->width != width || old_color_buffer->height != height) { + uint32_t packed_size = ((uint32_t)width) << 16 | (uint32_t)height; + gl_set_word(GL_UPDATE_NONE, offsetof(gl_server_state_t, fb_size), packed_size); + glViewport(0, 0, width, height); + glScissor(0, 0, width, height); + } + + gl_reset_uploaded_texture(); +} + +void gl_context_end() +{ +} + +GLenum glGetError(void) +{ + if (!gl_ensure_no_begin_end()) return 0; + + GLenum error = state->current_error; + state->current_error = GL_NO_ERROR; + return error; +} + +void gl_set_flag2(GLenum target, bool value) +{ + switch (target) { + case GL_RDPQ_MATERIAL_N64: + gl_set_flag_word2(GL_UPDATE_NONE, FLAG2_USE_RDPQ_MATERIAL, value); + break; + case GL_RDPQ_TEXTURING_N64: + gl_set_flag_word2(GL_UPDATE_NONE, FLAG2_USE_RDPQ_TEXTURING, value); + gl_reset_uploaded_texture(); + break; + case GL_SCISSOR_TEST: + gl_set_flag(GL_UPDATE_SCISSOR, FLAG_SCISSOR_TEST, value); + break; + case GL_DEPTH_TEST: + gl_set_flag(GL_UPDATE_NONE, FLAG_DEPTH_TEST, value); + state->depth_test = value; + break; + case GL_BLEND: + gl_set_flag(GL_UPDATE_NONE, FLAG_BLEND, value); + break; + case GL_ALPHA_TEST: + gl_set_flag(GL_UPDATE_NONE, FLAG_ALPHA_TEST, value); + break; + case GL_DITHER: + gl_set_flag(GL_UPDATE_NONE, FLAG_DITHER, value); + break; + case GL_FOG: + gl_set_flag(GL_UPDATE_NONE, FLAG_FOG, value); + state->fog = value; + break; + case GL_MULTISAMPLE_ARB: + gl_set_flag_word2(GL_UPDATE_NONE, FLAG2_MULTISAMPLE, value); + break; + case GL_TEXTURE_1D: + gl_set_flag(GL_UPDATE_NONE, FLAG_TEXTURE_1D, value); + state->texture_1d = value; + break; + case GL_TEXTURE_2D: + gl_set_flag(GL_UPDATE_NONE, FLAG_TEXTURE_2D, value); + state->texture_2d = value; + break; + case GL_CULL_FACE: + gl_set_flag(GL_UPDATE_NONE, FLAG_CULL_FACE, value); + state->cull_face = value; + break; + case GL_LIGHTING: + gl_set_flag(GL_UPDATE_NONE, FLAG_LIGHTING, value); + state->lighting = value; + set_can_use_rsp_dirty(); + break; + case GL_LIGHT0: + case GL_LIGHT1: + case GL_LIGHT2: + case GL_LIGHT3: + case GL_LIGHT4: + case GL_LIGHT5: + case GL_LIGHT6: + case GL_LIGHT7: + uint32_t light_index = target - GL_LIGHT0; + gl_set_flag(GL_UPDATE_NONE, FLAG_LIGHT0 << light_index, value); + state->lights[light_index].enabled = value; + break; + case GL_COLOR_MATERIAL: + gl_set_flag(GL_UPDATE_NONE, FLAG_COLOR_MATERIAL, value); + state->color_material = value; + break; + case GL_TEXTURE_GEN_S: + case GL_TEXTURE_GEN_T: + case GL_TEXTURE_GEN_R: + case GL_TEXTURE_GEN_Q: + uint32_t tex_gen_index = target - GL_TEXTURE_GEN_S; + gl_set_flag(GL_UPDATE_NONE, FLAG_TEX_GEN_S << tex_gen_index, value); + state->tex_gen[tex_gen_index].enabled = value; + set_can_use_rsp_dirty(); + break; + case GL_NORMALIZE: + gl_set_flag(GL_UPDATE_NONE, FLAG_NORMALIZE, value); + state->normalize = value; + break; + case GL_MATRIX_PALETTE_ARB: + gl_set_flag(GL_UPDATE_NONE, FLAG_MATRIX_PALETTE, value); + state->matrix_palette_enabled = value; + break; + case GL_TEXTURE_FLIP_T_N64: + gl_set_flag_word2(GL_UPDATE_NONE, FLAG2_TEX_FLIP_T, value); + state->tex_flip_t = value; + break; + case GL_CLIP_PLANE0: + case GL_CLIP_PLANE1: + case GL_CLIP_PLANE2: + case GL_CLIP_PLANE3: + case GL_CLIP_PLANE4: + case GL_CLIP_PLANE5: + assertf(!value, "User clip planes are not supported!"); + break; + case GL_STENCIL_TEST: + assertf(!value, "Stencil test is not supported!"); + break; + case GL_COLOR_LOGIC_OP: + case GL_INDEX_LOGIC_OP: + assertf(!value, "Logical pixel operation is not supported!"); + break; + case GL_POINT_SMOOTH: + case GL_LINE_SMOOTH: + case GL_POLYGON_SMOOTH: + assertf(!value, "Smooth rendering is not supported (Use multisampling instead)!"); + break; + case GL_LINE_STIPPLE: + case GL_POLYGON_STIPPLE: + assertf(!value, "Stipple is not supported!"); + break; + case GL_POLYGON_OFFSET_FILL: + case GL_POLYGON_OFFSET_LINE: + case GL_POLYGON_OFFSET_POINT: + assertf(!value, "Polygon offset is not supported!"); + break; + case GL_SAMPLE_ALPHA_TO_COVERAGE_ARB: + case GL_SAMPLE_ALPHA_TO_ONE_ARB: + case GL_SAMPLE_COVERAGE_ARB: + assertf(!value, "Coverage value manipulation is not supported!"); + break; + case GL_MAP1_COLOR_4: + case GL_MAP1_INDEX: + case GL_MAP1_NORMAL: + case GL_MAP1_TEXTURE_COORD_1: + case GL_MAP1_TEXTURE_COORD_2: + case GL_MAP1_TEXTURE_COORD_3: + case GL_MAP1_TEXTURE_COORD_4: + case GL_MAP1_VERTEX_3: + case GL_MAP1_VERTEX_4: + case GL_MAP2_COLOR_4: + case GL_MAP2_INDEX: + case GL_MAP2_NORMAL: + case GL_MAP2_TEXTURE_COORD_1: + case GL_MAP2_TEXTURE_COORD_2: + case GL_MAP2_TEXTURE_COORD_3: + case GL_MAP2_TEXTURE_COORD_4: + case GL_MAP2_VERTEX_3: + case GL_MAP2_VERTEX_4: + assertf(!value, "Evaluators are not supported!"); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid enable target", target); + return; + } +} + +void glEnable(GLenum target) +{ + if (!gl_ensure_no_begin_end()) return; + gl_set_flag2(target, true); +} + +void glDisable(GLenum target) +{ + if (!gl_ensure_no_begin_end()) return; + gl_set_flag2(target, false); +} + +void gl_copy_fill_color(uint32_t offset) +{ + __rdpq_autosync_change(AUTOSYNC_PIPE); + gl_write_rdp(1, GL_CMD_COPY_FILL_COLOR, offset); +} + +void glClear(GLbitfield buf) +{ + extern void __rdpq_set_mode_fill(void); + extern void __rdpq_clear_z(const uint16_t *z); + extern void __rdpq_clear(const color_t* color); + + if (!gl_ensure_no_begin_end()) return; + + if (!buf) { + return; + } + + rdpq_mode_push(); + __rdpq_set_mode_fill(); + + if (buf & (GL_STENCIL_BUFFER_BIT | GL_ACCUM_BUFFER_BIT)) { + assertf(0, "Only color and depth buffers are supported!"); + } + + if (buf & GL_DEPTH_BUFFER_BIT) { + gl_copy_fill_color(offsetof(gl_server_state_t, clear_depth)); + __rdpq_clear_z(NULL); + } + + if (buf & GL_COLOR_BUFFER_BIT) { + gl_copy_fill_color(offsetof(gl_server_state_t, clear_color)); + __rdpq_clear(NULL); + } + + rdpq_mode_pop(); +} + +void glClearColor(GLclampf r, GLclampf g, GLclampf b, GLclampf a) +{ + if (!gl_ensure_no_begin_end()) return; + + color_t clear_color = RGBA32(CLAMPF_TO_U8(r), CLAMPF_TO_U8(g), CLAMPF_TO_U8(b), CLAMPF_TO_U8(a)); + gl_set_word(GL_UPDATE_NONE, offsetof(gl_server_state_t, clear_color), color_to_packed32(clear_color)); +} + +void glClearDepth(GLclampd d) +{ + if (!gl_ensure_no_begin_end()) return; + + color_t clear_depth = color_from_packed16(ZBUF_VAL(d)); + gl_set_word(GL_UPDATE_NONE, offsetof(gl_server_state_t, clear_depth), color_to_packed32(clear_depth)); +} + +void glDitherModeN64(rdpq_dither_t mode){ + gl_set_word(GL_UPDATE_NONE, offsetof(gl_server_state_t, dither_mode), mode << (SOM_ALPHADITHER_SHIFT - 32)); +} + +void glFlush(void) +{ + if (!gl_ensure_no_begin_end()) return; + + rspq_flush(); +} + +void glFinish(void) +{ + if (!gl_ensure_no_begin_end()) return; + + rspq_wait(); +} + +void glHint(GLenum target, GLenum hint) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (target) + { + case GL_PERSPECTIVE_CORRECTION_HINT: + // TODO: enable/disable texture perspective correction? + break; + case GL_FOG_HINT: + // TODO: per-pixel fog + break; + case GL_MULTISAMPLE_HINT_N64: + // Use full AA by default, unless RA has been requested + gl_set_flag_word2(GL_UPDATE_NONE, FLAG2_REDUCED_ALIASING, hint == GL_FASTEST); + break; + case GL_POINT_SMOOTH_HINT: + case GL_LINE_SMOOTH_HINT: + case GL_POLYGON_SMOOTH_HINT: + // Ignored + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid hint target", target); + break; + } +} + +bool gl_storage_alloc(gl_storage_t *storage, uint32_t size) +{ + GLvoid *mem = malloc_uncached(size); + if (mem == NULL) { + return false; + } + + storage->data = mem; + storage->size = size; + + return true; +} + +void gl_storage_free(gl_storage_t *storage) +{ + // TODO: need to wait until buffer is no longer used! + + if (storage->data != NULL) { + free(storage->data); + storage->data = NULL; + } +} + +bool gl_storage_resize(gl_storage_t *storage, uint32_t new_size) +{ + if (storage->size >= new_size) { + return true; + } + + GLvoid *mem = malloc(new_size); + if (mem == NULL) { + return false; + } + + gl_storage_free(storage); + + storage->data = mem; + storage->size = new_size; + + return true; +} + +void glTexSizeN64(GLushort width, GLushort height) +{ + width <<= TEX_COORD_SHIFT; + height <<= TEX_COORD_SHIFT; + gl_set_word(GL_UPDATE_NONE, offsetof(gl_server_state_t, tex_size[0]), (width << 16) | height); +} + +extern inline uint32_t next_pow2(uint32_t v); +extern inline bool is_in_heap_memory(void *ptr); +extern inline void gl_set_flag_raw(gl_update_func_t update_func, uint32_t offset, uint32_t flag, bool value); +extern inline void gl_set_flag(gl_update_func_t update_func, uint32_t flag, bool value); +extern inline void gl_set_flag_word2(gl_update_func_t update_func, uint32_t flag, bool value); +extern inline void gl_set_byte(gl_update_func_t update_func, uint32_t offset, uint8_t value); +extern inline void gl_set_short(gl_update_func_t update_func, uint32_t offset, uint16_t value); +extern inline void gl_set_word(gl_update_func_t update_func, uint32_t offset, uint32_t value); +extern inline void gl_set_long(gl_update_func_t update_func, uint32_t offset, uint64_t value); +extern inline void gl_update(gl_update_func_t update_func); +extern inline void gl_get_value(void *dst, uint32_t offset, uint32_t size); +extern inline void gl_bind_texture(GLenum target, gl_texture_object_t *texture); +extern inline void gl_update_texture_completeness(uint32_t offset); +extern inline void gl_set_palette_idx(uint32_t index); +extern inline void gl_set_current_color(GLfloat *color); +extern inline void gl_set_current_texcoords(GLfloat *texcoords); +extern inline void gl_set_current_normal(GLfloat *normal); +extern inline void gl_pre_init_pipe(GLenum primitive_mode); +extern inline void glpipe_init(); +extern inline void glpipe_draw_triangle(int i0, int i1, int i2); +extern inline int gl_get_rdpcmds_for_update_func(gl_update_func_t update_func); +extern inline void* gl_get_attrib_pointer(gl_obj_attributes_t *attribs, gl_array_type_t array_type); +extern inline uint32_t gl_type_to_index(GLenum type); +extern inline void gl_set_current_mtx_index(GLubyte *index); +extern inline gl_cmd_stream_t gl_cmd_stream_begin(uint32_t ovl_id, uint32_t cmd_id, int size); +extern inline void gl_cmd_stream_commit(gl_cmd_stream_t *s); +extern inline void gl_cmd_stream_put_byte(gl_cmd_stream_t *s, uint8_t v); +extern inline void gl_cmd_stream_put_half(gl_cmd_stream_t *s, uint16_t v); +extern inline void gl_cmd_stream_end(gl_cmd_stream_t *s); +extern inline void glpipe_set_vtx_cmd_size(uint16_t patched_cmd_descriptor, uint16_t *cmd_descriptor); diff --git a/src/GL/gl_constants.h b/src/GL/gl_constants.h new file mode 100644 index 000000000..131f0b1c7 --- /dev/null +++ b/src/GL/gl_constants.h @@ -0,0 +1,192 @@ +/** + * @file gl_constants.h + * @author Dennis Heinze + */ +#ifndef __GL_CONSTANTS +#define __GL_CONSTANTS + +#define MODELVIEW_STACK_SIZE 32 +#define PROJECTION_STACK_SIZE 2 +#define TEXTURE_STACK_SIZE 2 +#define PALETTE_STACK_SIZE 1 + +#define VERTEX_UNIT_COUNT 1 +#define MATRIX_PALETTE_SIZE 32 + +#define VERTEX_CACHE_SIZE 32 + +#define CLIPPING_PLANE_COUNT 6 +#define CLIPPING_CACHE_SIZE 9 +#define CLIPPING_PLANE_SIZE 8 + +#define MATRIX_SIZE 64 + +#define TEX_COORD_COUNT 4 +#define TEX_GEN_COUNT TEX_COORD_COUNT +#define TEX_GEN_PLANE_COUNT 2 +#define TEX_GEN_SIZE 34 + +#define TEX_GEN_STRUCT_SIZE 144 +#define TEX_GEN_INTEGER_OFFSET 0 +#define TEX_GEN_FRACTION_OFFSET 64 +#define TEX_GEN_MODE_OFFSET 128 +#define TEX_GEN_CONST_SIZE (4*2) + +#define LIGHT_COUNT 8 +#define LIGHT_ATTR_SIZE 8 +#define LIGHT_ATTR_ARRAY_SIZE (LIGHT_COUNT*LIGHT_ATTR_SIZE) +#define LIGHT_STRUCT_SIZE (LIGHT_ATTR_ARRAY_SIZE*5) + +#define LIGHT_POSITION_OFFSET (LIGHT_ATTR_ARRAY_SIZE*0) +#define LIGHT_AMBIENT_OFFSET (LIGHT_ATTR_ARRAY_SIZE*1) +#define LIGHT_DIFFUSE_OFFSET (LIGHT_ATTR_ARRAY_SIZE*2) +#define LIGHT_ATTENUATION_INT_OFFSET (LIGHT_ATTR_ARRAY_SIZE*3) +#define LIGHT_ATTENUATION_FRAC_OFFSET (LIGHT_ATTR_ARRAY_SIZE*4) + +#define MAX_TEXTURE_SIZE 64 +#define MAX_TEXTURE_LEVELS 7 + +#define TEXTURE_IMAGE_SIZE 6 +#define TEXTURE_OBJECT_PROPS_OFFSET (TEXTURE_IMAGE_SIZE * MAX_TEXTURE_LEVELS) + +#define TEXTURE_LEVELS_COUNT_OFFSET (TEXTURE_OBJECT_PROPS_OFFSET + 0) +#define TEXTURE_TLUT_MODE_OFFSET (TEXTURE_OBJECT_PROPS_OFFSET + 1) +#define TEXTURE_LEVELS_BLOCK_OFFSET (TEXTURE_OBJECT_PROPS_OFFSET + 2) +#define TEXTURE_FLAGS_OFFSET (TEXTURE_OBJECT_PROPS_OFFSET + 62) +#define TEXTURE_MIN_FILTER_OFFSET (TEXTURE_OBJECT_PROPS_OFFSET + 66) +#define TEXTURE_MAG_FILTER_OFFSET (TEXTURE_OBJECT_PROPS_OFFSET + 68) + +#define TEXTURE_OBJECT_SIZE (TEXTURE_OBJECT_PROPS_OFFSET + 70) + +#define IMAGE_WIDTH_OFFSET 0 +#define IMAGE_HEIGHT_OFFSET 2 +#define IMAGE_INTERNAL_FORMAT_OFFSET 4 + +#define TEXTURE_BILINEAR_MASK 0x001 +#define TEXTURE_INTERPOLATE_MASK 0x002 +#define TEXTURE_MIPMAP_MASK 0x100 + +#define MAX_PIXEL_MAP_SIZE 32 + +#define ATTRIB_TYPE_COUNT 9 + +#define VTX_SHIFT 5 +#define TEX_SHIFT 8 + +#define FLAG_DITHER (1 << 0) +#define FLAG_BLEND (1 << 1) +#define FLAG_ALPHA_TEST (1 << 2) +#define FLAG_FOG (1 << 3) +#define FLAG_SCISSOR_TEST (1 << 4) +#define FLAG_TEXTURE_1D (1 << 5) +#define FLAG_TEXTURE_2D (1 << 6) +#define FLAG_CULL_FACE (1 << 7) +#define FLAG_TEX_GEN_LINEAR (1 << 8) +#define FLAG_TEX_GEN_SPHERICAL (1 << 9) +#define FLAG_LIGHTING (1 << 10) +#define FLAG_COLOR_MATERIAL (1 << 11) +#define FLAG_NORMALIZE (1 << 12) +#define FLAG_NEED_EYE_SPACE (1 << 13) +#define FLAG_MATRIX_PALETTE (1 << 14) +#define FLAG_TEX_MATRIX (1 << 15) +#define FLAG_DEPTH_TEST (1 << 16) +#define FLAG_TEXTURE_ACTIVE (1 << 17) +#define FLAG_DEPTH_MASK (1 << 18) +#define FLAG_FINAL_MTX_DIRTY (1 << 19) +#define FLAG_LIGHT0 (1 << 20) +#define FLAG_LIGHT1 (1 << 21) +#define FLAG_LIGHT2 (1 << 22) +#define FLAG_LIGHT3 (1 << 23) +#define FLAG_LIGHT4 (1 << 24) +#define FLAG_LIGHT5 (1 << 25) +#define FLAG_LIGHT6 (1 << 26) +#define FLAG_LIGHT7 (1 << 27) +#define FLAG_TEX_GEN_S (1 << 28) +#define FLAG_TEX_GEN_T (1 << 29) +#define FLAG_TEX_GEN_R (1 << 30) +#define FLAG_TEX_GEN_Q (1 << 31) + +#define FLAG2_USE_RDPQ_MATERIAL (1 << 0) +#define FLAG2_USE_RDPQ_TEXTURING (1 << 1) +#define FLAG2_REDUCED_ALIASING (1 << 2) +#define FLAG2_MULTISAMPLE (1 << 3) +#define FLAG2_TEX_FLIP_T (1 << 4) + +#define TEX_FLAG_COMPLETE (1 << 0) +#define TEX_FLAG_UPLOAD_DIRTY (1 << 1) +#define TEX_FLAG_FORCE_COMPLETE (1 << 2) +#define TEX_FLAG_DETAIL (1 << 3) + +#define DITHER_MASK (SOM_RGBDITHER_MASK | SOM_ALPHADITHER_MASK) +#define BLEND_MASK SOM_ZMODE_MASK +#define DEPTH_TEST_MASK SOM_Z_COMPARE +#define DEPTH_MASK_MASK SOM_Z_WRITE +#define POINTS_MASK (SOM_ZSOURCE_MASK | SOM_TEXTURE_PERSP) +#define ALPHA_TEST_MASK SOM_ALPHACOMPARE_MASK + +#define LOAD_TILE 7 + +#define GUARD_BAND_FACTOR 2 + +#define ASSERT_INVALID_VTX_ID 0x2001 + +#define MULTISAMPLE_FLAG_SHIFT 2 +#define ZMODE_BLEND_FLAG_SHIFT 10 + +#define COMBINER_FLAG_CONSTANT (1 << 0) +#define COMBINER_FLAG_REPLACE (1 << 1) +#define COMBINER_FLAG_TEXTURE (1 << 2) + +#define TEXTURE_ACTIVE_SHIFT 17 +#define TEX_ACTIVE_COMBINER_SHIFT (TEXTURE_ACTIVE_SHIFT - 2) + +#define TEX_COORD_SHIFT 6 +#define HALF_TEXEL 0x0010 + +#define TEX_BILINEAR_SHIFT 13 +#define TEX_BILINEAR_OFFSET_SHIFT 4 + +#define TEX_DETAIL_SHIFT 15 + +#define BILINEAR_TEX_OFFSET_SHIFT 9 + +#define TRICMD_ATTR_SHIFT 8 +#define TRICMD_ATTR_MASK 0x300 + +#define LIGHT0_SHIFT 20 + +#define TEX_GEN_S_SHIFT 28 +#define TEX_GEN_LINEAR_FLAG_SHIFT 8 +#define TEX_GEN_SPHERICAL_FLAG_SHIFT 9 + +#define NEED_EYE_SPACE_SHIFT 13 + +#define VTX_LOADER_MAX_COMMANDS 11 +#define VTX_LOADER_MAX_SIZE (VTX_LOADER_MAX_COMMANDS * 4) + +#define RDPQ_TEXTURING_MASK ((SOM_SAMPLE_MASK | SOM_TEXTURE_LOD | SOMX_LOD_INTERPOLATE | SOMX_NUMLODS_MASK | SOM_TLUT_MASK)>>32) + +#define PALETTE_MATRIX_INDEX 3 + +#define PALETTE_DIRTY_FLAGS_SIZE ((MATRIX_PALETTE_SIZE+7)>>3) + +#define PRIM_VTX_CS_POSi 0 // X, Y, Z, W (all 32-bit) +#define PRIM_VTX_CS_POSf 8 // X, Y, Z, W (all 32-bit) +#define PRIM_VTX_X 16 // Object space position (16-bit) +#define PRIM_VTX_Y 18 // Object space position (16-bit) +#define PRIM_VTX_Z 20 // Object space position (16-bit) +#define PRIM_VTX_W 22 // Object space position (16-bit) +#define PRIM_VTX_R 24 +#define PRIM_VTX_G 26 +#define PRIM_VTX_B 28 +#define PRIM_VTX_A 30 +#define PRIM_VTX_TEX_S 32 +#define PRIM_VTX_TEX_T 34 +#define PRIM_VTX_TEX_R 36 +#define PRIM_VTX_TEX_Q 38 +#define PRIM_VTX_NORMAL 40 // Normal X,Y,Z (8 bit) +#define PRIM_VTX_MTX_INDEX 43 +#define PRIM_VTX_TRCODE 44 // trivial-reject clipping flags (against -w/+w) +#define PRIM_VTX_SIZE 45 + +#endif diff --git a/src/GL/gl_internal.h b/src/GL/gl_internal.h new file mode 100644 index 000000000..e8b2b7480 --- /dev/null +++ b/src/GL/gl_internal.h @@ -0,0 +1,903 @@ +/** + * @file gl_internal.h + * @author Dennis Heinze + */ +#ifndef __GL_INTERNAL +#define __GL_INTERNAL + +#include "GL/gl.h" +#include "GL/gl_integration.h" +#include "obj_map.h" +#include "surface.h" +#include "../utils.h" +#include +#include +#include "gl_constants.h" +#include "rspq.h" +#include "rdpq.h" +#include "../rdpq/rdpq_internal.h" +#include "rdpq_tri.h" + +#define RADIANS(x) ((x) * (float)M_PI / 180.0f) + +#define CLAMP01(x) CLAMP((x), 0, 1) + +#define CLAMPF_TO_BOOL(x) ((x)!=0.0) + +#define CLAMPF_TO_U8(x) ((x)*0xFF) +#define CLAMPF_TO_I8(x) ((x)*0x7F) +#define CLAMPF_TO_U16(x) ((x)*0xFFFF) +#define CLAMPF_TO_I16(x) ((x)*0x7FFF) +#define CLAMPF_TO_U32(x) ((x)*0xFFFFFFFF) +#define CLAMPF_TO_I32(x) ((x)*0x7FFFFFFF) + +#define FLOAT_TO_U8(x) (CLAMP((x), 0.f, 1.f)*0xFF) +#define FLOAT_TO_I8(x) (CLAMP((x), -1.f, 1.f)*0x7F) +#define FLOAT_TO_I16(x) (CLAMP((x), -1.f, 1.f)*0x7FFF) + +#define U8_TO_FLOAT(x) ((x)/(float)(0xFF)) +#define U16_TO_FLOAT(x) ((x)/(float)(0xFFFF)) +#define U32_TO_FLOAT(x) ((x)/(float)(0xFFFFFFFF)) +#define I8_TO_FLOAT(x) MAX((x)/(float)(0x7F),-1.f) +#define I16_TO_FLOAT(x) MAX((x)/(float)(0x7FFF),-1.f) +#define I32_TO_FLOAT(x) MAX((x)/(float)(0x7FFFFFFF),-1.f) + +#define RGBA32_FROM_FLOAT(r, g, b, a) RGBA32(FLOAT_TO_U8(r), FLOAT_TO_U8(g), FLOAT_TO_U8(b), FLOAT_TO_U8(a)) +#define PACKED_RGBA32_FROM_FLOAT(r, g, b, a) color_to_packed32(RGBA32_FROM_FLOAT(r, g, b, a)) + +#define GL_SET_STATE(var, value) ({ \ + typeof(value) _v = (value); \ + bool dirty_flag = _v != var; \ + var = _v; \ + dirty_flag; \ +}) + +#define gl_set_error(error, message, ...) ({ \ + state->current_error = error; \ + assertf(error == GL_NO_ERROR, "%s: " message, #error, ##__VA_ARGS__); \ +}) + +#define gl_ensure_no_begin_end() ({ \ + assertf(state, "gl_init() not called"); \ + if (state->begin_end_active) { \ + gl_set_error(GL_INVALID_OPERATION, "%s is not allowed between glBegin/glEnd", __func__); \ + } \ + true; \ +}) + +#define gl_assert_no_display_list() assertf(state->current_list == 0, "%s cannot be recorded into a display list", __func__) + +typedef int16_t int16u_t __attribute__((aligned(1))); +typedef uint16_t uint16u_t __attribute__((aligned(1))); +typedef int32_t int32u_t __attribute__((aligned(1))); +typedef uint32_t uint32u_t __attribute__((aligned(1))); +typedef float floatu __attribute__((aligned(1))); +typedef double doubleu __attribute__((aligned(1))); + +extern uint32_t gl_overlay_id; +extern uint32_t glp_overlay_id; +extern uint32_t gl_rsp_state; + +#define gl_write(cmd_id, ...) rspq_write(gl_overlay_id, cmd_id, ##__VA_ARGS__) +#define glp_write(cmd_id, ...) rspq_write(glp_overlay_id, cmd_id, ##__VA_ARGS__) +#define gl_write_rdp(rdpcmds, cmd_id, ...) rdpq_write(rdpcmds, gl_overlay_id, cmd_id, ##__VA_ARGS__) +#define glp_write_rdp(rdpcmds, cmd_id, ...) rdpq_write(rdpcmds, glp_overlay_id, cmd_id, ##__VA_ARGS__) + + +typedef enum { + GL_CMD_SET_FLAG = 0x0, + GL_CMD_SET_BYTE = 0x1, + GL_CMD_SET_SHORT = 0x2, + GL_CMD_SET_WORD = 0x3, + GL_CMD_SET_LONG = 0x4, + GL_CMD_UPDATE = 0x5, + GL_CMD_BIND_TEXTURE = 0x6, + GL_CMD_GET_VALUE = 0x7, + GL_CMD_COPY_FILL_COLOR = 0x8, + GL_CMD_SET_LIGHT_POS = 0x9, + GL_CMD_MATRIX_PUSH = 0xA, + GL_CMD_MATRIX_POP = 0xB, + GL_CMD_MATRIX_LOAD = 0xC, + GL_CMD_PRE_INIT_PIPE = 0xD, + GL_CMD_PRE_INIT_PIPE_TEX= 0xE, + GL_CMD_SET_PALETTE_IDX = 0xF, + GL_CMD_MATRIX_COPY = 0x10, +} gl_command_t; + +typedef enum { + GLP_CMD_INIT_PIPE = 0x0, + GLP_CMD_SET_VTX_LOADER = 0x1, + GLP_CMD_SET_VTX_CMD_SIZE = 0x2, + GLP_CMD_DRAW_TRI = 0x3, + GLP_CMD_SET_PRIM_VTX = 0x4, + GLP_CMD_SET_BYTE = 0x5, + GLP_CMD_SET_WORD = 0x6, + GLP_CMD_SET_LONG = 0x7, +} glp_command_t; + +typedef enum { + GL_UPDATE_NONE = 0x0, + GL_UPDATE_SCISSOR = 0x1, + GL_UPDATE_TEXTURE_COMPLETENESS = 0x2, + GL_UPDATE_TEXTURE_OBJECTS = 0x3, +} gl_update_func_t; + +typedef enum { + ATTRIB_VERTEX, + ATTRIB_COLOR, + ATTRIB_TEXCOORD, + ATTRIB_NORMAL, + ATTRIB_MTX_INDEX, + ATTRIB_COUNT +} gl_array_type_t; + +typedef struct { + GLfloat position[4]; + GLfloat color[4]; + GLfloat texcoord[4]; + GLfloat normal[3]; + GLubyte mtx_index[VERTEX_UNIT_COUNT]; +} gl_obj_attributes_t; + +typedef struct { + GLfloat screen_pos[2]; + GLfloat depth; + GLfloat shade[4]; + GLfloat texcoord[2]; + GLfloat inv_w; + GLfloat cs_pos[4]; + gl_obj_attributes_t obj_attributes; + uint8_t clip_code; + uint8_t tr_code; + uint8_t t_l_applied; +} gl_vtx_t; + +#define VTX_SCREEN_POS_OFFSET (offsetof(gl_vtx_t, screen_pos) / sizeof(float)) +#define VTX_SHADE_OFFSET (offsetof(gl_vtx_t, shade) / sizeof(float)) +#define VTX_TEXCOORD_OFFSET (offsetof(gl_vtx_t, texcoord) / sizeof(float)) +#define VTX_DEPTH_OFFSET (offsetof(gl_vtx_t, depth) / sizeof(float)) + +typedef struct { + GLfloat m[4][4]; +} gl_matrix_t; + +typedef struct { + GLfloat scale[3]; + GLfloat offset[3]; +} gl_viewport_t; + +typedef struct { + gl_matrix_t *storage; + int32_t size; + int32_t cur_depth; +} gl_matrix_stack_t; + +typedef struct { + gl_matrix_stack_t *mv_stack; + gl_matrix_t mvp; + bool is_mvp_dirty; +} gl_matrix_target_t; + +typedef struct { + int16_t i[4][4]; + uint16_t f[4][4]; +} gl_matrix_srv_t; +_Static_assert(sizeof(gl_matrix_srv_t) == MATRIX_SIZE, "Matrix size does not match"); + +typedef struct { + uint16_t width; + uint16_t height; + uint16_t internal_format; +} __attribute__((packed)) gl_texture_image_t; +_Static_assert(sizeof(gl_texture_image_t) == TEXTURE_IMAGE_SIZE, "Texture image has incorrect size!"); +_Static_assert(offsetof(gl_texture_image_t, width) == IMAGE_WIDTH_OFFSET, "Texture image has incorrect layout!"); +_Static_assert(offsetof(gl_texture_image_t, height) == IMAGE_HEIGHT_OFFSET, "Texture image has incorrect layout!"); +_Static_assert(offsetof(gl_texture_image_t, internal_format) == IMAGE_INTERNAL_FORMAT_OFFSET, "Texture image has incorrect layout!"); + +typedef struct { + gl_texture_image_t levels[MAX_TEXTURE_LEVELS]; + uint8_t levels_count; // number of mipmaps minus one + uint8_t tlut_mode; + uint32_t levels_block[MAX_TEXTURE_LEVELS*2+1]; + uint32_t flags; + uint16_t min_filter; + uint16_t mag_filter; +} __attribute__((aligned(16), packed)) gl_srv_texture_object_t; +_Static_assert(sizeof(gl_srv_texture_object_t) == TEXTURE_OBJECT_SIZE, "Texture object has incorrect size!"); +_Static_assert((TEXTURE_OBJECT_SIZE % 8) == 0, "Texture object has incorrect size!"); +_Static_assert(offsetof(gl_srv_texture_object_t, levels_count) == TEXTURE_LEVELS_COUNT_OFFSET, "Texture object has incorrect layout!"); +_Static_assert(offsetof(gl_srv_texture_object_t, tlut_mode) == TEXTURE_TLUT_MODE_OFFSET, "Texture object has incorrect layout!"); +_Static_assert(offsetof(gl_srv_texture_object_t, levels_block) == TEXTURE_LEVELS_BLOCK_OFFSET, "Texture object has incorrect layout!"); +_Static_assert((TEXTURE_LEVELS_BLOCK_OFFSET % 4) == 0, "Texture object has incorrect layout!"); +_Static_assert(offsetof(gl_srv_texture_object_t, flags) == TEXTURE_FLAGS_OFFSET, "Texture object has incorrect layout!"); +_Static_assert(offsetof(gl_srv_texture_object_t, min_filter) == TEXTURE_MIN_FILTER_OFFSET, "Texture object has incorrect layout!"); +_Static_assert(offsetof(gl_srv_texture_object_t, mag_filter) == TEXTURE_MAG_FILTER_OFFSET, "Texture object has incorrect layout!"); + +typedef enum { + TEX_IS_DEFAULT = 0x1, + TEX_HAS_IMAGE = 0x2, +} gl_texture_flag_t; + +typedef struct { + GLenum dimensionality; + uint16_t flags; + uint16_t wrap_s; + uint16_t wrap_t; + + sprite_t *sprite; + surface_t surfaces[MAX_TEXTURE_LEVELS]; + rspq_block_t *blocks[MAX_TEXTURE_LEVELS]; + + gl_srv_texture_object_t *srv_object; +} gl_texture_object_t; + +typedef struct { + gl_vtx_t *vertices[CLIPPING_PLANE_COUNT + 3]; + uint32_t count; +} gl_clipping_list_t; + +typedef struct { + GLfloat ambient[4]; + GLfloat diffuse[4]; + GLfloat specular[4]; + GLfloat emissive[4]; + GLfloat shininess; + GLenum color_target; +} gl_material_t; + +typedef struct { + GLfloat ambient[4]; + GLfloat diffuse[4]; + GLfloat specular[4]; + GLfloat position[4]; + GLfloat direction[3]; + GLfloat spot_exponent; + GLfloat spot_cutoff_cos; + GLfloat constant_attenuation; + GLfloat linear_attenuation; + GLfloat quadratic_attenuation; + bool enabled; +} gl_light_t; + +typedef struct { + int16_t position[LIGHT_COUNT][4]; + int16_t ambient[LIGHT_COUNT][4]; + int16_t diffuse[LIGHT_COUNT][4]; + int16_t attenuation_int[LIGHT_COUNT][4]; + uint16_t attenuation_frac[LIGHT_COUNT][4]; +} __attribute__((packed)) gl_lights_soa_t; +_Static_assert(sizeof(gl_lights_soa_t) == LIGHT_STRUCT_SIZE); +_Static_assert(offsetof(gl_lights_soa_t, position) == LIGHT_POSITION_OFFSET); +_Static_assert(offsetof(gl_lights_soa_t, ambient) == LIGHT_AMBIENT_OFFSET); +_Static_assert(offsetof(gl_lights_soa_t, diffuse) == LIGHT_DIFFUSE_OFFSET); +_Static_assert(offsetof(gl_lights_soa_t, attenuation_int) == LIGHT_ATTENUATION_INT_OFFSET); +_Static_assert(offsetof(gl_lights_soa_t, attenuation_frac) == LIGHT_ATTENUATION_FRAC_OFFSET); + +typedef struct { + GLvoid *data; + uint32_t size; +} gl_storage_t; + +typedef struct { + GLenum usage; + GLenum access; + GLvoid *pointer; + gl_storage_t storage; + bool mapped; +} gl_buffer_object_t; + +typedef struct { + rspq_write_t w; + union { + uint8_t bytes[4]; + uint32_t word; + }; + uint32_t buffer_head; +} gl_cmd_stream_t; + +typedef void (*cpu_read_attrib_func)(void*,const void*,uint32_t); +typedef void (*rsp_read_attrib_func)(gl_cmd_stream_t*,const void*,uint32_t); + +typedef struct { + GLint size; + GLenum type; + GLsizei stride; + const GLvoid *pointer; + gl_buffer_object_t *binding; + bool normalize; + bool enabled; + + const GLvoid *final_pointer; + uint16_t final_stride; + cpu_read_attrib_func cpu_read_func; + rsp_read_attrib_func rsp_read_func; +} gl_array_t; + +typedef struct { + gl_array_t arrays[ATTRIB_COUNT]; +} gl_array_object_t; + +typedef uint32_t (*read_index_func)(const void*,uint32_t); + +typedef struct { + GLenum mode; + GLfloat eye_plane[TEX_COORD_COUNT]; + GLfloat object_plane[TEX_COORD_COUNT]; + bool enabled; +} gl_tex_gen_t; + +typedef struct { + int16_t integer[TEX_COORD_COUNT][TEX_GEN_PLANE_COUNT][TEX_GEN_COUNT]; + uint16_t fraction[TEX_COORD_COUNT][TEX_GEN_PLANE_COUNT][TEX_GEN_COUNT]; + uint16_t mode[TEX_GEN_COUNT]; + uint16_t mode_const[4]; +} gl_tex_gen_soa_t; +_Static_assert(sizeof(gl_tex_gen_soa_t) == TEX_GEN_STRUCT_SIZE); +_Static_assert(offsetof(gl_tex_gen_soa_t, integer) == TEX_GEN_INTEGER_OFFSET); +_Static_assert(offsetof(gl_tex_gen_soa_t, fraction) == TEX_GEN_FRACTION_OFFSET); +_Static_assert(offsetof(gl_tex_gen_soa_t, mode) == TEX_GEN_MODE_OFFSET); + +typedef struct { + int16_t factor_int; + int16_t offset_int; + uint16_t factor_frac; + uint16_t offset_frac; +} gl_fog_params_t; + +typedef struct { + GLsizei size; + GLfloat entries[MAX_PIXEL_MAP_SIZE]; +} gl_pixel_map_t; + +typedef struct { + void (*begin)(); + void (*end)(); + void (*vertex)(const void*,GLenum,uint32_t); + void (*color)(const void*,GLenum,uint32_t); + void (*tex_coord)(const void*,GLenum,uint32_t); + void (*normal)(const void*,GLenum,uint32_t); + void (*mtx_index)(const void*,GLenum,uint32_t); + void (*array_element)(uint32_t); + void (*draw_arrays)(uint32_t,uint32_t); + void (*draw_elements)(uint32_t,const void*,read_index_func); +} gl_pipeline_t; + +typedef struct { + GLuint target_precision; + GLuint precision; + GLint shift_amount; + GLfloat to_float_factor; +} gl_fixed_precision_t; + +typedef struct { + // Pipeline state + + bool cull_face; + bool texture_1d; + bool texture_2d; + bool depth_test; + bool lighting; + bool fog; + bool color_material; + bool normalize; + bool matrix_palette_enabled; + bool tex_flip_t; + + GLenum cull_face_mode; + GLenum front_face; + GLenum polygon_mode; + + GLenum primitive_mode; + + GLfloat point_size; + GLfloat line_width; + + GLfloat fog_start; + GLfloat fog_end; + GLfloat fog_offset; + GLfloat fog_factor; + + gl_material_t material; + gl_light_t lights[LIGHT_COUNT]; + + GLfloat light_model_ambient[4]; + bool light_model_local_viewer; + + GLenum shade_model; + + gl_tex_gen_t tex_gen[TEX_GEN_COUNT]; + + gl_viewport_t current_viewport; + + GLenum matrix_mode; + GLint current_palette_matrix; + + gl_matrix_t *current_matrix; + + gl_matrix_t modelview_stack_storage[MODELVIEW_STACK_SIZE]; + gl_matrix_t projection_stack_storage[PROJECTION_STACK_SIZE]; + gl_matrix_t texture_stack_storage[TEXTURE_STACK_SIZE]; + gl_matrix_t palette_stack_storage[MATRIX_PALETTE_SIZE][PALETTE_STACK_SIZE]; + + gl_matrix_stack_t modelview_stack; + gl_matrix_stack_t projection_stack; + gl_matrix_stack_t texture_stack; + gl_matrix_stack_t palette_stacks[MATRIX_PALETTE_SIZE]; + gl_matrix_stack_t *current_matrix_stack; + + gl_matrix_target_t default_matrix_target; + gl_matrix_target_t palette_matrix_targets[MATRIX_PALETTE_SIZE]; + gl_matrix_target_t *current_matrix_target; + + bool begin_end_active; + + gl_texture_object_t *texture_1d_object; + gl_texture_object_t *texture_2d_object; + + gl_obj_attributes_t current_attributes; + + uint8_t prim_size; + uint8_t prim_indices[3]; + uint8_t prim_progress; + uint32_t prim_counter; + uint8_t (*prim_func)(void); + uint32_t prim_id; + bool lock_next_vertex; + uint8_t locked_vertex; + + uint16_t prim_tex_width; + uint16_t prim_tex_height; + bool prim_texture; + bool prim_bilinear; + uint8_t prim_mipmaps; + + int32_t last_array_element; + + rdpq_trifmt_t trifmt; + + gl_vtx_t vertex_cache[VERTEX_CACHE_SIZE]; + uint32_t vertex_cache_ids[VERTEX_CACHE_SIZE]; + uint32_t lru_age_table[VERTEX_CACHE_SIZE]; + uint32_t lru_next_age; + + gl_vtx_t *primitive_vertices[3]; + + GLfloat flat_color[4]; + + // Client state + + const surface_t *color_buffer; + + GLenum current_error; + + gl_array_object_t default_array_object; + gl_array_object_t *array_object; + + gl_texture_object_t *default_textures; + + obj_map_t list_objects; + GLuint next_list_name; + GLuint list_base; + GLuint current_list; + + gl_buffer_object_t *array_buffer; + gl_buffer_object_t *element_array_buffer; + + gl_matrix_srv_t *matrix_stacks[3]; + gl_matrix_srv_t *matrix_palette; + + GLboolean unpack_swap_bytes; + GLboolean unpack_lsb_first; + GLint unpack_row_length; + GLint unpack_skip_rows; + GLint unpack_skip_pixels; + GLint unpack_alignment; + + GLboolean map_color; + GLfloat transfer_scale[4]; + GLfloat transfer_bias[4]; + + gl_pixel_map_t pixel_maps[4]; + + bool transfer_is_noop; + + gl_fixed_precision_t vertex_halfx_precision; + gl_fixed_precision_t texcoord_halfx_precision; + + bool can_use_rsp; + bool can_use_rsp_dirty; + + const gl_pipeline_t *current_pipeline; +} gl_state_t; + +typedef struct { + gl_matrix_srv_t matrices[5]; + gl_lights_soa_t lights; + gl_tex_gen_soa_t tex_gen; + int16_t viewport_scale[4]; + int16_t viewport_offset[4]; + int16_t light_ambient[4]; + int16_t mat_ambient[4]; + int16_t mat_diffuse[4]; + int16_t mat_specular[4]; + int16_t mat_emissive[4]; + uint16_t mat_color_target[3]; + uint16_t mat_shininess; + int16_t color[4]; + int16_t tex_coords[4]; + int8_t normal[3]; + uint8_t mtx_index; + uint32_t matrix_pointers[5]; + uint32_t loaded_mtx_index[2]; + uint32_t flags; + gl_fog_params_t fog_params; + uint16_t tex_size[2]; + uint16_t tex_offset[2]; + uint16_t polygon_mode; + uint16_t prim_type; + uint16_t cull_mode; + uint16_t front_face; + uint16_t shade_model; + uint16_t point_size; + uint16_t line_width; + uint16_t matrix_mode; + uint16_t tri_cmd; + uint8_t tri_cull[2]; + + gl_srv_texture_object_t bound_textures[2]; + uint16_t scissor_rect[4]; + uint32_t blend_cycle; + uint32_t fog_color; + uint32_t flags2; + uint32_t texture_ids[2]; + uint32_t uploaded_tex; + uint32_t clear_color; + uint32_t clear_depth; + uint32_t palette_index; + uint32_t dither_mode; + uint16_t fb_size[2]; + uint16_t depth_func; + uint16_t alpha_func; + uint16_t blend_src; + uint16_t blend_dst; + uint16_t tex_env_mode; + uint8_t alpha_ref; + uint8_t palette_dirty[PALETTE_DIRTY_FLAGS_SIZE]; +} __attribute__((aligned(8), packed)) gl_server_state_t; + +_Static_assert((offsetof(gl_server_state_t, bound_textures) & 0x7) == 0, "Bound textures must be aligned to 8 bytes in server state"); + +void gl_matrix_init(); +void gl_texture_init(); +void gl_lighting_init(); +void gl_rendermode_init(); +void gl_array_init(); +void gl_primitive_init(); +void gl_pixel_init(); +void gl_list_init(); + +void gl_texture_close(); +void gl_primitive_close(); +void gl_list_close(); + +gl_matrix_t * gl_matrix_stack_get_matrix(gl_matrix_stack_t *stack); + +void gl_update_matrix_targets(); + +void gl_matrix_mult(GLfloat *d, const gl_matrix_t *m, const GLfloat *v); +void gl_matrix_mult3x3(GLfloat *d, const gl_matrix_t *m, const GLfloat *v); +void gl_matrix_mult4x2(GLfloat *d, const gl_matrix_t *m, const GLfloat *v); + +void gl_perform_lighting(GLfloat *color, const GLfloat *input, const GLfloat *v, const GLfloat *n, const gl_material_t *material); + +gl_texture_object_t * gl_get_active_texture(); + +void gl_cross(GLfloat* p, const GLfloat* a, const GLfloat* b); +float dot_product3(const float *a, const float *b); +void gl_normalize(GLfloat *d, const GLfloat *v); + +uint32_t gl_get_type_size(GLenum type); + +bool gl_storage_alloc(gl_storage_t *storage, uint32_t size); +void gl_storage_free(gl_storage_t *storage); +bool gl_storage_resize(gl_storage_t *storage, uint32_t new_size); + +void set_can_use_rsp_dirty(); + +void gl_update_array_pointers(gl_array_object_t *obj); + +void gl_fill_attrib_defaults(gl_array_type_t array_type, uint32_t size); +void gl_fill_all_attrib_defaults(const gl_array_t *arrays); +void gl_load_attribs(const gl_array_t *arrays, uint32_t index); +bool gl_get_cache_index(uint32_t vertex_id, uint8_t *cache_index); +bool gl_prim_assembly(uint8_t cache_index, uint8_t *indices); +void gl_read_attrib(gl_array_type_t array_type, const void *value, GLenum type, uint32_t size); + +inline uint32_t next_pow2(uint32_t v) +{ + v--; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v++; + return v; +} + +inline uint32_t gl_type_to_index(GLenum type) +{ + switch (type) { + case GL_BYTE: + case GL_UNSIGNED_BYTE: + case GL_SHORT: + case GL_UNSIGNED_SHORT: + case GL_INT: + case GL_UNSIGNED_INT: + case GL_FLOAT: + return type - GL_BYTE; + case GL_DOUBLE: + return 7; + case GL_HALF_FIXED_N64: + return 8; + default: + return -1; + } +} + +#define next_prim_id() (state->prim_id++) + +inline const void *gl_get_attrib_element(const gl_array_t *src, uint32_t index) +{ + return src->final_pointer + index * src->final_stride; +} + +inline gl_cmd_stream_t gl_cmd_stream_begin(uint32_t ovl_id, uint32_t cmd_id, int size) +{ + return (gl_cmd_stream_t) { + .w = rspq_write_begin(ovl_id, cmd_id, size), + .buffer_head = 2, + }; +} + +inline void gl_cmd_stream_commit(gl_cmd_stream_t *s) +{ + rspq_write_arg(&s->w, s->word); + s->buffer_head = 0; + s->word = 0; +} + +inline void gl_cmd_stream_put_byte(gl_cmd_stream_t *s, uint8_t v) +{ + s->bytes[s->buffer_head++] = v; + + if (s->buffer_head == sizeof(uint32_t)) { + gl_cmd_stream_commit(s); + } +} + +inline void gl_cmd_stream_put_half(gl_cmd_stream_t *s, uint16_t v) +{ + s->bytes[s->buffer_head++] = v >> 8; + s->bytes[s->buffer_head++] = v & 0xFF; + + if (s->buffer_head == sizeof(uint32_t)) { + gl_cmd_stream_commit(s); + } +} + +inline void gl_cmd_stream_end(gl_cmd_stream_t *s) +{ + if (s->buffer_head > 0) { + gl_cmd_stream_commit(s); + } + + rspq_write_end(&s->w); +} + +inline bool is_in_heap_memory(void *ptr) +{ + ptr = CachedAddr(ptr); + return ptr >= HEAP_START_ADDR && ptr < ((void*)KSEG0_START_ADDR + __boot_memsize); +} + +inline bool is_valid_object_id(GLuint id) +{ + return is_in_heap_memory((void*)id); +} + +inline bool gl_tex_is_complete(const gl_texture_object_t *obj) +{ + return obj->srv_object->flags & TEX_FLAG_COMPLETE; +} + +inline uint8_t gl_tex_get_levels(const gl_texture_object_t *obj) +{ + return obj->srv_object->levels_count + 1; +} + +__attribute__((always_inline)) +inline int gl_get_rdpcmds_for_update_func(gl_update_func_t update_func) +{ + switch (update_func) { + case GL_UPDATE_NONE: return 0; + case GL_UPDATE_SCISSOR: return 1; + case GL_UPDATE_TEXTURE_COMPLETENESS: return 0; + case GL_UPDATE_TEXTURE_OBJECTS: return 0; + } + __builtin_unreachable(); +} + +__attribute__((always_inline)) +inline void gl_set_flag_raw(gl_update_func_t update_func, uint32_t offset, uint32_t flag, bool value) +{ + gl_write_rdp(gl_get_rdpcmds_for_update_func(update_func), + GL_CMD_SET_FLAG, _carg(update_func, 0x7FF, 13) | _carg(offset, 0xFFC, 0) | _carg(value, 0x1, 0), value ? flag : ~flag); +} + +__attribute__((always_inline)) +inline void gl_set_flag(gl_update_func_t update_func, uint32_t flag, bool value) +{ + gl_set_flag_raw(update_func, offsetof(gl_server_state_t, flags), flag, value); +} + +__attribute__((always_inline)) +inline void gl_set_flag_word2(gl_update_func_t update_func, uint32_t flag, bool value) +{ + gl_set_flag_raw(update_func, offsetof(gl_server_state_t, flags2), flag, value); +} + +__attribute__((always_inline)) +inline void gl_set_byte(gl_update_func_t update_func, uint32_t offset, uint8_t value) +{ + gl_write_rdp(gl_get_rdpcmds_for_update_func(update_func), + GL_CMD_SET_BYTE, _carg(update_func, 0x7FF, 13) | _carg(offset, 0xFFF, 0), value); +} + +__attribute__((always_inline)) +inline void gl_set_short(gl_update_func_t update_func, uint32_t offset, uint16_t value) +{ + gl_write_rdp(gl_get_rdpcmds_for_update_func(update_func), + GL_CMD_SET_SHORT, _carg(update_func, 0x7FF, 13) | _carg(offset, 0xFFF, 0), value); +} + +__attribute__((always_inline)) +inline void gl_set_word(gl_update_func_t update_func, uint32_t offset, uint32_t value) +{ + gl_write_rdp(gl_get_rdpcmds_for_update_func(update_func), + GL_CMD_SET_WORD, _carg(update_func, 0x7FF, 13) | _carg(offset, 0xFFF, 0), value); +} + +__attribute__((always_inline)) +inline void gl_set_long(gl_update_func_t update_func, uint32_t offset, uint64_t value) +{ + gl_write_rdp(gl_get_rdpcmds_for_update_func(update_func), + GL_CMD_SET_LONG, _carg(update_func, 0x7FF, 13) | _carg(offset, 0xFFF, 0), value >> 32, value & 0xFFFFFFFF); +} + +__attribute__((always_inline)) +inline void gl_update(gl_update_func_t update_func) +{ + gl_write_rdp(gl_get_rdpcmds_for_update_func(update_func), + GL_CMD_UPDATE, _carg(update_func, 0x7FF, 13)); +} + +inline void gl_get_value(void *dst, uint32_t offset, uint32_t size) +{ + gl_write(GL_CMD_GET_VALUE, _carg(size-1, 0xFFF, 12) | _carg(offset, 0xFFF, 0), PhysicalAddr(dst)); +} + +inline void gl_bind_texture(GLenum target, gl_texture_object_t *texture) +{ + uint32_t index = target == GL_TEXTURE_2D ? 1 : 0; + uint32_t id_offset = index * sizeof(uint32_t); + uint32_t tex_offset = index * sizeof(gl_srv_texture_object_t); + gl_write(GL_CMD_BIND_TEXTURE, (id_offset << 16) | tex_offset, PhysicalAddr(texture->srv_object)); +} + +inline void gl_update_texture_completeness(uint32_t offset) +{ + gl_write(GL_CMD_UPDATE, _carg(GL_UPDATE_TEXTURE_COMPLETENESS, 0x7FF, 13) | (offset - offsetof(gl_server_state_t, bound_textures))); +} + +inline void* gl_get_attrib_pointer(gl_obj_attributes_t *attribs, gl_array_type_t array_type) +{ + switch (array_type) { + case ATTRIB_VERTEX: + return attribs->position; + case ATTRIB_COLOR: + return attribs->color; + case ATTRIB_TEXCOORD: + return attribs->texcoord; + case ATTRIB_NORMAL: + return attribs->normal; + case ATTRIB_MTX_INDEX: + return attribs->mtx_index; + default: + assert(0); + return NULL; + } +} + +inline void gl_set_current_color(GLfloat *color) +{ + int16_t r_fx = FLOAT_TO_I16(color[0]); + int16_t g_fx = FLOAT_TO_I16(color[1]); + int16_t b_fx = FLOAT_TO_I16(color[2]); + int16_t a_fx = FLOAT_TO_I16(color[3]); + + uint64_t packed = ((uint64_t)r_fx << 48) | ((uint64_t)g_fx << 32) | ((uint64_t)b_fx << 16) | (uint64_t)a_fx; + gl_set_long(GL_UPDATE_NONE, offsetof(gl_server_state_t, color), packed); +} + +inline void gl_set_current_texcoords(GLfloat *texcoords) +{ + int16_t fixed_s = texcoords[0] * (1 << 5); + int16_t fixed_t = texcoords[1] * (1 << 5); + int16_t fixed_r = texcoords[2] * (1 << 5); + int16_t fixed_q = texcoords[3] * (1 << 5); + + uint64_t packed = ((uint64_t)fixed_s << 48) | ((uint64_t)fixed_t << 32) | ((uint64_t)fixed_r << 16) | (uint64_t)fixed_q; + gl_set_long(GL_UPDATE_NONE, offsetof(gl_server_state_t, tex_coords), packed); +} + +inline void gl_set_current_normal(GLfloat *normal) +{ + int8_t fixed_nx = normal[0] * 0x7F; + int8_t fixed_ny = normal[1] * 0x7F; + int8_t fixed_nz = normal[2] * 0x7F; + + uint32_t packed = ((uint32_t)fixed_nx << 24) | ((uint32_t)fixed_ny << 16) | ((uint32_t)fixed_nz << 8); + gl_set_word(GL_UPDATE_NONE, offsetof(gl_server_state_t, normal), packed); +} + +inline void gl_set_current_mtx_index(GLubyte *index) +{ + for (uint32_t i = 0; i < VERTEX_UNIT_COUNT; i++) + { + gl_set_byte(GL_UPDATE_NONE, offsetof(gl_server_state_t, mtx_index) + i, index[i]); + } +} + +inline void gl_set_palette_idx(uint32_t index) +{ + gl_write(GL_CMD_SET_PALETTE_IDX, index * sizeof(gl_matrix_srv_t)); +} + +inline void gl_pre_init_pipe(GLenum primitive_mode) +{ + // PreInitPipeTex will run a block with nesting level 1 for texture upload. + // The command itself does not emit RDP commands (the block does that, so + // we use a plain gl_write() for it. + rspq_block_run_rsp(1); + gl_write(GL_CMD_PRE_INIT_PIPE_TEX); + + // PreInitPipe is similar to rdpq_set_mode_standard wrt RDP commands. + // It issues SET_SCISSOR + CC + SOM. + gl_write_rdp(3, GL_CMD_PRE_INIT_PIPE, primitive_mode); +} + +inline void glpipe_init() +{ + glp_write(GLP_CMD_INIT_PIPE, gl_rsp_state); +} + +inline void glpipe_set_vtx_cmd_size(uint16_t patched_cmd_descriptor, uint16_t *cmd_descriptor) +{ + glp_write(GLP_CMD_SET_VTX_CMD_SIZE, patched_cmd_descriptor, PhysicalAddr(cmd_descriptor)); +} + +#define TEX_SCALE 32.0f +#define OBJ_SCALE 32.0f + +inline void glpipe_draw_triangle(int i0, int i1, int i2) +{ + // We pass -1 because the triangle can be clipped and split into multiple + // triangles. + glp_write_rdp(-1, GLP_CMD_DRAW_TRI, + (i0*PRIM_VTX_SIZE), + ((i1*PRIM_VTX_SIZE)<<16) | (i2*PRIM_VTX_SIZE) + ); +} + +#endif diff --git a/src/GL/glu.c b/src/GL/glu.c new file mode 100644 index 000000000..68d052632 --- /dev/null +++ b/src/GL/glu.c @@ -0,0 +1,77 @@ +/** + * @file glu.c + * @author Giovanni Bajo + * @author Dennis Heinze + * @brief OpenGL Utility Library (GLU) convenience functions. + */ +#include "GL/glu.h" +#include "gl_internal.h" + +void gluLookAt(float eyex, float eyey, float eyez, + float centerx, float centery, float centerz, + float upx, float upy, float upz) +{ + GLfloat eye[3] = {eyex, eyey, eyez}; + GLfloat f[3] = {centerx - eyex, centery - eyey, centerz - eyez}; + GLfloat u[3] = {upx, upy, upz}; + GLfloat s[3]; + + gl_normalize(f, f); + + gl_cross(s, f, u); + gl_normalize(s, s); + + gl_cross(u, s, f); + + GLfloat m[4][4]; + + m[0][0] = s[0]; + m[0][1] = u[0]; + m[0][2] = -f[0]; + m[0][3] = 0; + + m[1][0] = s[1]; + m[1][1] = u[1]; + m[1][2] = -f[1]; + m[1][3] = 0; + + m[2][0] = s[2]; + m[2][1] = u[2]; + m[2][2] = -f[2]; + m[2][3] = 0; + + m[3][0] = -dot_product3(s, eye); + m[3][1] = -dot_product3(u, eye); + m[3][2] = dot_product3(f, eye); + m[3][3] = 1; + + glMultMatrixf(&m[0][0]); +}; + +void gluPerspective(float fovy, float aspect, float zNear, float zFar) +{ + float sine, cotangent, deltaZ; + float radians = fovy / 2 * (float)M_PI / 180; + deltaZ = zFar - zNear; + sine = sinf(radians); + if ((deltaZ == 0) || (sine == 0) || (aspect == 0)) + { + return; + } + cotangent = cosf(radians) / sine; + + float m[4][4] = { + {1,0,0,0}, + {0,1,0,0}, + {0,0,1,0}, + {0,0,0,1}, + }; + m[0][0] = cotangent / aspect; + m[1][1] = cotangent; + m[2][2] = -(zFar + zNear) / deltaZ; + m[2][3] = -1; + m[3][2] = -2 * zNear * zFar / deltaZ; + m[3][3] = 0; + + glMultMatrixf(&m[0][0]); +} diff --git a/src/GL/libdragon.mk b/src/GL/libdragon.mk new file mode 100644 index 000000000..b6af1d7c9 --- /dev/null +++ b/src/GL/libdragon.mk @@ -0,0 +1,18 @@ +LIBDRAGON_OBJS += \ + $(BUILD_DIR)/GL/gl.o \ + $(BUILD_DIR)/GL/lighting.o \ + $(BUILD_DIR)/GL/matrix.o \ + $(BUILD_DIR)/GL/primitive.o \ + $(BUILD_DIR)/GL/query.o \ + $(BUILD_DIR)/GL/rendermode.o \ + $(BUILD_DIR)/GL/texture.o \ + $(BUILD_DIR)/GL/array.o \ + $(BUILD_DIR)/GL/pixelrect.o \ + $(BUILD_DIR)/GL/obj_map.o \ + $(BUILD_DIR)/GL/list.o \ + $(BUILD_DIR)/GL/buffer.o \ + $(BUILD_DIR)/GL/rsp_gl.o \ + $(BUILD_DIR)/GL/rsp_gl_pipeline.o \ + $(BUILD_DIR)/GL/glu.o \ + $(BUILD_DIR)/GL/cpu_pipeline.o \ + $(BUILD_DIR)/GL/rsp_pipeline.o \ No newline at end of file diff --git a/src/GL/lighting.c b/src/GL/lighting.c new file mode 100644 index 000000000..822c7779a --- /dev/null +++ b/src/GL/lighting.c @@ -0,0 +1,863 @@ +/** + * @file lighting.c + * @author Dennis Heinze + * @brief OpenGL lighting calculations and material/light state management. + */ +#include "gl_internal.h" +#include "utils.h" +#include "debug.h" +#include + +_Static_assert((1<material); + + for (uint32_t i = 0; i < LIGHT_COUNT; i++) + { + gl_init_light(&state->lights[i]); + } + + state->lights[0].diffuse[0] = 0.2f; + state->lights[0].diffuse[1] = 0.2f; + state->lights[0].diffuse[2] = 0.2f; + + state->lights[0].specular[0] = 0.8f; + state->lights[0].specular[1] = 0.8f; + state->lights[0].specular[2] = 0.8f; + + state->light_model_ambient[0] = 0.2f; + state->light_model_ambient[1] = 0.2f; + state->light_model_ambient[2] = 0.2f; + state->light_model_ambient[3] = 1.0f; + state->light_model_local_viewer = false; +} + +float gl_mag2(const GLfloat *v) +{ + return v[0]*v[0] + v[1]*v[1] + v[2]*v[2]; +} + +float gl_mag(const GLfloat *v) +{ + return sqrtf(gl_mag2(v)); +} + +void gl_normalize(GLfloat *d, const GLfloat *v) +{ + float inv_mag = 1.0f / gl_mag(v); + + d[0] = v[0] * inv_mag; + d[1] = v[1] * inv_mag; + d[2] = v[2] * inv_mag; +} + +void gl_homogeneous_unit_diff(GLfloat *d, const GLfloat *p1, const GLfloat *p2) +{ + bool p1wzero = p1[3] == 0.0f; + bool p2wzero = p2[3] == 0.0f; + + if (!(p1wzero ^ p2wzero)) { + d[0] = p2[0] - p1[0]; + d[1] = p2[1] - p1[1]; + d[2] = p2[2] - p1[2]; + } else if (p1wzero) { + d[0] = -p1[0]; + d[1] = -p1[1]; + d[2] = -p1[2]; + } else { + d[0] = p2[0]; + d[1] = p2[1]; + d[2] = p2[2]; + } + + gl_normalize(d, d); +} + +void gl_cross(GLfloat* p, const GLfloat* a, const GLfloat* b) +{ + p[0] = (a[1] * b[2] - a[2] * b[1]); + p[1] = (a[2] * b[0] - a[0] * b[2]); + p[2] = (a[0] * b[1] - a[1] * b[0]); +}; + +float dot_product3(const float *a, const float *b) +{ + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +} + +float gl_clamped_dot(const GLfloat *a, const GLfloat *b) +{ + return MAX(dot_product3(a, b), 0.0f); +} + +const GLfloat * gl_material_get_color(const gl_material_t *material, GLenum color, const GLfloat *input) +{ + GLenum target = material->color_target; + + switch (color) { + case GL_EMISSION: + return state->color_material && target == GL_EMISSION ? input : material->emissive; + case GL_AMBIENT: + return state->color_material && (target == GL_AMBIENT || target == GL_AMBIENT_AND_DIFFUSE) ? input : material->ambient; + case GL_DIFFUSE: + return state->color_material && (target == GL_DIFFUSE || target == GL_AMBIENT_AND_DIFFUSE) ? input : material->diffuse; + case GL_SPECULAR: + return state->color_material && target == GL_SPECULAR ? input : material->specular; + default: + assertf(0, "Invalid material color!"); + return NULL; + } +} + +void gl_perform_lighting(GLfloat *color, const GLfloat *input, const GLfloat *v, const GLfloat *n, const gl_material_t *material) +{ + const GLfloat *emissive = gl_material_get_color(material, GL_EMISSION, input); + const GLfloat *ambient = gl_material_get_color(material, GL_AMBIENT, input); + const GLfloat *diffuse = gl_material_get_color(material, GL_DIFFUSE, input); + const GLfloat *specular = gl_material_get_color(material, GL_SPECULAR, input); + + // Emission and ambient + color[0] = emissive[0] + ambient[0] * state->light_model_ambient[0]; + color[1] = emissive[1] + ambient[1] * state->light_model_ambient[1]; + color[2] = emissive[2] + ambient[2] * state->light_model_ambient[2]; + color[3] = diffuse[3]; + + for (uint32_t l = 0; l < LIGHT_COUNT; l++) + { + const gl_light_t *light = &state->lights[l]; + if (!light->enabled) { + continue; + } + + // Spotlight + float spot = 1.0f; + if (light->spot_cutoff_cos >= 0.0f) { + GLfloat plv[3]; + gl_homogeneous_unit_diff(plv, light->position, v); + + GLfloat s[3]; + gl_normalize(s, light->direction); + + float plvds = gl_clamped_dot(plv, s); + + if (plvds < light->spot_cutoff_cos) { + // Outside of spotlight cutoff + continue; + } + + spot = powf(plvds, light->spot_exponent); + } + + // Attenuation + float att = 1.0f; + if (light->position[3] != 0.0f) { + GLfloat diff[3] = { + v[0] - light->position[0], + v[1] - light->position[1], + v[2] - light->position[2], + }; + float dsq = gl_mag2(diff); + float d = sqrtf(dsq); + att = 1.0f / (light->constant_attenuation + light->linear_attenuation * d + light->quadratic_attenuation * dsq); + } + + // Light ambient color + GLfloat col[3] = { + ambient[0] * light->ambient[0], + ambient[1] * light->ambient[1], + ambient[2] * light->ambient[2], + }; + + GLfloat vpl[3]; + gl_homogeneous_unit_diff(vpl, v, light->position); + + float ndvp = gl_clamped_dot(n, vpl); + + // Diffuse + col[0] += diffuse[0] * light->diffuse[0] * ndvp; + col[1] += diffuse[1] * light->diffuse[1] * ndvp; + col[2] += diffuse[2] * light->diffuse[2] * ndvp; + + GLfloat spec_mix[3] = { + specular[0] * light->specular[0], + specular[1] * light->specular[1], + specular[2] * light->specular[2] + }; + + bool spec_any = spec_mix[0] != 0.0f || spec_mix[1] != 0.0f || spec_mix[2] != 0.0f; + + // Specular + if (ndvp != 0.0f && spec_any) { + GLfloat h[3] = { + vpl[0], + vpl[1], + vpl[2], + }; + if (state->light_model_local_viewer) { + GLfloat pe[4] = { 0, 0, 0, 1 }; + gl_homogeneous_unit_diff(pe, v, pe); + h[0] += pe[0]; + h[1] += pe[1]; + h[2] += pe[2]; + } else { + h[2] += 1; + } + gl_normalize(h, h); + + float ndh = gl_clamped_dot(n, h); + float spec_factor = powf(ndh, material->shininess); + + col[0] += spec_mix[0] * spec_factor; + col[1] += spec_mix[1] * spec_factor; + col[2] += spec_mix[2] * spec_factor; + } + + float light_factor = att * spot; + + color[0] += col[0] * light_factor; + color[1] += col[1] * light_factor; + color[2] += col[2] * light_factor; + } +} + +bool gl_validate_material_face(GLenum face) +{ + switch (face) { + case GL_FRONT_AND_BACK: + return true; + case GL_FRONT: + case GL_BACK: + assertf(0, "Separate materials for front and back faces are not supported!"); + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid material face", face); + return false; + } +} + +void gl_set_color_cpu(GLfloat *dst, GLfloat r, GLfloat g, GLfloat b, GLfloat a) +{ + dst[0] = r; + dst[1] = g; + dst[2] = b; + dst[3] = a; +} + +void gl_set_color(GLfloat *dst, uint32_t offset, GLfloat r, GLfloat g, GLfloat b, GLfloat a) +{ + int16_t r_fx = FLOAT_TO_I16(r); + int16_t g_fx = FLOAT_TO_I16(g); + int16_t b_fx = FLOAT_TO_I16(b); + int16_t a_fx = FLOAT_TO_I16(a); + + uint64_t packed = ((uint64_t)r_fx << 48) | ((uint64_t)g_fx << 32) | ((uint64_t)b_fx << 16) | (uint64_t)a_fx; + gl_set_long(GL_UPDATE_NONE, offset, packed); + gl_set_color_cpu(dst, r, g, b, a); +} + +void gl_set_material_ambient(GLfloat r, GLfloat g, GLfloat b, GLfloat a) +{ + gl_set_color(state->material.ambient, offsetof(gl_server_state_t, mat_ambient), r, g, b, a); +} + +void gl_set_material_diffuse(GLfloat r, GLfloat g, GLfloat b, GLfloat a) +{ + gl_set_color(state->material.diffuse, offsetof(gl_server_state_t, mat_diffuse), r, g, b, a); +} + +void gl_set_material_specular(GLfloat r, GLfloat g, GLfloat b, GLfloat a) +{ + gl_set_color(state->material.specular, offsetof(gl_server_state_t, mat_specular), r, g, b, a); + set_can_use_rsp_dirty(); +} + +void gl_set_material_emissive(GLfloat r, GLfloat g, GLfloat b, GLfloat a) +{ + gl_set_color(state->material.emissive, offsetof(gl_server_state_t, mat_emissive), r, g, b, a); +} + +void gl_set_material_shininess(GLfloat param) +{ + state->material.shininess = param; + gl_set_short(GL_UPDATE_NONE, offsetof(gl_server_state_t, mat_shininess), param * 32.f); +} + +void gl_set_material_paramf(GLenum pname, const GLfloat *params) +{ + switch (pname) { + case GL_AMBIENT: + gl_set_material_ambient(params[0], params[1], params[2], params[3]); + break; + case GL_DIFFUSE: + gl_set_material_diffuse(params[0], params[1], params[2], params[3]); + break; + case GL_AMBIENT_AND_DIFFUSE: + gl_set_material_ambient(params[0], params[1], params[2], params[3]); + gl_set_material_diffuse(params[0], params[1], params[2], params[3]); + break; + case GL_SPECULAR: + gl_set_material_specular(params[0], params[1], params[2], params[3]); + break; + case GL_EMISSION: + gl_set_material_emissive(params[0], params[1], params[2], params[3]); + break; + case GL_SHININESS: + gl_set_material_shininess(params[0]); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void gl_set_material_parami(GLenum pname, const GLint *params) +{ + switch (pname) { + case GL_AMBIENT: + gl_set_material_ambient( + I32_TO_FLOAT(params[0]), + I32_TO_FLOAT(params[1]), + I32_TO_FLOAT(params[2]), + I32_TO_FLOAT(params[3])); + break; + case GL_DIFFUSE: + gl_set_material_diffuse( + I32_TO_FLOAT(params[0]), + I32_TO_FLOAT(params[1]), + I32_TO_FLOAT(params[2]), + I32_TO_FLOAT(params[3])); + break; + case GL_AMBIENT_AND_DIFFUSE: + gl_set_material_ambient( + I32_TO_FLOAT(params[0]), + I32_TO_FLOAT(params[1]), + I32_TO_FLOAT(params[2]), + I32_TO_FLOAT(params[3])); + gl_set_material_diffuse( + I32_TO_FLOAT(params[0]), + I32_TO_FLOAT(params[1]), + I32_TO_FLOAT(params[2]), + I32_TO_FLOAT(params[3])); + break; + case GL_SPECULAR: + gl_set_material_specular( + I32_TO_FLOAT(params[0]), + I32_TO_FLOAT(params[1]), + I32_TO_FLOAT(params[2]), + I32_TO_FLOAT(params[3])); + break; + case GL_EMISSION: + gl_set_material_emissive( + I32_TO_FLOAT(params[0]), + I32_TO_FLOAT(params[1]), + I32_TO_FLOAT(params[2]), + I32_TO_FLOAT(params[3])); + break; + case GL_SHININESS: + gl_set_material_shininess(params[0]); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void glMaterialf(GLenum face, GLenum pname, GLfloat param) +{ + switch (pname) { + case GL_SHININESS: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } + + if (!gl_validate_material_face(face)) { + return; + } + + gl_set_material_paramf(pname, ¶m); +} + +void glMateriali(GLenum face, GLenum pname, GLint param) { glMaterialf(face, pname, param); } + +void glMaterialiv(GLenum face, GLenum pname, const GLint *params) +{ + switch (pname) { + case GL_AMBIENT: + case GL_DIFFUSE: + case GL_AMBIENT_AND_DIFFUSE: + case GL_SPECULAR: + case GL_EMISSION: + case GL_SHININESS: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } + + if (!gl_validate_material_face(face)) { + return; + } + + gl_set_material_parami(pname, params); +} + +void glMaterialfv(GLenum face, GLenum pname, const GLfloat *params) +{ + switch (pname) { + case GL_AMBIENT: + case GL_DIFFUSE: + case GL_AMBIENT_AND_DIFFUSE: + case GL_SPECULAR: + case GL_EMISSION: + case GL_SHININESS: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } + + if (!gl_validate_material_face(face)) { + return; + } + + gl_set_material_paramf(pname, params); +} + +uint32_t gl_get_light_offset(GLenum light) +{ + uint32_t light_index = light - GL_LIGHT0; + return offsetof(gl_server_state_t, lights) + light_index * sizeof(int16_t) * 4; +} + +gl_light_t * gl_get_light(GLenum light) +{ + if (light < GL_LIGHT0 || light > GL_LIGHT7) { + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid light (Must be in [GL_LIGHT0, GL_LIGHT7])", light); + return NULL; + } + + return &state->lights[light - GL_LIGHT0]; +} + +void gl_light_set_ambient(gl_light_t *light, uint32_t offset, GLfloat r, GLfloat g, GLfloat b, GLfloat a) +{ + gl_set_color(light->ambient, offset + offsetof(gl_lights_soa_t, ambient), r, g, b, a); +} + +void gl_light_set_diffuse(gl_light_t *light, uint32_t offset, GLfloat r, GLfloat g, GLfloat b, GLfloat a) +{ + gl_set_color(light->diffuse, offset + offsetof(gl_lights_soa_t, diffuse), r, g, b, a); +} + +void gl_light_set_specular(gl_light_t *light, uint32_t offset, GLfloat r, GLfloat g, GLfloat b, GLfloat a) +{ + gl_set_color_cpu(light->specular, r, g, b, a); +} + +void gl_light_set_position(gl_light_t *light, uint32_t offset, const GLfloat *pos) +{ + gl_matrix_mult(light->position, gl_matrix_stack_get_matrix(&state->modelview_stack), pos); + + int16_t x, y, z, w; + + if (pos[3] == 0.0f) { + // Light is directional + // -> Pre-normalize so the ucode doesn't need to + float mag = gl_mag(pos); + x = (pos[0] / mag) * 0x7FFF; + y = (pos[1] / mag) * 0x7FFF; + z = (pos[2] / mag) * 0x7FFF; + w = 0; + } else { + // Light is positional + // -> Convert to s10.5 to match with object space position + x = pos[0] * 32.f; + y = pos[1] * 32.f; + z = pos[2] * 32.f; + w = 32.f; + } + + uint32_t packed0 = ((uint32_t)x) << 16 | (uint32_t)y; + uint32_t packed1 = ((uint32_t)z) << 16 | (uint32_t)w; + + gl_write(GL_CMD_SET_LIGHT_POS, offset, packed0, packed1); +} + +void gl_light_set_direction(gl_light_t *light, uint32_t offset, const GLfloat *dir) +{ + gl_matrix_mult3x3(light->direction, gl_matrix_stack_get_matrix(&state->modelview_stack), dir); + +/* + int16_t x = dir[0] * 0x7FFF; + int16_t y = dir[1] * 0x7FFF; + int16_t z = dir[2] * 0x7FFF; + + uint32_t packed0 = ((uint32_t)x) << 16 | (uint32_t)y; + uint32_t packed1 = ((uint32_t)z) << 16; + + gl_write(GL_CMD_SET_LIGHT_DIR, offset, packed0, packed1); +*/ +} + +void gl_light_set_spot_exponent(gl_light_t *light, uint32_t offset, float param) +{ + light->spot_exponent = param; + //gl_set_byte(GL_UPDATE_NONE, offset + offsetof(gl_light_srv_t, spot_exponent), param); +} + +void gl_light_set_spot_cutoff(gl_light_t *light, uint32_t offset, float param) +{ + light->spot_cutoff_cos = cosf(RADIANS(param)); + //gl_set_short(GL_UPDATE_NONE, offset + offsetof(gl_light_srv_t, spot_cutoff_cos), light->spot_cutoff_cos * 0x7FFF); + set_can_use_rsp_dirty(); +} + +void gl_light_set_constant_attenuation(gl_light_t *light, uint32_t offset, float param) +{ + light->constant_attenuation = param; + // Shifted right by 1 to compensate for vrcp + uint32_t fx = param * (1<<15); + gl_set_short(GL_UPDATE_NONE, offset + offsetof(gl_lights_soa_t, attenuation_int) + 0, fx >> 16); + gl_set_short(GL_UPDATE_NONE, offset + offsetof(gl_lights_soa_t, attenuation_frac) + 0, fx & 0xFFFF); +} + +void gl_light_set_linear_attenuation(gl_light_t *light, uint32_t offset, float param) +{ + light->linear_attenuation = param; + // Shifted right by 4 to compensate for various precision shifts (see rsp_gl_lighting.inc) + // Shifted right by 1 to compensate for vrcp + // Result: Shifted right by 5 + uint32_t fx = param * (1 << (16 - 5)); + gl_set_short(GL_UPDATE_NONE, offset + offsetof(gl_lights_soa_t, attenuation_int) + 2, fx >> 16); + gl_set_short(GL_UPDATE_NONE, offset + offsetof(gl_lights_soa_t, attenuation_frac) + 2, fx & 0xFFFF); +} + +void gl_light_set_quadratic_attenuation(gl_light_t *light, uint32_t offset, float param) +{ + light->quadratic_attenuation = param; + // Shifted left by 6 to compensate for various precision shifts (see rsp_gl_lighting.inc) + // Shifted right by 1 to compensate for vrcp + // Result: Shifted left by 5 + uint32_t fx = param * (1 << (16 + 5)); + gl_set_short(GL_UPDATE_NONE, offset + offsetof(gl_lights_soa_t, attenuation_int) + 4, fx >> 16); + gl_set_short(GL_UPDATE_NONE, offset + offsetof(gl_lights_soa_t, attenuation_frac) + 4, fx & 0xFFFF); +} + +void glLightf(GLenum light, GLenum pname, GLfloat param) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_light_t *l = gl_get_light(light); + if (l == NULL) { + return; + } + + uint32_t offset = gl_get_light_offset(light); + + switch (pname) { + case GL_SPOT_EXPONENT: + gl_light_set_spot_exponent(l, offset, param); + break; + case GL_SPOT_CUTOFF: + gl_light_set_spot_cutoff(l, offset, param); + break; + case GL_CONSTANT_ATTENUATION: + gl_light_set_constant_attenuation(l, offset, param); + break; + case GL_LINEAR_ATTENUATION: + gl_light_set_linear_attenuation(l, offset, param); + break; + case GL_QUADRATIC_ATTENUATION: + gl_light_set_quadratic_attenuation(l, offset, param); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void glLighti(GLenum light, GLenum pname, GLint param) +{ + if (!gl_ensure_no_begin_end()) return; + + glLightf(light, pname, param); +} + +void glLightiv(GLenum light, GLenum pname, const GLint *params) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_light_t *l = gl_get_light(light); + if (l == NULL) { + return; + } + + uint32_t offset = gl_get_light_offset(light); + + GLfloat tmp[4]; + + switch (pname) { + case GL_AMBIENT: + gl_light_set_ambient(l, offset, + I32_TO_FLOAT(params[0]), + I32_TO_FLOAT(params[1]), + I32_TO_FLOAT(params[2]), + I32_TO_FLOAT(params[3])); + break; + case GL_DIFFUSE: + gl_light_set_diffuse(l, offset, + I32_TO_FLOAT(params[0]), + I32_TO_FLOAT(params[1]), + I32_TO_FLOAT(params[2]), + I32_TO_FLOAT(params[3])); + break; + case GL_SPECULAR: + gl_light_set_specular(l, offset, + I32_TO_FLOAT(params[0]), + I32_TO_FLOAT(params[1]), + I32_TO_FLOAT(params[2]), + I32_TO_FLOAT(params[3])); + break; + case GL_POSITION: + tmp[0] = params[0]; + tmp[1] = params[1]; + tmp[2] = params[2]; + tmp[3] = params[3]; + gl_light_set_position(l, offset, tmp); + break; + case GL_SPOT_DIRECTION: + tmp[0] = params[0]; + tmp[1] = params[1]; + tmp[2] = params[2]; + gl_light_set_direction(l, offset, tmp); + break; + case GL_SPOT_EXPONENT: + gl_light_set_spot_exponent(l, offset, params[0]); + break; + case GL_SPOT_CUTOFF: + gl_light_set_spot_cutoff(l, offset, params[0]); + break; + case GL_CONSTANT_ATTENUATION: + gl_light_set_constant_attenuation(l, offset, params[0]); + break; + case GL_LINEAR_ATTENUATION: + gl_light_set_linear_attenuation(l, offset, params[0]); + break; + case GL_QUADRATIC_ATTENUATION: + gl_light_set_quadratic_attenuation(l, offset, params[0]); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void glLightfv(GLenum light, GLenum pname, const GLfloat *params) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_light_t *l = gl_get_light(light); + if (l == NULL) { + return; + } + + uint32_t offset = gl_get_light_offset(light); + + switch (pname) { + case GL_AMBIENT: + gl_light_set_ambient(l, offset, params[0], params[1], params[2], params[3]); + break; + case GL_DIFFUSE: + gl_light_set_diffuse(l, offset, params[0], params[1], params[2], params[3]); + break; + case GL_SPECULAR: + gl_light_set_specular(l, offset, params[0], params[1], params[2], params[3]); + break; + case GL_POSITION: + gl_light_set_position(l, offset, params); + break; + case GL_SPOT_DIRECTION: + gl_light_set_direction(l, offset, params); + break; + case GL_SPOT_EXPONENT: + gl_light_set_spot_exponent(l, offset, params[0]); + break; + case GL_SPOT_CUTOFF: + gl_light_set_spot_cutoff(l, offset, params[0]); + break; + case GL_CONSTANT_ATTENUATION: + gl_light_set_constant_attenuation(l, offset, params[0]); + break; + case GL_LINEAR_ATTENUATION: + gl_light_set_linear_attenuation(l, offset, params[0]); + break; + case GL_QUADRATIC_ATTENUATION: + gl_light_set_quadratic_attenuation(l, offset, params[0]); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void gl_set_light_model_local_viewer(bool param) +{ + state->light_model_local_viewer = param; +} + +void gl_set_light_model_ambient(GLfloat r, GLfloat g, GLfloat b, GLfloat a) +{ + gl_set_color(state->light_model_ambient, offsetof(gl_server_state_t, light_ambient), r, g, b, a); +} + +void glLightModeli(GLenum pname, GLint param) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (pname) { + case GL_LIGHT_MODEL_LOCAL_VIEWER: + gl_set_light_model_local_viewer(param != 0); + break; + case GL_LIGHT_MODEL_TWO_SIDE: + assertf(0, "Two sided lighting is not supported!"); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} +void glLightModelf(GLenum pname, GLfloat param) +{ + if (!gl_ensure_no_begin_end()) return; + + glLightModeli(pname, param); +} + +void glLightModeliv(GLenum pname, const GLint *params) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (pname) { + case GL_LIGHT_MODEL_AMBIENT: + gl_set_light_model_ambient( + I32_TO_FLOAT(params[0]), + I32_TO_FLOAT(params[1]), + I32_TO_FLOAT(params[2]), + I32_TO_FLOAT(params[3])); + break; + case GL_LIGHT_MODEL_LOCAL_VIEWER: + gl_set_light_model_local_viewer(params[0] != 0); + break; + case GL_LIGHT_MODEL_TWO_SIDE: + assertf(0, "Two sided lighting is not supported!"); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void glLightModelfv(GLenum pname, const GLfloat *params) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (pname) { + case GL_LIGHT_MODEL_AMBIENT: + gl_set_light_model_ambient(params[0], params[1], params[2], params[3]); + break; + case GL_LIGHT_MODEL_LOCAL_VIEWER: + gl_set_light_model_local_viewer(params[0] != 0); + break; + case GL_LIGHT_MODEL_TWO_SIDE: + assertf(0, "Two sided lighting is not supported!"); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void glColorMaterial(GLenum face, GLenum mode) +{ + if (!gl_ensure_no_begin_end()) return; + + if (!gl_validate_material_face(face)) { + return; + } + + uint64_t color_target = 0; + + switch (mode) { + case GL_AMBIENT: + color_target |= 1ULL << 48; + break; + case GL_DIFFUSE: + color_target |= 1ULL << 32; + break; + case GL_SPECULAR: + case GL_EMISSION: + color_target |= 1ULL << 16; + break; + case GL_AMBIENT_AND_DIFFUSE: + color_target |= 1ULL << 48; + color_target |= 1ULL << 32; + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid color material mode", mode); + return; + } + + gl_set_long(GL_UPDATE_NONE, offsetof(gl_server_state_t, mat_color_target), color_target); + state->material.color_target = mode; +} + +void glShadeModel(GLenum mode) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (mode) { + case GL_FLAT: + case GL_SMOOTH: + gl_set_short(GL_UPDATE_NONE, offsetof(gl_server_state_t, shade_model), mode); + state->shade_model = mode; + set_can_use_rsp_dirty(); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid shade model", mode); + return; + } +} diff --git a/src/GL/list.c b/src/GL/list.c new file mode 100644 index 000000000..f6bf4e119 --- /dev/null +++ b/src/GL/list.c @@ -0,0 +1,248 @@ +/** + * @file list.c + * @author Dennis Heinze + * @brief OpenGL display list management and execution. + */ +#include "gl_internal.h" +#include "rspq.h" + +#define EMPTY_LIST ((rspq_block_t*)1) + +extern gl_state_t *state; + +typedef GLuint (*read_list_id_func)(const GLvoid*, GLsizei); + +inline bool is_non_empty_list(rspq_block_t *block) +{ + return block != NULL && block != EMPTY_LIST; +} + +void block_free_safe(rspq_block_t *block) +{ + // Silently ignore NULL and EMPTY_LIST + if (!is_non_empty_list(block)) return; + rdpq_call_deferred((void (*)(void*))rspq_block_free, block); +} + +void gl_list_init() +{ + // TODO: Get rid of the hash map. This will be difficult due to the semantics of glGenLists (it's guaranteed to generate consecutive IDs) + obj_map_new(&state->list_objects); + state->next_list_name = 1; +} + +void gl_list_close() +{ + obj_map_iter_t list_iter = obj_map_iterator(&state->list_objects); + while (obj_map_iterator_next(&list_iter)) { + block_free_safe((rspq_block_t*)list_iter.value); + } + + obj_map_free(&state->list_objects); +} + +void glNewList(GLuint n, GLenum mode) +{ + if (!gl_ensure_no_begin_end()) return; + + if (n == 0) { + gl_set_error(GL_INVALID_VALUE, "Display list ID must not be 0"); + return; + } + + switch (mode) { + case GL_COMPILE: + break; + case GL_COMPILE_AND_EXECUTE: + assertf(0, "Compile and execute is not supported!"); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid display list compilation mode", mode); + return; + } + + if (state->current_list != 0) { + gl_set_error(GL_INVALID_OPERATION, "A display list is already being recorded"); + return; + } + + state->current_list = n; + + rspq_block_begin(); +} + +void glEndList(void) +{ + if (!gl_ensure_no_begin_end()) return; + + if (state->current_list == 0) { + gl_set_error(GL_INVALID_OPERATION, "No display list is currently being recorded"); + return; + } + + rspq_block_t *block = rspq_block_end(); + + block = obj_map_set(&state->list_objects, state->current_list, block); + block_free_safe(block); + + state->current_list = 0; +} + +void glCallList(GLuint n) +{ + // The spec allows glCallList in within glBegin/glEnd pairs, but our current architecture doesn't allow for this. + // During display list recording, we cannot anticipate whether it will be called within a glBegin/glEnd pair or not. + assertf(!state->begin_end_active, "glCallList between glBegin/glEnd is not supported!"); + + rspq_block_t *block = obj_map_get(&state->list_objects, n); + // Silently ignore NULL and EMPTY_LIST + if (is_non_empty_list(block)) { + rspq_block_run(block); + } +} + +GLuint gl_get_list_name_byte(const GLvoid *lists, GLsizei n) +{ + return ((const GLbyte*)lists)[n]; +} + +GLuint gl_get_list_name_ubyte(const GLvoid *lists, GLsizei n) +{ + return ((const GLubyte*)lists)[n]; +} + +GLuint gl_get_list_name_short(const GLvoid *lists, GLsizei n) +{ + return ((const GLshort*)lists)[n]; +} + +GLuint gl_get_list_name_ushort(const GLvoid *lists, GLsizei n) +{ + return ((const GLushort*)lists)[n]; +} + +GLuint gl_get_list_name_int(const GLvoid *lists, GLsizei n) +{ + return ((const GLint*)lists)[n]; +} + +GLuint gl_get_list_name_uint(const GLvoid *lists, GLsizei n) +{ + return ((const GLuint*)lists)[n]; +} + +GLuint gl_get_list_name_float(const GLvoid *lists, GLsizei n) +{ + return ((const GLfloat*)lists)[n]; +} + +GLuint gl_get_list_name_2bytes(const GLvoid *lists, GLsizei n) +{ + GLubyte l0 = ((const GLubyte*)lists)[n*2+0]; + GLubyte l1 = ((const GLubyte*)lists)[n*2+1]; + return ((GLuint)l0) * 255 + ((GLuint)l1); +} + +GLuint gl_get_list_name_3bytes(const GLvoid *lists, GLsizei n) +{ + GLubyte l0 = ((const GLubyte*)lists)[n*3+0]; + GLubyte l1 = ((const GLubyte*)lists)[n*3+1]; + GLubyte l2 = ((const GLubyte*)lists)[n*3+2]; + return ((GLuint)l0) * 65536 + ((GLuint)l1) * 255 + ((GLuint)l2); +} + +GLuint gl_get_list_name_4bytes(const GLvoid *lists, GLsizei n) +{ + GLubyte l0 = ((const GLubyte*)lists)[n*4+0]; + GLubyte l1 = ((const GLubyte*)lists)[n*4+1]; + GLubyte l2 = ((const GLubyte*)lists)[n*4+2]; + GLubyte l3 = ((const GLubyte*)lists)[n*4+3]; + return ((GLuint)l0) * 16777216 + ((GLuint)l1) * 65536 + ((GLuint)l2) * 255 + ((GLuint)l3); +} + +read_list_id_func get_read_list_id_func(GLenum type) +{ + switch (type) { + case GL_BYTE: + return gl_get_list_name_byte; + case GL_UNSIGNED_BYTE: + return gl_get_list_name_ubyte; + case GL_SHORT: + return gl_get_list_name_short; + case GL_UNSIGNED_SHORT: + return gl_get_list_name_ushort; + case GL_INT: + return gl_get_list_name_int; + case GL_UNSIGNED_INT: + return gl_get_list_name_uint; + case GL_FLOAT: + return gl_get_list_name_float; + case GL_2_BYTES: + return gl_get_list_name_2bytes; + case GL_3_BYTES: + return gl_get_list_name_3bytes; + case GL_4_BYTES: + return gl_get_list_name_4bytes; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid display list ID type", type); + return NULL; + } +} + +void glCallLists(GLsizei n, GLenum type, const GLvoid *lists) +{ + // See glCallList for an explanation + assertf(!state->begin_end_active, "glCallLists between glBegin/glEnd is not supported!"); + + read_list_id_func func = get_read_list_id_func(type); + if (func == NULL) return; + + for (GLsizei i = 0; i < n; i++) + { + GLuint l = func(lists, i); + glCallList(l + state->list_base); + } +} + +void glListBase(GLuint base) +{ + if (!gl_ensure_no_begin_end()) return; + + state->list_base = base; +} + +GLuint glGenLists(GLsizei s) +{ + if (!gl_ensure_no_begin_end()) return 0; + + if (s == 0) return 0; + + GLuint result = state->next_list_name; + + // Set newly used indices to empty lists (which marks them as used without actually creating a block) + for (size_t i = 0; i < s; i++) + { + obj_map_set(&state->list_objects, state->next_list_name++, EMPTY_LIST); + } + + return result; +} + +GLboolean glIsList(GLuint list) +{ + if (!gl_ensure_no_begin_end()) return 0; + + // We do not check for EMPTY_LIST here because that also denotes a used list index + return obj_map_get(&state->list_objects, list) != NULL; +} + +void glDeleteLists(GLuint list, GLsizei range) +{ + if (!gl_ensure_no_begin_end()) return; + + for (GLuint i = 0; i < range; i++) + { + rspq_block_t *block = obj_map_remove(&state->list_objects, list + i); + block_free_safe(block); + } +} diff --git a/src/GL/matrix.c b/src/GL/matrix.c new file mode 100644 index 000000000..4ed70ad9b --- /dev/null +++ b/src/GL/matrix.c @@ -0,0 +1,465 @@ +/** + * @file matrix.c + * @author Dennis Heinze + * @brief OpenGL matrix stack and transformation operations. + */ +#include "gl_internal.h" +#include + +extern gl_state_t *state; + +void gl_matrix_init() +{ + state->modelview_stack = (gl_matrix_stack_t) { + .storage = state->modelview_stack_storage, + .size = MODELVIEW_STACK_SIZE, + }; + + state->default_matrix_target.mv_stack = &state->modelview_stack; + + state->projection_stack = (gl_matrix_stack_t) { + .storage = state->projection_stack_storage, + .size = PROJECTION_STACK_SIZE, + }; + + state->texture_stack = (gl_matrix_stack_t) { + .storage = state->texture_stack_storage, + .size = TEXTURE_STACK_SIZE, + }; + + for (uint32_t i = 0; i < MATRIX_PALETTE_SIZE; i++) { + state->palette_stacks[i] = (gl_matrix_stack_t) { + .storage = state->palette_stack_storage[i], + .size = PALETTE_STACK_SIZE, + }; + + state->palette_matrix_targets[i].mv_stack = &state->palette_stacks[i]; + } + + glMatrixMode(GL_MATRIX_PALETTE_ARB); + for (uint32_t i = 0; i < MATRIX_PALETTE_SIZE; i++) { + glCurrentPaletteMatrixARB(i); + glLoadIdentity(); + } + + glMatrixMode(GL_TEXTURE); + glLoadIdentity(); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); +} + +gl_matrix_t * gl_matrix_stack_get_matrix(gl_matrix_stack_t *stack) +{ + return &stack->storage[stack->cur_depth]; +} + +void gl_update_current_matrix() +{ + state->current_matrix = gl_matrix_stack_get_matrix(state->current_matrix_stack); +} + +void gl_matrix_mult(GLfloat *d, const gl_matrix_t *m, const GLfloat *v) +{ + d[0] = m->m[0][0] * v[0] + m->m[1][0] * v[1] + m->m[2][0] * v[2] + m->m[3][0] * v[3]; + d[1] = m->m[0][1] * v[0] + m->m[1][1] * v[1] + m->m[2][1] * v[2] + m->m[3][1] * v[3]; + d[2] = m->m[0][2] * v[0] + m->m[1][2] * v[1] + m->m[2][2] * v[2] + m->m[3][2] * v[3]; + d[3] = m->m[0][3] * v[0] + m->m[1][3] * v[1] + m->m[2][3] * v[2] + m->m[3][3] * v[3]; +} + +void gl_matrix_mult3x3(GLfloat *d, const gl_matrix_t *m, const GLfloat *v) +{ + d[0] = m->m[0][0] * v[0] + m->m[1][0] * v[1] + m->m[2][0] * v[2]; + d[1] = m->m[0][1] * v[0] + m->m[1][1] * v[1] + m->m[2][1] * v[2]; + d[2] = m->m[0][2] * v[0] + m->m[1][2] * v[1] + m->m[2][2] * v[2]; +} + +void gl_matrix_mult4x2(GLfloat *d, const gl_matrix_t *m, const GLfloat *v) +{ + d[0] = m->m[0][0] * v[0] + m->m[1][0] * v[1] + m->m[2][0] * v[2] + m->m[3][0] * v[3]; + d[1] = m->m[0][1] * v[0] + m->m[1][1] * v[1] + m->m[2][1] * v[2] + m->m[3][1] * v[3]; +} + +void gl_matrix_mult_full(gl_matrix_t *d, const gl_matrix_t *l, const gl_matrix_t *r) +{ + gl_matrix_mult(d->m[0], l, r->m[0]); + gl_matrix_mult(d->m[1], l, r->m[1]); + gl_matrix_mult(d->m[2], l, r->m[2]); + gl_matrix_mult(d->m[3], l, r->m[3]); +} + +void gl_update_matrix_target(gl_matrix_target_t *target) +{ + if (target->is_mvp_dirty) { + gl_matrix_mult_full(&target->mvp, gl_matrix_stack_get_matrix(&state->projection_stack), gl_matrix_stack_get_matrix(target->mv_stack)); + target->is_mvp_dirty = false; + } +} + +void gl_update_matrix_targets() +{ + if (state->matrix_palette_enabled) { + for (uint32_t i = 0; i < MATRIX_PALETTE_SIZE; i++) + { + gl_update_matrix_target(&state->palette_matrix_targets[i]); + } + } else { + gl_update_matrix_target(&state->default_matrix_target); + } +} + +void gl_update_current_matrix_stack() +{ + switch (state->matrix_mode) { + case GL_MODELVIEW: + state->current_matrix_stack = &state->modelview_stack; + state->current_matrix_target = &state->default_matrix_target; + break; + case GL_PROJECTION: + state->current_matrix_stack = &state->projection_stack; + state->current_matrix_target = NULL; + break; + case GL_TEXTURE: + state->current_matrix_stack = &state->texture_stack; + state->current_matrix_target = NULL; + break; + case GL_MATRIX_PALETTE_ARB: + state->current_matrix_stack = &state->palette_stacks[state->current_palette_matrix]; + state->current_matrix_target = &state->palette_matrix_targets[state->current_palette_matrix]; + break; + } + + gl_update_current_matrix(); +} + +void glMatrixMode(GLenum mode) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (mode) { + case GL_MODELVIEW: + case GL_PROJECTION: + case GL_TEXTURE: + case GL_MATRIX_PALETTE_ARB: + state->matrix_mode = mode; + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid matrix mode", mode); + return; + } + + gl_update_current_matrix_stack(); + + gl_set_short(GL_UPDATE_NONE, offsetof(gl_server_state_t, matrix_mode), mode); +} + +void glCurrentPaletteMatrixARB(GLint index) +{ + if (!gl_ensure_no_begin_end()) return; + + if (index < 0 || index >= MATRIX_PALETTE_SIZE) { + gl_set_error(GL_INVALID_VALUE, "%#04lx is not a valid palette matrix index (Must be in [0, %d])", index, MATRIX_PALETTE_SIZE-1); + return; + } + + state->current_palette_matrix = index; + gl_update_current_matrix_stack(); + gl_set_palette_idx(index); +} + +static inline void write_shorts(rspq_write_t *w, const uint16_t *s, uint32_t count) +{ + for (uint32_t i = 0; i < count; i += 2) + { + uint32_t packed = ((uint32_t)s[i] << 16) | (uint32_t)s[i+1]; + rspq_write_arg(w, packed); + } +} + +static inline void gl_matrix_write(rspq_write_t *w, const GLfloat *m) +{ + uint16_t integer[16]; + uint16_t fraction[16]; + + for (uint32_t i = 0; i < 16; i++) + { + int32_t fixed = m[i] * (1<<16); + integer[i] = (uint16_t)((fixed & 0xFFFF0000) >> 16); + fraction[i] = (uint16_t)(fixed & 0x0000FFFF); + } + + write_shorts(w, integer, 16); + write_shorts(w, fraction, 16); +} + +static inline void gl_matrix_load_rsp(const GLfloat *m, bool multiply) +{ + rspq_write_t w = rspq_write_begin(gl_overlay_id, GL_CMD_MATRIX_LOAD, 17); + rspq_write_arg(&w, multiply ? 1 : 0); + gl_matrix_write(&w, m); + rspq_write_end(&w); +} + +static void gl_mark_matrix_target_dirty() +{ + if (state->current_matrix_target != NULL) { + state->current_matrix_target->is_mvp_dirty = true; + } else if (state->current_matrix_stack == &state->projection_stack) { + state->default_matrix_target.is_mvp_dirty = true; + for (uint32_t i = 0; i < MATRIX_PALETTE_SIZE; i++) + { + state->palette_matrix_targets[i].is_mvp_dirty = true; + } + } +} + +void gl_load_matrix(const GLfloat *m) +{ + memcpy(state->current_matrix, m, sizeof(gl_matrix_t)); + gl_mark_matrix_target_dirty(); + gl_matrix_load_rsp(m, false); +} + +void glLoadMatrixf(const GLfloat *m) +{ + if (!gl_ensure_no_begin_end()) return; + gl_load_matrix(m); +} + +void glLoadMatrixd(const GLdouble *m) +{ + if (!gl_ensure_no_begin_end()) return; + + GLfloat tmp[16]; + for (size_t i = 0; i < 16; i++) + { + tmp[i] = m[i]; + } + gl_load_matrix(tmp); +} + +void gl_mult_matrix(const GLfloat *m) +{ + gl_matrix_t tmp = *state->current_matrix; + gl_matrix_mult_full(state->current_matrix, &tmp, (gl_matrix_t*)m); + gl_mark_matrix_target_dirty(); + + gl_matrix_load_rsp(m, true); +} + +void glMultMatrixf(const GLfloat *m) +{ + if (!gl_ensure_no_begin_end()) return; + gl_mult_matrix(m); +} + +void glMultMatrixd(const GLdouble *m) +{ + if (!gl_ensure_no_begin_end()) return; + + GLfloat tmp[16]; + for (size_t i = 0; i < 16; i++) + { + tmp[i] = m[i]; + } + + gl_mult_matrix(tmp); +} + +void glLoadIdentity(void) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_matrix_t identity = (gl_matrix_t){ .m={ + {1,0,0,0}, + {0,1,0,0}, + {0,0,1,0}, + {0,0,0,1}, + }}; + + gl_load_matrix(identity.m[0]); +} + +void gl_rotate(GLfloat angle, GLfloat x, GLfloat y, GLfloat z) +{ + float a = angle * (M_PI / 180.0f); + float c = cosf(a); + float s = sinf(a); + float ic = 1.f - c; + + float mag = sqrtf(x*x + y*y + z*z); + x /= mag; + y /= mag; + z /= mag; + + gl_matrix_t rotation = (gl_matrix_t){ .m={ + {x*x*ic+c, y*x*ic+z*s, z*x*ic-y*s, 0.f}, + {x*y*ic-z*s, y*y*ic+c, z*y*ic+x*s, 0.f}, + {x*z*ic+y*s, y*z*ic-x*s, z*z*ic+c, 0.f}, + {0.f, 0.f, 0.f, 1.f}, + }}; + + gl_mult_matrix(rotation.m[0]); +} + +void glRotatef(GLfloat angle, GLfloat x, GLfloat y, GLfloat z) +{ + if (!gl_ensure_no_begin_end()) return; + gl_rotate(angle, x, y, z); +} + +void glRotated(GLdouble angle, GLdouble x, GLdouble y, GLdouble z) +{ + if (!gl_ensure_no_begin_end()) return; + gl_rotate(angle, x, y, z); +} + +void gl_translate(GLfloat x, GLfloat y, GLfloat z) +{ + gl_matrix_t translation = (gl_matrix_t){ .m={ + {1.f, 0.f, 0.f, 0.f}, + {0.f, 1.f, 0.f, 0.f}, + {0.f, 0.f, 1.f, 0.f}, + {x, y, z, 1.f}, + }}; + + gl_mult_matrix(translation.m[0]); +} + +void glTranslatef(GLfloat x, GLfloat y, GLfloat z) +{ + if (!gl_ensure_no_begin_end()) return; + gl_translate(x, y, z); +} + +void glTranslated(GLdouble x, GLdouble y, GLdouble z) +{ + if (!gl_ensure_no_begin_end()) return; + gl_translate(x, y, z); +} + +void gl_scale(GLfloat x, GLfloat y, GLfloat z) +{ + gl_matrix_t scale = (gl_matrix_t){ .m={ + {x, 0.f, 0.f, 0.f}, + {0.f, y, 0.f, 0.f}, + {0.f, 0.f, z, 0.f}, + {0.f, 0.f, 0.f, 1.f}, + }}; + + gl_mult_matrix(scale.m[0]); +} + +void glScalef(GLfloat x, GLfloat y, GLfloat z) +{ + if (!gl_ensure_no_begin_end()) return; + gl_scale(x, y, z); +} + +void glScaled(GLdouble x, GLdouble y, GLdouble z) +{ + if (!gl_ensure_no_begin_end()) return; + gl_scale(x, y, z); +} + +void glFrustum(GLdouble l, GLdouble r, GLdouble b, GLdouble t, GLdouble n, GLdouble f) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_matrix_t frustum = (gl_matrix_t){ .m={ + {(2*n)/(r-l), 0.f, 0.f, 0.f}, + {0.f, (2.f*n)/(t-b), 0.f, 0.f}, + {(r+l)/(r-l), (t+b)/(t-b), -(f+n)/(f-n), -1.f}, + {0.f, 0.f, -(2*f*n)/(f-n), 0.f}, + }}; + + gl_mult_matrix(frustum.m[0]); +} + +void glOrtho(GLdouble l, GLdouble r, GLdouble b, GLdouble t, GLdouble n, GLdouble f) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_matrix_t ortho = (gl_matrix_t){ .m={ + {2.0f/(r-l), 0.f, 0.f, 0.f}, + {0.f, 2.0f/(t-b), 0.f, 0.f}, + {0.f, 0.f, -2.0f/(f-n), 0.f}, + {-(r+l)/(r-l), -(t+b)/(t-b), -(f+n)/(f-n), 1.f}, + }}; + + gl_mult_matrix(ortho.m[0]); +} + +void glPushMatrix(void) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_matrix_stack_t *stack = state->current_matrix_stack; + + int32_t new_depth = stack->cur_depth + 1; + if (new_depth >= stack->size) { + gl_set_error(GL_STACK_OVERFLOW, "The current matrix stack has already reached the maximum depth of %ld", stack->size); + return; + } + + stack->cur_depth = new_depth; + memcpy(&stack->storage[new_depth], &stack->storage[new_depth-1], sizeof(gl_matrix_t)); + + gl_update_current_matrix(); + + gl_write(GL_CMD_MATRIX_PUSH); +} + +void glPopMatrix(void) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_matrix_stack_t *stack = state->current_matrix_stack; + + int32_t new_depth = stack->cur_depth - 1; + if (new_depth < 0) { + gl_set_error(GL_STACK_UNDERFLOW, "The current matrix stack is already at depth 0"); + return; + } + + stack->cur_depth = new_depth; + + gl_update_current_matrix(); + gl_mark_matrix_target_dirty(); + + gl_write(GL_CMD_MATRIX_POP); +} + +void glCopyMatrixN64(GLenum source) +{ + if (!gl_ensure_no_begin_end()) return; + int src_id; + gl_matrix_stack_t *matrix_stack; + switch(source) + { + case GL_MODELVIEW: + src_id = 0; + matrix_stack = &state->modelview_stack; + break; + + case GL_PROJECTION: + src_id = 1; + matrix_stack = &state->projection_stack; + break; + + case GL_TEXTURE: + src_id = 2; + matrix_stack = &state->texture_stack; + break; + + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid matrix source for copying matrices", source); + return; + } + memcpy(state->current_matrix, gl_matrix_stack_get_matrix(matrix_stack), sizeof(gl_matrix_t)); + gl_mark_matrix_target_dirty(); + gl_write(GL_CMD_MATRIX_COPY, src_id << 6); +} \ No newline at end of file diff --git a/src/GL/obj_map.c b/src/GL/obj_map.c new file mode 100644 index 000000000..da4290d86 --- /dev/null +++ b/src/GL/obj_map.c @@ -0,0 +1,176 @@ +/** + * @file obj_map.c + * @author Dennis Heinze + * @brief Simple hash map implementation for OpenGL object management. + * + * This is a very simple hash map that uses open adressing (linear probing). + * The hash function is the identity for now, since it uses integer keys. + */ + +#include "obj_map.h" + +#include +#include +#include + +#define OBJ_MAP_MIN_CAPACITY 32 +#define OBJ_MAP_DELETED_KEY 0xFFFFFFFF + +void obj_map_new(obj_map_t *map) +{ + assertf(map->entries == NULL, "Map has not been freed!"); + + map->entries = calloc(OBJ_MAP_MIN_CAPACITY, sizeof(obj_map_entry_t)); + assertf(map->entries != NULL, "Out of memory"); + map->capacity = OBJ_MAP_MIN_CAPACITY; + map->count = 0; +} + +void obj_map_free(obj_map_t *map) +{ + assertf(map->entries != NULL, "Map is not initialized!"); + + free(map->entries); + map->entries = NULL; +} + +obj_map_entry_t * obj_map_find_entry(const obj_map_t *map, uint32_t key) +{ + uint32_t mask = (map->capacity - 1); + + for (uint32_t i = 0; i < map->capacity; i++) { + obj_map_entry_t *entry = &map->entries[(key + i) & mask]; + + if (entry->key == key) { + return entry; + } + + if (entry->value == NULL && entry->key != OBJ_MAP_DELETED_KEY) { + // Stop probing when unused entry is found. + // However, keep searching if the entry has been deleted + break; + } + } + + return NULL; +} + +void * obj_map_set_without_expanding(obj_map_t *map, uint32_t key, void *value) +{ + uint32_t mask = (map->capacity - 1); + + for (uint32_t i = 0; i < map->capacity; i++) { + obj_map_entry_t *e = &map->entries[(key + i) & mask]; + + if (e->value == NULL) { + // Entry is unused or has been deleted + // -> New entry is added + e->key = key; + e->value = value; + map->count++; + return NULL; + } + + if (e->key == key) { + // Key is already present + // -> Value is changed, but no new entry is added + void *old_value = e->value; + e->value = value; + return old_value; + } + } + + assertf(0, "Map is full!"); + abort(); +} + +void obj_map_expand(obj_map_t *map) +{ + obj_map_entry_t *old_entries = map->entries; + uint32_t old_capacity = map->capacity; + + map->capacity = old_capacity << 1; + map->entries = calloc(map->capacity, sizeof(obj_map_entry_t)); + assertf(map->entries != NULL, "Out of memory"); + map->count = 0; + + // Re-populate the map with all used entries + for (uint32_t i = 0; i < old_capacity; i++) { + obj_map_entry_t *entry = &old_entries[i]; + if (entry->value != NULL) { + obj_map_set_without_expanding(map, entry->key, entry->value); + } + } + + free(old_entries); +} + +void * obj_map_get(const obj_map_t *map, uint32_t key) +{ + assertf(map->entries != NULL, "Map is not initialized!"); + + obj_map_entry_t *entry = obj_map_find_entry(map, key); + return entry == NULL ? NULL : entry->value; +} + +void * obj_map_set(obj_map_t *map, uint32_t key, void *value) +{ + assertf(map->entries != NULL, "Map is not initialized!"); + assertf(value != NULL, "Can't insert NULL into map!"); + + if (map->count * 2 > map->capacity) { + // If more than half the capacity is used, expand the map + obj_map_expand(map); + } + + return obj_map_set_without_expanding(map, key, value); +} + +void * obj_map_remove(obj_map_t *map, uint32_t key) +{ + assertf(map->entries != NULL, "Map is not initialized!"); + + obj_map_entry_t *entry = obj_map_find_entry(map, key); + + if (entry != NULL) { + void *v = entry->value; + entry->value = NULL; + // Mark the entry as deleted with magic value + entry->key = OBJ_MAP_DELETED_KEY; + map->count--; + return v; + } + + return NULL; +} + +obj_map_iter_t obj_map_iterator(obj_map_t *map) +{ + assertf(map->entries != NULL, "Map is not initialized!"); + + return (obj_map_iter_t) { + ._map = map, + ._index = 0 + }; +} + +bool obj_map_iterator_next(obj_map_iter_t *iter) +{ + assertf(iter->_map != NULL, "Map iterator is not initialized!"); + + uint32_t cur_index = iter->_index; + + while (cur_index < iter->_map->capacity) { + obj_map_entry_t *cur_entry = &iter->_map->entries[cur_index]; + cur_index++; + + if (cur_entry->value != NULL) { + iter->key = cur_entry->key; + iter->value = cur_entry->value; + iter->_index = cur_index; + return true; + } + } + + return false; +} diff --git a/src/GL/obj_map.h b/src/GL/obj_map.h new file mode 100644 index 000000000..a3181262f --- /dev/null +++ b/src/GL/obj_map.h @@ -0,0 +1,57 @@ +/** + * @file obj_map.h + * @author Dennis Heinze + */ +#ifndef _GL_OBJ_MAP_H +#define _GL_OBJ_MAP_H + +#include +#include +#include + +typedef struct { + uint32_t key; + void *value; +} obj_map_entry_t; + +typedef struct { + obj_map_entry_t *entries; + uint32_t capacity; + uint32_t count; +} obj_map_t; + +typedef struct { + uint32_t key; + void *value; + + obj_map_t *_map; + uint32_t _index; +} obj_map_iter_t; + +#ifdef __cplusplus +extern "C" { +#endif + +void obj_map_new(obj_map_t *map); +void obj_map_free(obj_map_t *map); + +inline uint32_t obj_map_count(const obj_map_t *map) +{ + assertf(map->entries != NULL, "Map is not initialized!"); + + return map->count; +} + +void * obj_map_get(const obj_map_t *map, uint32_t key); +void * obj_map_set(obj_map_t *map, uint32_t key, void *value); +void * obj_map_remove(obj_map_t *map, uint32_t key); + +obj_map_iter_t obj_map_iterator(obj_map_t *map); +bool obj_map_iterator_next(obj_map_iter_t *iter); + +#ifdef __cplusplus +} +#endif + + +#endif diff --git a/src/GL/pixelrect.c b/src/GL/pixelrect.c new file mode 100644 index 000000000..37610e9ca --- /dev/null +++ b/src/GL/pixelrect.c @@ -0,0 +1,262 @@ +/** + * @file pixelrect.c + * @author Dennis Heinze + * @brief OpenGL pixel transfer and pixel storage operations. + */ +#include "gl_internal.h" +#include + +extern gl_state_t *state; + +bool gl_calc_transfer_is_noop() +{ + if (state->map_color || state->unpack_swap_bytes) { + return false; + } + + for (uint32_t i = 0; i < 4; i++) + { + if (state->transfer_bias[i] != 0.0f || state->transfer_scale[i] != 1.0f) { + return false; + } + } + + return true; +} + +void gl_update_transfer_state() +{ + state->transfer_is_noop = gl_calc_transfer_is_noop(); +} + +void gl_pixel_init() +{ + state->unpack_alignment = 4; + state->transfer_scale[0] = 1; + state->transfer_scale[1] = 1; + state->transfer_scale[2] = 1; + state->transfer_scale[3] = 1; + + state->pixel_maps[0].size = 1; + state->pixel_maps[1].size = 1; + state->pixel_maps[2].size = 1; + state->pixel_maps[3].size = 1; + + gl_update_transfer_state(); +} + +void glPixelStorei(GLenum pname, GLint param) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (pname) { + case GL_UNPACK_SWAP_BYTES: + state->unpack_swap_bytes = param != 0; + gl_update_transfer_state(); + break; + case GL_UNPACK_LSB_FIRST: + state->unpack_lsb_first = param != 0; + break; + case GL_UNPACK_ROW_LENGTH: + if (param < 0) { + gl_set_error(GL_INVALID_VALUE, "GL_UNPACK_ROW_LENGTH must not be negative"); + return; + } + state->unpack_row_length = param; + break; + case GL_UNPACK_SKIP_ROWS: + if (param < 0) { + gl_set_error(GL_INVALID_VALUE, "GL_UNPACK_SKIP_ROWS must not be negative"); + return; + } + state->unpack_skip_rows = param; + break; + case GL_UNPACK_SKIP_PIXELS: + if (param < 0) { + gl_set_error(GL_INVALID_VALUE, "GL_UNPACK_SKIP_PIXELS must not be negative"); + return; + } + state->unpack_skip_pixels = param; + break; + case GL_UNPACK_ALIGNMENT: + if (param != 1 && param != 2 && param != 4 && param != 8) { + gl_set_error(GL_INVALID_VALUE, "GL_UNPACK_ALIGNMENT must be 1, 2, 4 or 8"); + return; + } + state->unpack_alignment = param; + break; + case GL_PACK_SWAP_BYTES: + case GL_PACK_LSB_FIRST: + case GL_PACK_ROW_LENGTH: + case GL_PACK_SKIP_ROWS: + case GL_PACK_SKIP_PIXELS: + case GL_PACK_ALIGNMENT: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void glPixelStoref(GLenum pname, GLfloat param) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (pname) { + case GL_UNPACK_SWAP_BYTES: + state->unpack_swap_bytes = param != 0.0f; + gl_update_transfer_state(); + break; + case GL_UNPACK_LSB_FIRST: + state->unpack_lsb_first = param != 0.0f; + break; + default: + glPixelStorei(pname, param); + break; + } +} + +void glPixelTransferi(GLenum pname, GLint value) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (pname) { + case GL_MAP_COLOR: + state->map_color = value != 0; + gl_update_transfer_state(); + break; + default: + glPixelTransferf(pname, value); + break; + } +} + +void glPixelTransferf(GLenum pname, GLfloat value) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (pname) { + case GL_MAP_COLOR: + state->map_color = value != 0.0f; + break; + case GL_RED_SCALE: + state->transfer_scale[0] = value; + break; + case GL_GREEN_SCALE: + state->transfer_scale[1] = value; + break; + case GL_BLUE_SCALE: + state->transfer_scale[2] = value; + break; + case GL_ALPHA_SCALE: + state->transfer_scale[3] = value; + break; + case GL_RED_BIAS: + state->transfer_bias[0] = value; + break; + case GL_GREEN_BIAS: + state->transfer_bias[1] = value; + break; + case GL_BLUE_BIAS: + state->transfer_bias[2] = value; + break; + case GL_ALPHA_BIAS: + state->transfer_bias[3] = value; + break; + case GL_DEPTH_SCALE: + case GL_DEPTH_BIAS: + case GL_MAP_STENCIL: + case GL_INDEX_SHIFT: + case GL_INDEX_OFFSET: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } + + gl_update_transfer_state(); +} + +gl_pixel_map_t * gl_get_pixel_map(GLenum map) +{ + switch (map) { + case GL_PIXEL_MAP_R_TO_R: + return &state->pixel_maps[0]; + case GL_PIXEL_MAP_G_TO_G: + return &state->pixel_maps[1]; + case GL_PIXEL_MAP_B_TO_B: + return &state->pixel_maps[2]; + case GL_PIXEL_MAP_A_TO_A: + return &state->pixel_maps[3]; + case GL_PIXEL_MAP_I_TO_I: + case GL_PIXEL_MAP_S_TO_S: + case GL_PIXEL_MAP_I_TO_R: + case GL_PIXEL_MAP_I_TO_G: + case GL_PIXEL_MAP_I_TO_B: + case GL_PIXEL_MAP_I_TO_A: + return NULL; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid pixel map", map); + return NULL; + } +} + +void glPixelMapusv(GLenum map, GLsizei size, const GLushort *values) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_pixel_map_t *pixel_map = gl_get_pixel_map(map); + if (pixel_map == NULL) { + return; + } + + if (size < 1 || size > MAX_PIXEL_MAP_SIZE) { + gl_set_error(GL_INVALID_VALUE, "Size must be in [1,%d]", MAX_PIXEL_MAP_SIZE); + return; + } + + for (GLsizei i = 0; i < size; i++) + { + pixel_map->entries[i] = U16_TO_FLOAT(values[i]); + } +} + +void glPixelMapuiv(GLenum map, GLsizei size, const GLuint *values) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_pixel_map_t *pixel_map = gl_get_pixel_map(map); + if (pixel_map == NULL) { + return; + } + + if (size < 1 || size > MAX_PIXEL_MAP_SIZE) { + gl_set_error(GL_INVALID_VALUE, "Size must be in [1,%d]", MAX_PIXEL_MAP_SIZE); + return; + } + + for (GLsizei i = 0; i < size; i++) + { + pixel_map->entries[i] = U32_TO_FLOAT(values[i]); + } +} + +void glPixelMapfv(GLenum map, GLsizei size, const GLfloat *values) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_pixel_map_t *pixel_map = gl_get_pixel_map(map); + if (pixel_map == NULL) { + return; + } + + if (size < 1 || size > MAX_PIXEL_MAP_SIZE) { + gl_set_error(GL_INVALID_VALUE, "Size must be in [1,%d]", MAX_PIXEL_MAP_SIZE); + return; + } + + for (GLsizei i = 0; i < size; i++) + { + pixel_map->entries[i] = values[i]; + } +} diff --git a/src/GL/primitive.c b/src/GL/primitive.c new file mode 100644 index 000000000..3c689a95c --- /dev/null +++ b/src/GL/primitive.c @@ -0,0 +1,1097 @@ +/** + * @file primitive.c + * @author Dennis Heinze + * @brief OpenGL primitive drawing and vertex processing routines. + */ +#include "gl_internal.h" +#include "utils.h" +#include "rdpq.h" +#include "rdpq_tri.h" +#include "rdpq_rect.h" +#include "rdpq_mode.h" +#include "rdpq_debug.h" +#include "../rdpq/rdpq_internal.h" +#include +#include + +_Static_assert(((RDPQ_CMD_TRI << 8) | (FLAG_DEPTH_TEST >> TRICMD_ATTR_SHIFT)) == (RDPQ_CMD_TRI_ZBUF << 8)); +_Static_assert(((RDPQ_CMD_TRI << 8) | (FLAG_TEXTURE_ACTIVE >> TRICMD_ATTR_SHIFT)) == (RDPQ_CMD_TRI_TEX << 8)); +_Static_assert(((RDPQ_CMD_TRI << 8) | TRICMD_ATTR_MASK) == (RDPQ_CMD_TRI_TEX_ZBUF << 8)); +_Static_assert(((FLAG_DEPTH_TEST | FLAG_TEXTURE_ACTIVE) >> TRICMD_ATTR_SHIFT) == TRICMD_ATTR_MASK); + +extern gl_state_t *state; + +uint8_t gl_points(); +uint8_t gl_lines(); +uint8_t gl_line_strip(); +uint8_t gl_triangles(); +uint8_t gl_triangle_strip(); +uint8_t gl_triangle_fan(); +uint8_t gl_quads(); + +void gl_reset_vertex_cache(); + +void gl_init_cpu_pipe(); +void gl_vertex_pre_tr(uint8_t cache_index); +void gl_draw_primitive(const uint8_t *indices); + +void gl_primitive_init() +{ + state->tex_gen[0].mode = GL_EYE_LINEAR; + state->tex_gen[0].object_plane[0] = 1; + state->tex_gen[0].eye_plane[0] = 1; + + state->tex_gen[1].mode = GL_EYE_LINEAR; + state->tex_gen[1].object_plane[1] = 1; + state->tex_gen[1].eye_plane[1] = 1; + + state->tex_gen[2].mode = GL_EYE_LINEAR; + state->tex_gen[3].mode = GL_EYE_LINEAR; + + state->point_size = 1; + state->line_width = 1; + + state->current_attributes.color[0] = 1; + state->current_attributes.color[1] = 1; + state->current_attributes.color[2] = 1; + state->current_attributes.color[3] = 1; + state->current_attributes.texcoord[3] = 1; + state->current_attributes.normal[2] = 1; + + state->vertex_halfx_precision.target_precision = VTX_SHIFT; + state->texcoord_halfx_precision.target_precision = TEX_SHIFT; + + glVertexHalfFixedPrecisionN64(VTX_SHIFT); + glTexCoordHalfFixedPrecisionN64(TEX_SHIFT); + + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + + set_can_use_rsp_dirty(); +} + +void gl_primitive_close() +{ +} + +bool gl_can_use_rsp_pipeline() +{ + #define WARN_CPU_REQUIRED(msg) ({ \ + static bool warn_state ## __LINE__; \ + if (!warn_state ## __LINE__) { \ + warn_state ## __LINE__ = true; \ + debugf("GL WARNING: The CPU pipeline is being used because a feature is enabled that is not supported on RSP: " msg "\n"); \ + } \ + }) + + // Points and lines are not implemented + if (state->polygon_mode != GL_FILL) { + WARN_CPU_REQUIRED("polygon mode"); + return false; + } + + if (state->lighting) { + // Flat shading is not implemented + if (state->shade_model == GL_FLAT) { + WARN_CPU_REQUIRED("flat shading"); + return false; + } + + // Spot lights are not implemented + for (uint32_t i = 0; i < LIGHT_COUNT; i++) + { + if (state->lights[i].spot_cutoff_cos >= 0.0f) { + WARN_CPU_REQUIRED("spotlights"); + return false; + } + } + + // Specular material is not implemented + if (state->material.specular[0] != 0.0f || + state->material.specular[1] != 0.0f || + state->material.specular[2] != 0.0f) { + WARN_CPU_REQUIRED("specular lighting"); + return false; + } + } + + return true; + + #undef WARN_CPU_REQUIRED +} + +void set_can_use_rsp_dirty() +{ + state->can_use_rsp_dirty = true; +} + +bool gl_init_prim_assembly(GLenum mode) +{ + + state->lock_next_vertex = false; + + switch (mode) { + case GL_POINTS: + state->prim_func = gl_points; + state->prim_size = 1; + break; + case GL_LINES: + state->prim_func = gl_lines; + state->prim_size = 2; + break; + case GL_LINE_LOOP: + // Line loop is equivalent to line strip, except for special case handled in glEnd + state->prim_func = gl_line_strip; + state->prim_size = 2; + state->lock_next_vertex = true; + break; + case GL_LINE_STRIP: + state->prim_func = gl_line_strip; + state->prim_size = 2; + break; + case GL_TRIANGLES: + state->prim_func = gl_triangles; + state->prim_size = 3; + break; + case GL_TRIANGLE_STRIP: + state->prim_func = gl_triangle_strip; + state->prim_size = 3; + break; + case GL_TRIANGLE_FAN: + state->prim_func = gl_triangle_fan; + state->prim_size = 3; + state->lock_next_vertex = true; + break; + case GL_QUADS: + state->prim_func = gl_quads; + state->prim_size = 3; + break; + case GL_QUAD_STRIP: + // Quad strip is equivalent to triangle strip + state->prim_func = gl_triangle_strip; + state->prim_size = 3; + break; + case GL_POLYGON: + // Polygon is equivalent to triangle fan + state->prim_func = gl_triangle_fan; + state->prim_size = 3; + state->lock_next_vertex = true; + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid primitive mode", mode); + return false; + } + + state->primitive_mode = mode; + state->prim_progress = 0; + state->prim_counter = 0; + state->prim_id = 0x80000000; + + return true; +} + +extern const gl_pipeline_t gl_cpu_pipeline; +extern const gl_pipeline_t gl_rsp_pipeline; + +bool gl_begin(GLenum mode) +{ + if (state->can_use_rsp_dirty) { + state->can_use_rsp = gl_can_use_rsp_pipeline(); + state->can_use_rsp_dirty = false; + } + + if (!gl_init_prim_assembly(mode)) { + return false; + } + + gl_reset_vertex_cache(); + + __rdpq_autosync_change(AUTOSYNC_PIPE | AUTOSYNC_TILES | AUTOSYNC_TMEM(0)); + + gl_pre_init_pipe(mode); + + // FIXME: This is pessimistically marking everything as used, even if textures are turned off + // CAUTION: texture state is owned by the RSP currently, so how can we determine this? + __rdpq_autosync_use(AUTOSYNC_PIPE | AUTOSYNC_TILES | AUTOSYNC_TMEM(0)); + + gl_update_array_pointers(state->array_object); + + // Only triangles are implemented on RSP + bool rsp_pipeline_enabled = state->can_use_rsp && state->prim_size == 3; + state->current_pipeline = rsp_pipeline_enabled ? &gl_rsp_pipeline : &gl_cpu_pipeline; + + state->current_pipeline->begin(); + + return true; +} + +void gl_end() +{ + state->current_pipeline->end(); +} + +void glBegin(GLenum mode) +{ + if (!gl_ensure_no_begin_end()) return; + + if (gl_begin(mode)) { + state->begin_end_active = true; + } +} + +void glEnd(void) +{ + if (!state->begin_end_active) { + gl_set_error(GL_INVALID_OPERATION, "glEnd must be called after glBegin"); + } + + gl_end(); + + state->begin_end_active = false; +} + +void gl_reset_vertex_cache() +{ + memset(state->vertex_cache_ids, 0, sizeof(state->vertex_cache_ids)); + memset(state->lru_age_table, 0, sizeof(state->lru_age_table)); + state->lru_next_age = 1; +} + +bool gl_check_vertex_cache(uint32_t id, uint8_t *cache_index, bool lock) +{ + const uint32_t INFINITE_AGE = 0xFFFFFFFF; // infinitely recent + + bool miss = true; + + uint32_t min_age = INFINITE_AGE; + for (uint8_t ci = 0; ci < VERTEX_CACHE_SIZE; ci++) + { + if (state->vertex_cache_ids[ci] == id) { + miss = false; + *cache_index = ci; + break; + } + + if (state->lru_age_table[ci] < min_age) { + min_age = state->lru_age_table[ci]; + *cache_index = ci; + } + } + + uint32_t age = lock ? INFINITE_AGE : state->lru_next_age++; + state->lru_age_table[*cache_index] = age; + state->vertex_cache_ids[*cache_index] = id; + + return miss; +} + +bool gl_get_cache_index(uint32_t vertex_id, uint8_t *cache_index) +{ + bool result = gl_check_vertex_cache(vertex_id + 1, cache_index, state->lock_next_vertex); + + if (state->lock_next_vertex) { + state->lock_next_vertex = false; + state->locked_vertex = *cache_index; + } + + return result; +} + +void gl_load_attribs(const gl_array_t *arrays, uint32_t index) +{ + for (uint32_t i = 0; i < ATTRIB_COUNT; i++) + { + const gl_array_t *array = &arrays[i]; + if (!array->enabled) { + continue; + } + + void *dst = gl_get_attrib_pointer(&state->current_attributes, i); + const void *src = gl_get_attrib_element(array, index); + + array->cpu_read_func(dst, src, array->size); + } +} + +void gl_fill_attrib_defaults(gl_array_type_t array_type, uint32_t size) +{ + static const GLfloat default_attribute_value[] = {0.0f, 0.0f, 0.0f, 1.0f}; + uint32_t element_size = sizeof(GLfloat); + + switch (array_type) { + case ATTRIB_VERTEX: + case ATTRIB_COLOR: + case ATTRIB_TEXCOORD: + break; + default: + return; + } + + const GLfloat *src = default_attribute_value + size; + void *dst = gl_get_attrib_pointer(&state->current_attributes, array_type) + size * element_size; + memcpy(dst, src, (4 - size) * element_size); +} + +void gl_fill_all_attrib_defaults(const gl_array_t *arrays) +{ + // There are no default values for the matrix index because it is always specified fully. + for (uint32_t i = 0; i < ATTRIB_COUNT; i++) + { + const gl_array_t *array = &arrays[i]; + if (!arrays[i].enabled) { + continue; + } + + gl_fill_attrib_defaults(i, array->size); + } +} + +uint8_t gl_points() +{ + // Reset the progress to zero since we start with a completely new primitive that + // won't share any vertices with the previous ones + return 0; +} + +uint8_t gl_lines() +{ + // Reset the progress to zero since we start with a completely new primitive that + // won't share any vertices with the previous ones + return 0; +} + +uint8_t gl_line_strip() +{ + state->prim_indices[0] = state->prim_indices[1]; + + return 1; +} + +uint8_t gl_triangles() +{ + // Reset the progress to zero since we start with a completely new primitive that + // won't share any vertices with the previous ones + return 0; +} + +uint8_t gl_triangle_strip() +{ + // Which vertices are shared depends on whether the primitive counter is odd or even + state->prim_indices[state->prim_counter] = state->prim_indices[2]; + state->prim_counter ^= 1; + + // The next triangle will share two vertices with the previous one, so reset progress to 2 + return 2; +} + +uint8_t gl_triangle_fan() +{ + state->prim_indices[1] = state->prim_indices[2]; + + // The next triangle will share two vertices with the previous one, so reset progress to 2 + // It will always share the last one and the very first vertex that was specified. + // To make sure the first vertex is not overwritten it was locked earlier (see glBegin) + return 2; +} + +uint8_t gl_quads() +{ + state->prim_indices[1] = state->prim_indices[2]; + + state->prim_counter ^= 1; + return state->prim_counter << 1; +} + +bool gl_prim_assembly(uint8_t cache_index, uint8_t *indices) +{ + if (state->lock_next_vertex) { + state->lock_next_vertex = false; + state->locked_vertex = cache_index; + } + + state->prim_indices[state->prim_progress] = cache_index; + state->prim_progress++; + + if (state->prim_progress < state->prim_size) { + return false; + } + + memcpy(indices, state->prim_indices, state->prim_size * sizeof(uint8_t)); + + assert(state->prim_func != NULL); + state->prim_progress = state->prim_func(); + return true; +} + +void glDrawArrays(GLenum mode, GLint first, GLsizei count) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (mode) { + case GL_POINTS: + case GL_LINES: + case GL_LINE_LOOP: + case GL_LINE_STRIP: + case GL_TRIANGLES: + case GL_TRIANGLE_STRIP: + case GL_TRIANGLE_FAN: + case GL_QUADS: + case GL_QUAD_STRIP: + case GL_POLYGON: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid primitive mode", mode); + return; + } + + if (count == 0) { + return; + } + + gl_begin(mode); + state->current_pipeline->draw_arrays(first, count); + gl_end(); +} + +uint32_t read_index_8(const uint8_t *src, uint32_t i) +{ + return src[i]; +} + +uint32_t read_index_16(const uint16_t *src, uint32_t i) +{ + return src[i]; +} + +uint32_t read_index_32(const uint32_t *src, uint32_t i) +{ + return src[i]; +} + +void glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid *indices) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (mode) { + case GL_POINTS: + case GL_LINES: + case GL_LINE_LOOP: + case GL_LINE_STRIP: + case GL_TRIANGLES: + case GL_TRIANGLE_STRIP: + case GL_TRIANGLE_FAN: + case GL_QUADS: + case GL_QUAD_STRIP: + case GL_POLYGON: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid primitive mode", mode); + return; + } + + read_index_func read_index; + + switch (type) { + case GL_UNSIGNED_BYTE: + read_index = (read_index_func)read_index_8; + break; + case GL_UNSIGNED_SHORT: + read_index = (read_index_func)read_index_16; + break; + case GL_UNSIGNED_INT: + read_index = (read_index_func)read_index_32; + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid index type", type); + return; + } + + if (count == 0) { + return; + } + + if (state->element_array_buffer != NULL) { + indices = state->element_array_buffer->storage.data + (uint32_t)indices; + } + + gl_begin(mode); + state->current_pipeline->draw_elements(count, indices, read_index); + gl_end(); +} + +void glArrayElement(GLint i) +{ + // Calling glArrayElement while the vertex array is enabled has, among other things, + // the same effect as glVertex. See __gl_vertex for that function's behavior. + assertf(!state->array_object->arrays[ATTRIB_VERTEX].enabled || state->begin_end_active, + "glArrayElement was called outside of glBegin/glEnd while vertex array was enabled"); + + if (i < 0) { + gl_set_error(GL_INVALID_VALUE, "Index must not be negative"); + return; + } + + state->current_pipeline->array_element(i); +} + +void __gl_vertex(GLenum type, const void *value, uint32_t size) +{ + // According to the spec, calling glVertex outside of glBegin/glEnd + // specifically results in UB instead of generating an error, so just assert. + assertf(state->begin_end_active, "glVertex was called outside of glBegin/glEnd"); + state->current_pipeline->vertex(value, type, size); +} + +void __gl_color(GLenum type, const void *value, uint32_t size) +{ + if (state->begin_end_active) { + state->current_pipeline->color(value, type, size); + } else { + gl_read_attrib(ATTRIB_COLOR, value, type, size); + gl_set_current_color(state->current_attributes.color); + } +} + +void __gl_tex_coord(GLenum type, const void *value, uint32_t size) +{ + if (state->begin_end_active) { + state->current_pipeline->tex_coord(value, type, size); + } else { + gl_read_attrib(ATTRIB_TEXCOORD, value, type, size); + gl_set_current_texcoords(state->current_attributes.texcoord); + } +} + +void __gl_normal(GLenum type, const void *value, uint32_t size) +{ + if (state->begin_end_active) { + state->current_pipeline->normal(value, type, size); + } else { + gl_read_attrib(ATTRIB_NORMAL, value, type, size); + gl_set_current_normal(state->current_attributes.normal); + } +} + +void __gl_mtx_index(GLenum type, const void *value, uint32_t size) +{ + if (size > VERTEX_UNIT_COUNT) { + gl_set_error(GL_INVALID_VALUE, "Size must not be greater than %d", VERTEX_UNIT_COUNT); + return; + } + + if (state->begin_end_active) { + state->current_pipeline->mtx_index(value, type, size); + } else { + gl_read_attrib(ATTRIB_MTX_INDEX, value, type, size); + gl_set_current_mtx_index(state->current_attributes.mtx_index); + } +} + +#define __ATTR_IMPL(func, argtype, enumtype, ...) ({\ + argtype tmp[] = { __VA_ARGS__ }; \ + func(enumtype, tmp, __COUNT_VARARGS(__VA_ARGS__)); \ +}) + +void glVertex2sv(const GLshort *v) { __gl_vertex(GL_SHORT, v, 2); } +void glVertex2iv(const GLint *v) { __gl_vertex(GL_INT, v, 2); } +void glVertex2fv(const GLfloat *v) { __gl_vertex(GL_FLOAT, v, 2); } +void glVertex2dv(const GLdouble *v) { __gl_vertex(GL_DOUBLE, v, 2); } +void glVertex2hxvN64(const GLhalfxN64 *v) { __gl_vertex(GL_HALF_FIXED_N64, v, 2); } + +void glVertex3sv(const GLshort *v) { __gl_vertex(GL_SHORT, v, 3); } +void glVertex3iv(const GLint *v) { __gl_vertex(GL_INT, v, 3); } +void glVertex3fv(const GLfloat *v) { __gl_vertex(GL_FLOAT, v, 3); } +void glVertex3dv(const GLdouble *v) { __gl_vertex(GL_DOUBLE, v, 3); } +void glVertex3hxvN64(const GLhalfxN64 *v) { __gl_vertex(GL_HALF_FIXED_N64, v, 3); } + +void glVertex4sv(const GLshort *v) { __gl_vertex(GL_SHORT, v, 4); } +void glVertex4iv(const GLint *v) { __gl_vertex(GL_INT, v, 4); } +void glVertex4fv(const GLfloat *v) { __gl_vertex(GL_FLOAT, v, 4); } +void glVertex4dv(const GLdouble *v) { __gl_vertex(GL_DOUBLE, v, 4); } +void glVertex4hxvN64(const GLhalfxN64 *v) { __gl_vertex(GL_HALF_FIXED_N64, v, 4); } + +void glVertex2s(GLshort x, GLshort y) { __ATTR_IMPL(__gl_vertex, GLshort, GL_SHORT, x, y); } +void glVertex2i(GLint x, GLint y) { __ATTR_IMPL(__gl_vertex, GLint, GL_INT, x, y); } +void glVertex2f(GLfloat x, GLfloat y) { __ATTR_IMPL(__gl_vertex, GLfloat, GL_FLOAT, x, y); } +void glVertex2d(GLdouble x, GLdouble y) { __ATTR_IMPL(__gl_vertex, GLdouble, GL_DOUBLE, x, y); } +void glVertex2hxN64(GLhalfxN64 x, GLhalfxN64 y) { __ATTR_IMPL(__gl_vertex, GLhalfxN64, GL_HALF_FIXED_N64, x, y); } + +void glVertex3s(GLshort x, GLshort y, GLshort z) { __ATTR_IMPL(__gl_vertex, GLshort, GL_SHORT, x, y, z); } +void glVertex3i(GLint x, GLint y, GLint z) { __ATTR_IMPL(__gl_vertex, GLint, GL_INT, x, y, z); } +void glVertex3f(GLfloat x, GLfloat y, GLfloat z) { __ATTR_IMPL(__gl_vertex, GLfloat, GL_FLOAT, x, y, z); } +void glVertex3d(GLdouble x, GLdouble y, GLdouble z) { __ATTR_IMPL(__gl_vertex, GLdouble, GL_DOUBLE, x, y, z); } +void glVertex3hxN64(GLhalfxN64 x, GLhalfxN64 y, GLhalfxN64 z) { __ATTR_IMPL(__gl_vertex, GLhalfxN64, GL_HALF_FIXED_N64, x, y, z); } + +void glVertex4s(GLshort x, GLshort y, GLshort z, GLshort w) { __ATTR_IMPL(__gl_vertex, GLshort, GL_SHORT, x, y, z, w); } +void glVertex4i(GLint x, GLint y, GLint z, GLint w) { __ATTR_IMPL(__gl_vertex, GLint, GL_INT, x, y, z, w); } +void glVertex4f(GLfloat x, GLfloat y, GLfloat z, GLfloat w) { __ATTR_IMPL(__gl_vertex, GLfloat, GL_FLOAT, x, y, z, w); } +void glVertex4d(GLdouble x, GLdouble y, GLdouble z, GLdouble w) { __ATTR_IMPL(__gl_vertex, GLdouble, GL_DOUBLE, x, y, z, w); } +void glVertex4hxN64(GLhalfxN64 x, GLhalfxN64 y, GLhalfxN64 z, GLhalfxN64 w) { __ATTR_IMPL(__gl_vertex, GLhalfxN64, GL_HALF_FIXED_N64, x, y, z, w); } + +void glColor3bv(const GLbyte *v) { __gl_color(GL_BYTE, v, 3); } +void glColor3sv(const GLshort *v) { __gl_color(GL_SHORT, v, 3); } +void glColor3iv(const GLint *v) { __gl_color(GL_INT, v, 3); } +void glColor3fv(const GLfloat *v) { __gl_color(GL_FLOAT, v, 3); } +void glColor3dv(const GLdouble *v) { __gl_color(GL_DOUBLE, v, 3); } +void glColor3ubv(const GLubyte *v) { __gl_color(GL_UNSIGNED_BYTE, v, 3); } +void glColor3usv(const GLushort *v) { __gl_color(GL_UNSIGNED_SHORT, v, 3); } +void glColor3uiv(const GLuint *v) { __gl_color(GL_UNSIGNED_INT, v, 3); } + +void glColor4bv(const GLbyte *v) { __gl_color(GL_BYTE, v, 4); } +void glColor4sv(const GLshort *v) { __gl_color(GL_SHORT, v, 4); } +void glColor4iv(const GLint *v) { __gl_color(GL_INT, v, 4); } +void glColor4fv(const GLfloat *v) { __gl_color(GL_FLOAT, v, 4); } +void glColor4dv(const GLdouble *v) { __gl_color(GL_DOUBLE, v, 4); } +void glColor4ubv(const GLubyte *v) { __gl_color(GL_UNSIGNED_BYTE, v, 4); } +void glColor4usv(const GLushort *v) { __gl_color(GL_UNSIGNED_SHORT, v, 4); } +void glColor4uiv(const GLuint *v) { __gl_color(GL_UNSIGNED_INT, v, 4); } + +void glColor3b(GLbyte r, GLbyte g, GLbyte b) { __ATTR_IMPL(__gl_color, GLbyte, GL_BYTE, r, g, b); } +void glColor3s(GLshort r, GLshort g, GLshort b) { __ATTR_IMPL(__gl_color, GLshort, GL_SHORT, r, g, b); } +void glColor3i(GLint r, GLint g, GLint b) { __ATTR_IMPL(__gl_color, GLint, GL_INT, r, g, b); } +void glColor3f(GLfloat r, GLfloat g, GLfloat b) { __ATTR_IMPL(__gl_color, GLfloat, GL_FLOAT, r, g, b); } +void glColor3d(GLdouble r, GLdouble g, GLdouble b) { __ATTR_IMPL(__gl_color, GLdouble, GL_DOUBLE, r, g, b); } +void glColor3ub(GLubyte r, GLubyte g, GLubyte b) { __ATTR_IMPL(__gl_color, GLubyte, GL_UNSIGNED_BYTE, r, g, b); } +void glColor3us(GLushort r, GLushort g, GLushort b) { __ATTR_IMPL(__gl_color, GLushort, GL_UNSIGNED_SHORT, r, g, b); } +void glColor3ui(GLuint r, GLuint g, GLuint b) { __ATTR_IMPL(__gl_color, GLuint, GL_UNSIGNED_INT, r, g, b); } + +void glColor4b(GLbyte r, GLbyte g, GLbyte b, GLbyte a) { __ATTR_IMPL(__gl_color, GLbyte, GL_BYTE, r, g, b, a); } +void glColor4s(GLshort r, GLshort g, GLshort b, GLshort a) { __ATTR_IMPL(__gl_color, GLshort, GL_SHORT, r, g, b, a); } +void glColor4i(GLint r, GLint g, GLint b, GLint a) { __ATTR_IMPL(__gl_color, GLint, GL_INT, r, g, b, a); } +void glColor4f(GLfloat r, GLfloat g, GLfloat b, GLfloat a) { __ATTR_IMPL(__gl_color, GLfloat, GL_FLOAT, r, g, b, a); } +void glColor4d(GLdouble r, GLdouble g, GLdouble b, GLdouble a) { __ATTR_IMPL(__gl_color, GLdouble, GL_DOUBLE, r, g, b, a); } +void glColor4ub(GLubyte r, GLubyte g, GLubyte b, GLubyte a) { __ATTR_IMPL(__gl_color, GLubyte, GL_UNSIGNED_BYTE, r, g, b, a); } +void glColor4us(GLushort r, GLushort g, GLushort b, GLushort a) { __ATTR_IMPL(__gl_color, GLushort, GL_UNSIGNED_SHORT, r, g, b, a); } +void glColor4ui(GLuint r, GLuint g, GLuint b, GLuint a) { __ATTR_IMPL(__gl_color, GLuint, GL_UNSIGNED_INT, r, g, b, a); } + +void glTexCoord1sv(const GLshort *v) { __gl_tex_coord(GL_SHORT, v, 1); } +void glTexCoord1iv(const GLint *v) { __gl_tex_coord(GL_INT, v, 1); } +void glTexCoord1fv(const GLfloat *v) { __gl_tex_coord(GL_FLOAT, v, 1); } +void glTexCoord1dv(const GLdouble *v) { __gl_tex_coord(GL_DOUBLE, v, 1); } +void glTexCoord1hxvN64(const GLhalfxN64 *v) { __gl_tex_coord(GL_HALF_FIXED_N64, v, 1); } + +void glTexCoord2sv(const GLshort *v) { __gl_tex_coord(GL_SHORT, v, 2); } +void glTexCoord2iv(const GLint *v) { __gl_tex_coord(GL_INT, v, 2); } +void glTexCoord2fv(const GLfloat *v) { __gl_tex_coord(GL_FLOAT, v, 2); } +void glTexCoord2dv(const GLdouble *v) { __gl_tex_coord(GL_DOUBLE, v, 2); } +void glTexCoord2hxvN64(const GLhalfxN64 *v) { __gl_tex_coord(GL_HALF_FIXED_N64, v, 2); } + +void glTexCoord3sv(const GLshort *v) { __gl_tex_coord(GL_SHORT, v, 3); } +void glTexCoord3iv(const GLint *v) { __gl_tex_coord(GL_INT, v, 3); } +void glTexCoord3fv(const GLfloat *v) { __gl_tex_coord(GL_FLOAT, v, 3); } +void glTexCoord3dv(const GLdouble *v) { __gl_tex_coord(GL_DOUBLE, v, 3); } +void glTexCoord3hxvN64(const GLhalfxN64 *v) { __gl_tex_coord(GL_HALF_FIXED_N64, v, 3); } + +void glTexCoord4sv(const GLshort *v) { __gl_tex_coord(GL_SHORT, v, 4); } +void glTexCoord4iv(const GLint *v) { __gl_tex_coord(GL_INT, v, 4); } +void glTexCoord4fv(const GLfloat *v) { __gl_tex_coord(GL_FLOAT, v, 4); } +void glTexCoord4dv(const GLdouble *v) { __gl_tex_coord(GL_DOUBLE, v, 4); } +void glTexCoord4hxvN64(const GLhalfxN64 *v) { __gl_tex_coord(GL_HALF_FIXED_N64, v, 4); } + +void glTexCoord1s(GLshort s) { __ATTR_IMPL(__gl_tex_coord, GLshort, GL_SHORT, s); } +void glTexCoord1i(GLint s) { __ATTR_IMPL(__gl_tex_coord, GLint, GL_INT, s); } +void glTexCoord1f(GLfloat s) { __ATTR_IMPL(__gl_tex_coord, GLfloat, GL_FLOAT, s); } +void glTexCoord1d(GLdouble s) { __ATTR_IMPL(__gl_tex_coord, GLdouble, GL_DOUBLE, s); } +void glTexCoord1hxN64(GLhalfxN64 s) { __ATTR_IMPL(__gl_tex_coord, GLhalfxN64, GL_HALF_FIXED_N64, s); } + +void glTexCoord2s(GLshort s, GLshort t) { __ATTR_IMPL(__gl_tex_coord, GLshort, GL_SHORT, s, t); } +void glTexCoord2i(GLint s, GLint t) { __ATTR_IMPL(__gl_tex_coord, GLint, GL_INT, s, t); } +void glTexCoord2f(GLfloat s, GLfloat t) { __ATTR_IMPL(__gl_tex_coord, GLfloat, GL_FLOAT, s, t); } +void glTexCoord2d(GLdouble s, GLdouble t) { __ATTR_IMPL(__gl_tex_coord, GLdouble, GL_DOUBLE, s, t); } +void glTexCoord2hxN64(GLhalfxN64 s, GLhalfxN64 t) { __ATTR_IMPL(__gl_tex_coord, GLhalfxN64, GL_HALF_FIXED_N64, s, t); } + +void glTexCoord3s(GLshort s, GLshort t, GLshort r) { __ATTR_IMPL(__gl_tex_coord, GLshort, GL_SHORT, s, t, r); } +void glTexCoord3i(GLint s, GLint t, GLint r) { __ATTR_IMPL(__gl_tex_coord, GLint, GL_INT, s, t, r); } +void glTexCoord3f(GLfloat s, GLfloat t, GLfloat r) { __ATTR_IMPL(__gl_tex_coord, GLfloat, GL_FLOAT, s, t, r); } +void glTexCoord3d(GLdouble s, GLdouble t, GLdouble r) { __ATTR_IMPL(__gl_tex_coord, GLdouble, GL_DOUBLE, s, t, r); } +void glTexCoord3hxN64(GLhalfxN64 s, GLhalfxN64 t, GLhalfxN64 r) { __ATTR_IMPL(__gl_tex_coord, GLhalfxN64, GL_HALF_FIXED_N64, s, t, r); } + +void glTexCoord4s(GLshort s, GLshort t, GLshort r, GLshort q) { __ATTR_IMPL(__gl_tex_coord, GLshort, GL_SHORT, s, t, r, q); } +void glTexCoord4i(GLint s, GLint t, GLint r, GLint q) { __ATTR_IMPL(__gl_tex_coord, GLint, GL_INT, s, t, r, q); } +void glTexCoord4f(GLfloat s, GLfloat t, GLfloat r, GLfloat q) { __ATTR_IMPL(__gl_tex_coord, GLfloat, GL_FLOAT, s, t, r, q); } +void glTexCoord4d(GLdouble s, GLdouble t, GLdouble r, GLdouble q) { __ATTR_IMPL(__gl_tex_coord, GLdouble, GL_DOUBLE, s, t, r, q); } +void glTexCoord4hxN64(GLhalfxN64 s, GLhalfxN64 t, GLhalfxN64 r, GLhalfxN64 q) { __ATTR_IMPL(__gl_tex_coord, GLhalfxN64, GL_HALF_FIXED_N64, s, t, r, q); } + +void glNormal3bv(const GLbyte *v) { __gl_normal(GL_BYTE, v, 3); } +void glNormal3sv(const GLshort *v) { __gl_normal(GL_SHORT, v, 3); } +void glNormal3iv(const GLint *v) { __gl_normal(GL_INT, v, 3); } +void glNormal3fv(const GLfloat *v) { __gl_normal(GL_FLOAT, v, 3); } +void glNormal3dv(const GLdouble *v) { __gl_normal(GL_DOUBLE, v, 3); } + +void glNormal3b(GLbyte nx, GLbyte ny, GLbyte nz) { __ATTR_IMPL(__gl_normal, GLbyte, GL_BYTE, nx, ny, nz); } +void glNormal3s(GLshort nx, GLshort ny, GLshort nz) { __ATTR_IMPL(__gl_normal, GLshort, GL_SHORT, nx, ny, nz); } +void glNormal3i(GLint nx, GLint ny, GLint nz) { __ATTR_IMPL(__gl_normal, GLint, GL_INT, nx, ny, nz); } +void glNormal3f(GLfloat nx, GLfloat ny, GLfloat nz) { __ATTR_IMPL(__gl_normal, GLfloat, GL_FLOAT, nx, ny, nz); } +void glNormal3d(GLdouble nx, GLdouble ny, GLdouble nz) { __ATTR_IMPL(__gl_normal, GLdouble, GL_DOUBLE, nx, ny, nz); } + +void glMatrixIndexubvARB(GLint size, const GLubyte *v) { __gl_mtx_index(GL_UNSIGNED_BYTE, v, size); } +void glMatrixIndexusvARB(GLint size, const GLushort *v) { __gl_mtx_index(GL_UNSIGNED_SHORT, v, size); } +void glMatrixIndexuivARB(GLint size, const GLuint *v) { __gl_mtx_index(GL_UNSIGNED_INT, v, size); } + +static void set_precision_bits(gl_fixed_precision_t *dst, GLuint bits) +{ + // One bit is reserved for the sign + static const GLuint max_bits = sizeof(GLhalfxN64) * 8 - 1; + + if (bits > max_bits) { + gl_set_error(GL_INVALID_VALUE, "Bits must not be greater than %ld", max_bits); + return; + } + + dst->precision = bits; + dst->shift_amount = dst->target_precision - bits; + dst->to_float_factor = 1.0f / (1<vertex_halfx_precision, bits); } +void glTexCoordHalfFixedPrecisionN64(GLuint bits) { set_precision_bits(&state->texcoord_halfx_precision, bits); } + +#define __RECT_IMPL(vertex, x1, y1, x2, y2) ({ \ + if (!gl_ensure_no_begin_end()) return; \ + glBegin(GL_POLYGON); \ + vertex(x1, y1); \ + vertex(x2, y1); \ + vertex(x2, y2); \ + vertex(x1, y2); \ + glEnd(); \ +}) + +void glRects(GLshort x1, GLshort y1, GLshort x2, GLshort y2) { __RECT_IMPL(glVertex2s, x1, y1, x2, y2); } +void glRecti(GLint x1, GLint y1, GLint x2, GLint y2) { __RECT_IMPL(glVertex2i, x1, y1, x2, y2); } +void glRectf(GLfloat x1, GLfloat y1, GLfloat x2, GLfloat y2) { __RECT_IMPL(glVertex2f, x1, y1, x2, y2); } +void glRectd(GLdouble x1, GLdouble y1, GLdouble x2, GLdouble y2) { __RECT_IMPL(glVertex2d, x1, y1, x2, y2); } + +void glRectsv(const GLshort *v1, const GLshort *v2) { __RECT_IMPL(glVertex2s, v1[0], v1[1], v2[0], v2[1]); } +void glRectiv(const GLint *v1, const GLint *v2) { __RECT_IMPL(glVertex2s, v1[0], v1[1], v2[0], v2[1]); } +void glRectfv(const GLfloat *v1, const GLfloat *v2) { __RECT_IMPL(glVertex2s, v1[0], v1[1], v2[0], v2[1]); } +void glRectdv(const GLdouble *v1, const GLdouble *v2) { __RECT_IMPL(glVertex2s, v1[0], v1[1], v2[0], v2[1]); } + +void glPointSize(GLfloat size) +{ + if (!gl_ensure_no_begin_end()) return; + + if (size <= 0.0f) { + gl_set_error(GL_INVALID_VALUE, "Point size must not be negative"); + return; + } + + state->point_size = size; + gl_set_short(GL_UPDATE_NONE, offsetof(gl_server_state_t, point_size), size*4); +} + +void glLineWidth(GLfloat width) +{ + if (!gl_ensure_no_begin_end()) return; + + if (width <= 0.0f) { + gl_set_error(GL_INVALID_VALUE, "Line width must not be negative"); + return; + } + + state->line_width = width; + gl_set_short(GL_UPDATE_NONE, offsetof(gl_server_state_t, line_width), width*4); +} + +void glPolygonMode(GLenum face, GLenum mode) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (face) { + case GL_FRONT: + case GL_BACK: + assertf(0, "Separate polygon modes for front and back faces are not supported!"); + break; + case GL_FRONT_AND_BACK: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid face target", face); + return; + } + + switch (mode) { + case GL_POINT: + case GL_LINE: + case GL_FILL: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid polygon mode", mode); + return; + } + + gl_set_short(GL_UPDATE_NONE, offsetof(gl_server_state_t, polygon_mode), (uint16_t)mode); + state->polygon_mode = mode; + set_can_use_rsp_dirty(); +} + +void glDepthRange(GLclampd n, GLclampd f) +{ + if (!gl_ensure_no_begin_end()) return; + + state->current_viewport.scale[2] = (f - n) * 0.5f; + state->current_viewport.offset[2] = n + (f - n) * 0.5f; + + gl_set_short(GL_UPDATE_NONE, + offsetof(gl_server_state_t, viewport_scale) + sizeof(int16_t) * 2, + state->current_viewport.scale[2] * 4); + gl_set_short(GL_UPDATE_NONE, + offsetof(gl_server_state_t, viewport_offset) + sizeof(int16_t) * 2, + state->current_viewport.offset[2] * 4); +} + +void glViewport(GLint x, GLint y, GLsizei w, GLsizei h) +{ + if (!gl_ensure_no_begin_end()) return; + + uint32_t fbh = state->color_buffer->height; + + state->current_viewport.scale[0] = w * 0.5f; + state->current_viewport.scale[1] = h * -0.5f; + state->current_viewport.offset[0] = x + w * 0.5f; + state->current_viewport.offset[1] = fbh - y - h * 0.5f; + + // Screen coordinates are s13.2 + #define SCREEN_XY_SCALE 4.0f + #define SCREEN_Z_SCALE 32767.0f + + // * 2.0f to compensate for RSP reciprocal missing 1 bit + uint16_t scale_x = state->current_viewport.scale[0] * SCREEN_XY_SCALE * 2.0f; + uint16_t scale_y = state->current_viewport.scale[1] * SCREEN_XY_SCALE * 2.0f; + uint16_t scale_z = state->current_viewport.scale[2] * SCREEN_Z_SCALE * 2.0f; + uint16_t offset_x = state->current_viewport.offset[0] * SCREEN_XY_SCALE; + uint16_t offset_y = state->current_viewport.offset[1] * SCREEN_XY_SCALE; + uint16_t offset_z = state->current_viewport.offset[2] * SCREEN_Z_SCALE; + + gl_set_long(GL_UPDATE_NONE, + offsetof(gl_server_state_t, viewport_scale), + ((uint64_t)scale_x << 48) | ((uint64_t)scale_y << 32) | ((uint64_t)scale_z << 16)); + gl_set_long(GL_UPDATE_NONE, + offsetof(gl_server_state_t, viewport_offset), + ((uint64_t)offset_x << 48) | ((uint64_t)offset_y << 32) | ((uint64_t)offset_z << 16)); +} + +gl_tex_gen_t *gl_get_tex_gen(GLenum coord) +{ + switch (coord) { + case GL_S: + return &state->tex_gen[0]; + case GL_T: + return &state->tex_gen[1]; + case GL_R: + return &state->tex_gen[2]; + case GL_Q: + return &state->tex_gen[3]; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid tex gen coordinate", coord); + return NULL; + } +} + +void gl_tex_gen_set_mode(gl_tex_gen_t *gen, GLenum coord, GLint param) +{ + switch (param) { + case GL_OBJECT_LINEAR: + case GL_EYE_LINEAR: + break; + case GL_SPHERE_MAP: + if (coord == GL_R || coord == GL_Q) { + gl_set_error(GL_INVALID_ENUM, "Sphere mapping can only be applied to S or T coordinates"); + return; + } + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid tex gen mode", param); + return; + } + + uint32_t coord_offset = (coord & 0x3) * sizeof(uint16_t); + + gl_set_short(GL_UPDATE_NONE, offsetof(gl_server_state_t, tex_gen) + offsetof(gl_tex_gen_soa_t, mode) + coord_offset, param); + gen->mode = param; + + set_can_use_rsp_dirty(); +} + +void gl_tex_gen_i(GLenum coord, GLenum pname, GLint param) +{ + gl_tex_gen_t *gen = gl_get_tex_gen(coord); + if (gen == NULL) { + return; + } + + if (pname != GL_TEXTURE_GEN_MODE) { + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } + + gl_tex_gen_set_mode(gen, coord, param); +} + +void glTexGeni(GLenum coord, GLenum pname, GLint param) +{ + if (!gl_ensure_no_begin_end()) return; + gl_tex_gen_i(coord, pname, param); +} + +void glTexGenf(GLenum coord, GLenum pname, GLfloat param) +{ + if (!gl_ensure_no_begin_end()) return; + gl_tex_gen_i(coord, pname, param); +} + +void glTexGend(GLenum coord, GLenum pname, GLdouble param) +{ + if (!gl_ensure_no_begin_end()) return; + gl_tex_gen_i(coord, pname, param); +} + +void gl_tex_gen_set_plane(GLenum coord, GLenum pname, const GLfloat *plane) +{ + uint32_t plane_offset = (pname - GL_OBJECT_PLANE) * TEX_GEN_COUNT * sizeof(uint16_t); + uint32_t gen_offset = (coord & 0x3) * sizeof(uint16_t); + uint32_t offset = offsetof(gl_server_state_t, tex_gen) + plane_offset + gen_offset; + + uint32_t coord_size = TEX_GEN_COUNT * TEX_GEN_PLANE_COUNT * sizeof(uint16_t); + + for (uint32_t i = 0; i < TEX_COORD_COUNT; i++) + { + int32_t fixed = plane[i] * (1 << 16); + uint16_t integer = (fixed & 0xFFFF0000) >> 16; + uint16_t fraction = fixed & 0x0000FFFF; + + uint32_t coord_offset = offset + coord_size * i; + + gl_set_short(GL_UPDATE_NONE, coord_offset + offsetof(gl_tex_gen_soa_t, integer), integer); + gl_set_short(GL_UPDATE_NONE, coord_offset + offsetof(gl_tex_gen_soa_t, fraction), fraction); + } +} + +void glTexGenfv(GLenum coord, GLenum pname, const GLfloat *params) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_tex_gen_t *gen = gl_get_tex_gen(coord); + if (gen == NULL) { + return; + } + + switch (pname) { + case GL_TEXTURE_GEN_MODE: + gl_tex_gen_set_mode(gen, coord, params[0]); + break; + case GL_OBJECT_PLANE: + gen->object_plane[0] = params[0]; + gen->object_plane[1] = params[1]; + gen->object_plane[2] = params[2]; + gen->object_plane[3] = params[3]; + gl_tex_gen_set_plane(coord, pname, gen->object_plane); + break; + case GL_EYE_PLANE: + gen->eye_plane[0] = params[0]; + gen->eye_plane[1] = params[1]; + gen->eye_plane[2] = params[2]; + gen->eye_plane[3] = params[3]; + gl_tex_gen_set_plane(coord, pname, gen->eye_plane); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void glTexGeniv(GLenum coord, GLenum pname, const GLint *params) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_tex_gen_t *gen = gl_get_tex_gen(coord); + if (gen == NULL) { + return; + } + + switch (pname) { + case GL_TEXTURE_GEN_MODE: + gl_tex_gen_set_mode(gen, coord, params[0]); + break; + case GL_OBJECT_PLANE: + gen->object_plane[0] = params[0]; + gen->object_plane[1] = params[1]; + gen->object_plane[2] = params[2]; + gen->object_plane[3] = params[3]; + gl_tex_gen_set_plane(coord, pname, gen->object_plane); + break; + case GL_EYE_PLANE: + gen->eye_plane[0] = params[0]; + gen->eye_plane[1] = params[1]; + gen->eye_plane[2] = params[2]; + gen->eye_plane[3] = params[3]; + gl_tex_gen_set_plane(coord, pname, gen->eye_plane); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void glTexGendv(GLenum coord, GLenum pname, const GLdouble *params) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_tex_gen_t *gen = gl_get_tex_gen(coord); + if (gen == NULL) { + return; + } + + switch (pname) { + case GL_TEXTURE_GEN_MODE: + gl_tex_gen_set_mode(gen, coord, params[0]); + break; + case GL_OBJECT_PLANE: + gen->object_plane[0] = params[0]; + gen->object_plane[1] = params[1]; + gen->object_plane[2] = params[2]; + gen->object_plane[3] = params[3]; + gl_tex_gen_set_plane(coord, pname, gen->object_plane); + break; + case GL_EYE_PLANE: + gen->eye_plane[0] = params[0]; + gen->eye_plane[1] = params[1]; + gen->eye_plane[2] = params[2]; + gen->eye_plane[3] = params[3]; + gl_tex_gen_set_plane(coord, pname, gen->eye_plane); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void glCullFace(GLenum mode) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (mode) { + case GL_BACK: + case GL_FRONT: + case GL_FRONT_AND_BACK: + state->cull_face_mode = mode; + gl_set_short(GL_UPDATE_NONE, offsetof(gl_server_state_t, cull_mode), mode); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid face culling mode", mode); + return; + } +} + +void glFrontFace(GLenum dir) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (dir) { + case GL_CW: + case GL_CCW: + state->front_face = dir; + gl_set_short(GL_UPDATE_NONE, offsetof(gl_server_state_t, front_face), dir); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid front face winding direction", dir); + return; + } +} diff --git a/src/GL/query.c b/src/GL/query.c new file mode 100644 index 000000000..e1d6be276 --- /dev/null +++ b/src/GL/query.c @@ -0,0 +1,649 @@ +/** + * @file query.c + * @author Dennis Heinze + * @brief OpenGL query object and state management. + */ +#include "gl_internal.h" +#include "gl_constants.h" +#include "rspq_constants.h" + +extern gl_state_t *state; + +typedef enum { + QUERY_BOOLEAN, + QUERY_INTEGER, + QUERY_FLOAT, + QUERY_DOUBLE, +} query_type_t; + +typedef void (*convert_func)(void*,const void*,uint32_t); + +typedef struct { + convert_func funcs[4]; +} conversion_t; + +static void convert_bool_to_boolean(GLboolean *dst, const bool *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) { dst[i] = src[i]; } +} +static void convert_bool_to_integer(GLint *dst, const bool *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) { dst[i] = src[i]; } +} +static void convert_bool_to_float(GLfloat *dst, const bool *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) { dst[i] = src[i]; } +} +static void convert_bool_to_double(GLdouble *dst, const bool *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) { dst[i] = src[i]; } +} +static const conversion_t from_bool = (conversion_t){ + .funcs = { + (convert_func)convert_bool_to_boolean, + (convert_func)convert_bool_to_integer, + (convert_func)convert_bool_to_float, + (convert_func)convert_bool_to_double, + } +}; + +static void convert_u32_to_boolean(GLboolean *dst, const uint32_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) { dst[i] = src[i] == 0 ? GL_FALSE : GL_TRUE; } +} +static void convert_u32_to_integer(GLint *dst, const uint32_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) { dst[i] = src[i]; } +} +static void convert_u32_to_float(GLfloat *dst, const uint32_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) { dst[i] = src[i]; } +} +static void convert_u32_to_double(GLdouble *dst, const uint32_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) { dst[i] = src[i]; } +} +static const conversion_t from_u32 = (conversion_t){ + .funcs = { + (convert_func)convert_u32_to_boolean, + (convert_func)convert_u32_to_integer, + (convert_func)convert_u32_to_float, + (convert_func)convert_u32_to_double, + } +}; + +static void convert_i32_to_boolean(GLboolean *dst, const int32_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) { dst[i] = src[i] == 0 ? GL_FALSE : GL_TRUE; } +} +static void convert_i32_to_integer(GLint *dst, const int32_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) { dst[i] = src[i]; } +} +static void convert_i32_to_float(GLfloat *dst, const int32_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) { dst[i] = src[i]; } +} +static void convert_i32_to_double(GLdouble *dst, const int32_t *src, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) { dst[i] = src[i]; } +} +static const conversion_t from_i32 = (conversion_t){ + .funcs = { + (convert_func)convert_i32_to_boolean, + (convert_func)convert_i32_to_integer, + (convert_func)convert_i32_to_float, + (convert_func)convert_i32_to_double, + } +}; + +static bool gl_query_get_value_source(GLenum value, const void **src, uint32_t *count, const conversion_t **conversion) +{ + #define SET_RESULT(s, cnt, cnv) ({ \ + *src = (s); \ + *count = (cnt); \ + *conversion = (cnv); \ + }) + + switch (value) { + case GL_MAX_LIGHTS: + static const GLint max_lights = LIGHT_COUNT; + SET_RESULT(&max_lights, 1, &from_i32); + break; + case GL_MAX_LIST_NESTING: + static const GLint max_list_nesting = RSPQ_MAX_BLOCK_NESTING_LEVEL; + SET_RESULT(&max_list_nesting, 1, &from_i32); + break; + case GL_MAX_MODELVIEW_STACK_DEPTH: + static const GLint max_modelview_stack_depth = MODELVIEW_STACK_SIZE; + SET_RESULT(&max_modelview_stack_depth, 1, &from_i32); + break; + case GL_MAX_PIXEL_MAP_TABLE: + static const GLint max_pixel_map_table = MAX_PIXEL_MAP_SIZE; + SET_RESULT(&max_pixel_map_table, 1, &from_i32); + break; + case GL_MAX_PROJECTION_STACK_DEPTH: + static const GLint max_projection_stack_depth = PROJECTION_STACK_SIZE; + SET_RESULT(&max_projection_stack_depth, 1, &from_i32); + break; + case GL_MAX_TEXTURE_SIZE: + static const GLint max_texture_size = MAX_TEXTURE_SIZE; // TODO: What is actually the correct value? + SET_RESULT(&max_texture_size, 1, &from_i32); + break; + case GL_MAX_TEXTURE_STACK_DEPTH: + static const GLint max_texture_stack_depth = TEXTURE_STACK_SIZE; + SET_RESULT(&max_texture_stack_depth, 1, &from_i32); + break; + case GL_MAX_MATRIX_PALETTE_STACK_DEPTH_ARB: + static const GLint max_matrix_palette_stack_depth = PALETTE_STACK_SIZE; + SET_RESULT(&max_matrix_palette_stack_depth, 1, &from_i32); + break; + case GL_MAX_PALETTE_MATRICES_ARB: + static const GLint max_palette_matrices = MATRIX_PALETTE_SIZE; + SET_RESULT(&max_palette_matrices, 1, &from_i32); + break; + case GL_VERTEX_HALF_FIXED_PRECISION_N64: + SET_RESULT(&state->vertex_halfx_precision.precision, 1, &from_u32); + break; + case GL_TEXTURE_COORD_HALF_FIXED_PRECISION_N64: + SET_RESULT(&state->texcoord_halfx_precision.precision, 1, &from_u32); + break; + case GL_VERTEX_ARRAY: + SET_RESULT(&state->array_object->arrays[ATTRIB_VERTEX].enabled, 1, &from_bool); + break; + case GL_VERTEX_ARRAY_SIZE: + SET_RESULT(&state->array_object->arrays[ATTRIB_VERTEX].size, 1, &from_i32); + break; + case GL_VERTEX_ARRAY_STRIDE: + SET_RESULT(&state->array_object->arrays[ATTRIB_VERTEX].stride, 1, &from_u32); + break; + case GL_VERTEX_ARRAY_TYPE: + SET_RESULT(&state->array_object->arrays[ATTRIB_VERTEX].type, 1, &from_u32); + break; + case GL_VERTEX_ARRAY_BUFFER_BINDING_ARB: + SET_RESULT(&state->array_object->arrays[ATTRIB_VERTEX].binding, 1, &from_u32); + break; + case GL_NORMAL_ARRAY: + SET_RESULT(&state->array_object->arrays[ATTRIB_NORMAL].enabled, 1, &from_bool); + break; + case GL_NORMAL_ARRAY_STRIDE: + SET_RESULT(&state->array_object->arrays[ATTRIB_NORMAL].stride, 1, &from_u32); + break; + case GL_NORMAL_ARRAY_TYPE: + SET_RESULT(&state->array_object->arrays[ATTRIB_NORMAL].type, 1, &from_u32); + break; + case GL_NORMAL_ARRAY_BUFFER_BINDING_ARB: + SET_RESULT(&state->array_object->arrays[ATTRIB_NORMAL].binding, 1, &from_u32); + break; + case GL_COLOR_ARRAY: + SET_RESULT(&state->array_object->arrays[ATTRIB_COLOR].enabled, 1, &from_bool); + break; + case GL_COLOR_ARRAY_SIZE: + SET_RESULT(&state->array_object->arrays[ATTRIB_COLOR].size, 1, &from_i32); + break; + case GL_COLOR_ARRAY_STRIDE: + SET_RESULT(&state->array_object->arrays[ATTRIB_COLOR].stride, 1, &from_u32); + break; + case GL_COLOR_ARRAY_TYPE: + SET_RESULT(&state->array_object->arrays[ATTRIB_COLOR].type, 1, &from_u32); + break; + case GL_COLOR_ARRAY_BUFFER_BINDING_ARB: + SET_RESULT(&state->array_object->arrays[ATTRIB_COLOR].binding, 1, &from_u32); + break; + case GL_TEXTURE_COORD_ARRAY: + SET_RESULT(&state->array_object->arrays[ATTRIB_TEXCOORD].enabled, 1, &from_bool); + break; + case GL_TEXTURE_COORD_ARRAY_SIZE: + SET_RESULT(&state->array_object->arrays[ATTRIB_TEXCOORD].size, 1, &from_i32); + break; + case GL_TEXTURE_COORD_ARRAY_STRIDE: + SET_RESULT(&state->array_object->arrays[ATTRIB_TEXCOORD].stride, 1, &from_u32); + break; + case GL_TEXTURE_COORD_ARRAY_TYPE: + SET_RESULT(&state->array_object->arrays[ATTRIB_TEXCOORD].type, 1, &from_u32); + break; + case GL_TEXTURE_COORD_ARRAY_BUFFER_BINDING_ARB: + SET_RESULT(&state->array_object->arrays[ATTRIB_TEXCOORD].binding, 1, &from_u32); + break; + case GL_MATRIX_INDEX_ARRAY_ARB: + SET_RESULT(&state->array_object->arrays[ATTRIB_MTX_INDEX].enabled, 1, &from_bool); + break; + case GL_MATRIX_INDEX_ARRAY_SIZE_ARB: + SET_RESULT(&state->array_object->arrays[ATTRIB_MTX_INDEX].size, 1, &from_i32); + break; + case GL_MATRIX_INDEX_ARRAY_STRIDE_ARB: + SET_RESULT(&state->array_object->arrays[ATTRIB_MTX_INDEX].stride, 1, &from_u32); + break; + case GL_MATRIX_INDEX_ARRAY_TYPE_ARB: + SET_RESULT(&state->array_object->arrays[ATTRIB_MTX_INDEX].type, 1, &from_u32); + break; + case GL_MATRIX_INDEX_ARRAY_BUFFER_BINDING_ARB: + SET_RESULT(&state->array_object->arrays[ATTRIB_MTX_INDEX].binding, 1, &from_u32); + break; + case GL_ARRAY_BUFFER_BINDING_ARB: + SET_RESULT(&state->array_buffer, 1, &from_u32); + break; + case GL_ELEMENT_ARRAY_BUFFER_BINDING_ARB: + SET_RESULT(&state->element_array_buffer, 1, &from_u32); + break; + case GL_VERTEX_ARRAY_BINDING: + SET_RESULT(&state->array_object, 1, &from_u32); + break; + case GL_UNPACK_ALIGNMENT: + SET_RESULT(&state->unpack_alignment, 1, &from_i32); + break; + case GL_UNPACK_LSB_FIRST: + SET_RESULT(&state->unpack_lsb_first, 1, &from_bool); + break; + case GL_UNPACK_ROW_LENGTH: + SET_RESULT(&state->unpack_row_length, 1, &from_i32); + break; + case GL_UNPACK_SKIP_PIXELS: + SET_RESULT(&state->unpack_skip_pixels, 1, &from_i32); + break; + case GL_UNPACK_SKIP_ROWS: + SET_RESULT(&state->unpack_skip_rows, 1, &from_i32); + break; + case GL_UNPACK_SWAP_BYTES: + SET_RESULT(&state->unpack_swap_bytes, 1, &from_bool); + break; + case GL_PACK_ALIGNMENT: + case GL_PACK_LSB_FIRST: + case GL_PACK_ROW_LENGTH: + case GL_PACK_SKIP_PIXELS: + case GL_PACK_SKIP_ROWS: + case GL_PACK_SWAP_BYTES: + case GL_ACCUM_ALPHA_BITS: + case GL_ACCUM_BLUE_BITS: + case GL_ACCUM_CLEAR_VALUE: + case GL_ACCUM_GREEN_BITS: + case GL_ACCUM_RED_BITS: + case GL_ALPHA_BIAS: + case GL_ALPHA_BITS: + case GL_ALPHA_SCALE: + case GL_ALPHA_TEST: + case GL_ALPHA_TEST_FUNC: + case GL_ALPHA_TEST_REF: + case GL_ATTRIB_STACK_DEPTH: + case GL_AUTO_NORMAL: + case GL_AUX_BUFFERS: + case GL_BLEND: + case GL_BLEND_DST: + case GL_BLEND_SRC: + case GL_BLUE_BIAS: + case GL_BLUE_BITS: + case GL_BLUE_SCALE: + case GL_CLIENT_ATTRIB_STACK_DEPTH: + case GL_COLOR_CLEAR_VALUE: + case GL_COLOR_LOGIC_OP: + case GL_COLOR_MATERIAL: + case GL_COLOR_MATERIAL_FACE: + case GL_COLOR_MATERIAL_PARAMETER: + case GL_COLOR_WRITEMASK: + case GL_CULL_FACE: + case GL_CULL_FACE_MODE: + case GL_CURRENT_COLOR: + case GL_CURRENT_INDEX: + case GL_CURRENT_NORMAL: + case GL_CURRENT_RASTER_COLOR: + case GL_CURRENT_RASTER_DISTANCE: + case GL_CURRENT_RASTER_INDEX: + case GL_CURRENT_RASTER_POSITION: + case GL_CURRENT_RASTER_POSITION_VALID: + case GL_CURRENT_RASTER_TEXTURE_COORDS: + case GL_CURRENT_TEXTURE_COORDS: + case GL_DEPTH_BIAS: + case GL_DEPTH_BITS: + case GL_DEPTH_CLEAR_VALUE: + case GL_DEPTH_FUNC: + case GL_DEPTH_RANGE: + case GL_DEPTH_SCALE: + case GL_DEPTH_TEST: + case GL_DEPTH_WRITEMASK: + case GL_DITHER: + case GL_DOUBLEBUFFER: + case GL_DRAW_BUFFER: + case GL_EDGE_FLAG: + case GL_EDGE_FLAG_ARRAY: + case GL_EDGE_FLAG_ARRAY_STRIDE: + case GL_FOG: + case GL_FOG_COLOR: + case GL_FOG_DENSITY: + case GL_FOG_END: + case GL_FOG_HINT: + case GL_FOG_INDEX: + case GL_FOG_MODE: + case GL_FOG_START: + case GL_FRONT_FACE: + case GL_GREEN_BIAS: + case GL_GREEN_BITS: + case GL_GREEN_SCALE: + case GL_INDEX_ARRAY: + case GL_INDEX_ARRAY_STRIDE: + case GL_INDEX_ARRAY_TYPE: + case GL_INDEX_BITS: + case GL_INDEX_CLEAR_VALUE: + case GL_INDEX_LOGIC_OP: + case GL_INDEX_MODE: + case GL_INDEX_OFFSET: + case GL_INDEX_SHIFT: + case GL_INDEX_WRITEMASK: + case GL_LIGHTING: + case GL_LIGHT_MODEL_AMBIENT: + case GL_LIGHT_MODEL_LOCAL_VIEWER: + case GL_LIGHT_MODEL_TWO_SIDE: + case GL_LINE_SMOOTH: + case GL_LINE_SMOOTH_HINT: + case GL_LINE_STIPPLE: + case GL_LINE_STIPPLE_PATTERN: + case GL_LINE_STIPPLE_REPEAT: + case GL_LINE_WIDTH: + case GL_LINE_WIDTH_GRANULARITY: + case GL_LINE_WIDTH_RANGE: + case GL_LIST_BASE: + case GL_LIST_INDEX: + case GL_LIST_MODE: + case GL_LOGIC_OP_MODE: + case GL_MAP1_COLOR_4: + case GL_MAP1_GRID_DOMAIN: + case GL_MAP1_GRID_SEGMENTS: + case GL_MAP1_INDEX: + case GL_MAP1_NORMAL: + case GL_MAP1_TEXTURE_COORD_1: + case GL_MAP1_TEXTURE_COORD_2: + case GL_MAP1_TEXTURE_COORD_3: + case GL_MAP1_TEXTURE_COORD_4: + case GL_MAP1_VERTEX_3: + case GL_MAP1_VERTEX_4: + case GL_MAP2_COLOR_4: + case GL_MAP2_GRID_DOMAIN: + case GL_MAP2_GRID_SEGMENTS: + case GL_MAP2_INDEX: + case GL_MAP2_NORMAL: + case GL_MAP2_TEXTURE_COORD_1: + case GL_MAP2_TEXTURE_COORD_2: + case GL_MAP2_TEXTURE_COORD_3: + case GL_MAP2_TEXTURE_COORD_4: + case GL_MAP2_VERTEX_3: + case GL_MAP2_VERTEX_4: + case GL_MAP_COLOR: + case GL_MAP_STENCIL: + case GL_MATRIX_MODE: + case GL_MAX_CLIENT_ATTRIB_STACK_DEPTH: + case GL_MAX_ATTRIB_STACK_DEPTH: + case GL_MAX_CLIP_PLANES: + case GL_MAX_EVAL_ORDER: + case GL_MAX_NAME_STACK_DEPTH: + case GL_MAX_VIEWPORT_DIMS: + case GL_MODELVIEW_MATRIX: + case GL_MODELVIEW_STACK_DEPTH: + case GL_NAME_STACK_DEPTH: + case GL_NORMALIZE: + case GL_PERSPECTIVE_CORRECTION_HINT: + case GL_PIXEL_MAP_A_TO_A_SIZE: + case GL_PIXEL_MAP_B_TO_B_SIZE: + case GL_PIXEL_MAP_G_TO_G_SIZE: + case GL_PIXEL_MAP_I_TO_A_SIZE: + case GL_PIXEL_MAP_I_TO_B_SIZE: + case GL_PIXEL_MAP_I_TO_G_SIZE: + case GL_PIXEL_MAP_I_TO_I_SIZE: + case GL_PIXEL_MAP_I_TO_R_SIZE: + case GL_PIXEL_MAP_R_TO_R_SIZE: + case GL_PIXEL_MAP_S_TO_S_SIZE: + case GL_POINT_SIZE: + case GL_POINT_SIZE_GRANULARITY: + case GL_POINT_SIZE_RANGE: + case GL_POINT_SMOOTH: + case GL_POINT_SMOOTH_HINT: + case GL_POLYGON_MODE: + case GL_POLYGON_OFFSET_FACTOR: + case GL_POLYGON_OFFSET_UNITS: + case GL_POLYGON_OFFSET_FILL: + case GL_POLYGON_OFFSET_LINE: + case GL_POLYGON_OFFSET_POINT: + case GL_POLYGON_SMOOTH: + case GL_POLYGON_SMOOTH_HINT: + case GL_POLYGON_STIPPLE: + case GL_PROJECTION_MATRIX: + case GL_PROJECTION_STACK_DEPTH: + case GL_READ_BUFFER: + case GL_RED_BIAS: + case GL_RED_BITS: + case GL_RED_SCALE: + case GL_RENDER_MODE: + case GL_RGBA_MODE: + case GL_MULTISAMPLE_ARB: + case GL_SAMPLE_ALPHA_TO_COVERAGE_ARB: + case GL_SAMPLE_ALPHA_TO_ONE_ARB: + case GL_SAMPLE_COVERAGE_ARB: + case GL_SAMPLE_BUFFERS_ARB: + case GL_SAMPLES_ARB: + case GL_SAMPLE_COVERAGE_VALUE_ARB: + case GL_SAMPLE_COVERAGE_INVERT_ARB: + case GL_SCISSOR_BOX: + case GL_SCISSOR_TEST: + case GL_SHADE_MODEL: + case GL_STENCIL_BITS: + case GL_STENCIL_CLEAR_VALUE: + case GL_STENCIL_FAIL: + case GL_STENCIL_FUNC: + case GL_STENCIL_PASS_DEPTH_FAIL: + case GL_STENCIL_PASS_DEPTH_PASS: + case GL_STENCIL_REF: + case GL_STENCIL_TEST: + case GL_STENCIL_VALUE_MASK: + case GL_STENCIL_WRITEMASK: + case GL_STEREO: + case GL_SUBPIXEL_BITS: + case GL_TEXTURE_1D: + case GL_TEXTURE_2D: + case GL_TEXTURE_GEN_Q: + case GL_TEXTURE_GEN_R: + case GL_TEXTURE_GEN_S: + case GL_TEXTURE_GEN_T: + case GL_TEXTURE_MATRIX: + case GL_TEXTURE_STACK_DEPTH: + case GL_VIEWPORT: + case GL_ZOOM_X: + case GL_ZOOM_Y: + case GL_INDEX_ARRAY_BUFFER_BINDING_ARB: + case GL_EDGE_FLAG_ARRAY_BUFFER_BINDING_ARB: + case GL_CLIP_PLANE0: + case GL_CLIP_PLANE1: + case GL_CLIP_PLANE2: + case GL_CLIP_PLANE3: + case GL_CLIP_PLANE4: + case GL_CLIP_PLANE5: + case GL_LIGHT0: + case GL_LIGHT1: + case GL_LIGHT2: + case GL_LIGHT3: + case GL_LIGHT4: + case GL_LIGHT5: + case GL_LIGHT6: + case GL_LIGHT7: + assertf(0, "querying %#04lx is not supported", value); + return false; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid value name", value); + return false; + } + + return true; + + #undef SET_RESULT +} + +static void gl_query_get_value(GLenum value, void *dst, query_type_t dst_type) +{ + const void *src; + uint32_t count; + const conversion_t *conversion; + + if (gl_query_get_value_source(value, &src, &count, &conversion)) + { + assertf(conversion->funcs[dst_type], "Conversion function is missing"); + conversion->funcs[dst_type](dst, src, count); + } +} + +void glGetBooleanv(GLenum value, GLboolean *data) +{ + gl_query_get_value(value, data, QUERY_BOOLEAN); +} + +void glGetIntegerv(GLenum value, GLint *data) +{ + gl_query_get_value(value, data, QUERY_INTEGER); +} + +void glGetFloatv(GLenum value, GLfloat *data) +{ + gl_query_get_value(value, data, QUERY_FLOAT); +} + +void glGetDoublev(GLenum value, GLdouble *data) +{ + gl_query_get_value(value, data, QUERY_DOUBLE); +} + +GLboolean glIsEnabled(GLenum value) +{ + switch (value) { + case GL_VERTEX_ARRAY: + return state->array_object->arrays[ATTRIB_VERTEX].enabled; + case GL_NORMAL_ARRAY: + return state->array_object->arrays[ATTRIB_NORMAL].enabled; + case GL_COLOR_ARRAY: + return state->array_object->arrays[ATTRIB_COLOR].enabled; + case GL_TEXTURE_COORD_ARRAY: + return state->array_object->arrays[ATTRIB_TEXCOORD].enabled; + case GL_MATRIX_INDEX_ARRAY_ARB: + return state->array_object->arrays[ATTRIB_MTX_INDEX].enabled; + case GL_ALPHA_TEST: + case GL_AUTO_NORMAL: + case GL_BLEND: + case GL_CLIP_PLANE0: + case GL_CLIP_PLANE1: + case GL_CLIP_PLANE2: + case GL_CLIP_PLANE3: + case GL_CLIP_PLANE4: + case GL_CLIP_PLANE5: + case GL_COLOR_MATERIAL: + case GL_CULL_FACE: + case GL_DEPTH_TEST: + case GL_DITHER: + case GL_FOG: + case GL_LIGHT0: + case GL_LIGHT1: + case GL_LIGHT2: + case GL_LIGHT3: + case GL_LIGHT4: + case GL_LIGHT5: + case GL_LIGHT6: + case GL_LIGHT7: + case GL_LIGHTING: + case GL_LINE_SMOOTH: + case GL_LINE_STIPPLE: + case GL_LOGIC_OP: + case GL_MAP1_COLOR_4: + case GL_MAP1_INDEX: + case GL_MAP1_NORMAL: + case GL_MAP1_TEXTURE_COORD_1: + case GL_MAP1_TEXTURE_COORD_2: + case GL_MAP1_TEXTURE_COORD_3: + case GL_MAP1_TEXTURE_COORD_4: + case GL_MAP1_VERTEX_3: + case GL_MAP1_VERTEX_4: + case GL_MAP2_COLOR_4: + case GL_MAP2_INDEX: + case GL_MAP2_NORMAL: + case GL_MAP2_TEXTURE_COORD_1: + case GL_MAP2_TEXTURE_COORD_2: + case GL_MAP2_TEXTURE_COORD_3: + case GL_MAP2_TEXTURE_COORD_4: + case GL_MAP2_VERTEX_3: + case GL_MAP2_VERTEX_4: + case GL_NORMALIZE: + case GL_POINT_SMOOTH: + case GL_POLYGON_SMOOTH: + case GL_POLYGON_STIPPLE: + case GL_SCISSOR_TEST: + case GL_STENCIL_TEST: + case GL_TEXTURE_1D: + case GL_TEXTURE_2D: + case GL_TEXTURE_GEN_Q: + case GL_TEXTURE_GEN_R: + case GL_TEXTURE_GEN_S: + case GL_TEXTURE_GEN_T: + case GL_EDGE_FLAG_ARRAY: + case GL_INDEX_ARRAY: + assertf(0, "querying %#04lx is not supported", value); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid value name", value); + } + return false; +} + +void glGetPointerv(GLenum pname, GLvoid **params) +{ + switch (pname) { + case GL_VERTEX_ARRAY_POINTER: + *params = (GLvoid*)state->array_object->arrays[ATTRIB_VERTEX].pointer; + break; + case GL_NORMAL_ARRAY_POINTER: + *params = (GLvoid*)state->array_object->arrays[ATTRIB_NORMAL].pointer; + break; + case GL_COLOR_ARRAY_POINTER: + *params = (GLvoid*)state->array_object->arrays[ATTRIB_COLOR].pointer; + break; + case GL_TEXTURE_COORD_ARRAY_POINTER: + *params = (GLvoid*)state->array_object->arrays[ATTRIB_TEXCOORD].pointer; + break; + case GL_MATRIX_INDEX_ARRAY_POINTER_ARB: + *params = (GLvoid*)state->array_object->arrays[ATTRIB_MTX_INDEX].pointer; + break; + case GL_EDGE_FLAG_ARRAY_POINTER: + case GL_INDEX_ARRAY_POINTER: + case GL_FEEDBACK_BUFFER_POINTER: + case GL_SELECTION_BUFFER_POINTER: + assertf(0, "querying %#04lx is not supported", pname); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid pointer name", pname); + break; + } +} + +static const char *extensions = "GL_ARB_multisample " + "GL_EXT_packed_pixels " + "GL_ARB_vertex_buffer_object " + "GL_ARB_texture_mirrored_repeat " + "GL_ARB_texture_non_power_of_two " + "GL_ARB_vertex_array_object " + "GL_ARB_matrix_palette " + "GL_N64_RDPQ_interop " + "GL_N64_surface_image " + "GL_N64_half_fixed_point " + "GL_N64_reduced_aliasing " + "GL_N64_interpenetrating " + "GL_N64_copy_matrix " + "GL_N64_texture_flip"; + +GLubyte *glGetString(GLenum name) +{ + switch (name) { + case GL_VENDOR: + return (GLubyte*)"Libdragon"; + case GL_RENDERER: + return (GLubyte*)"N64"; + case GL_VERSION: + return (GLubyte*)"1.1"; + case GL_EXTENSIONS: + return (GLubyte*)extensions; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid string name", name); + return NULL; + } +} diff --git a/src/GL/rendermode.c b/src/GL/rendermode.c new file mode 100644 index 000000000..b4b0f4bb2 --- /dev/null +++ b/src/GL/rendermode.c @@ -0,0 +1,416 @@ +/** + * @file rendermode.c + * @author Dennis Heinze + * @brief OpenGL render mode and state configuration. + */ +#include + +#include "gl_internal.h" +#include "rdpq_mode.h" +#include "rdpq_debug.h" +#include "rdpq_macros.h" +#include "rspq.h" + +_Static_assert(FLAG2_MULTISAMPLE == SOM_AA_ENABLE); +_Static_assert(FLAG2_REDUCED_ALIASING == (SOMX_AA_REDUCED>>32)); +_Static_assert(FLAG_BLEND << ZMODE_BLEND_FLAG_SHIFT == SOM_ZMODE_TRANSPARENT); +_Static_assert(FLAG_TEXTURE_ACTIVE == (1 << TEXTURE_ACTIVE_SHIFT)); +_Static_assert(FLAG_TEXTURE_ACTIVE >> TEX_ACTIVE_COMBINER_SHIFT == COMBINER_FLAG_TEXTURE); + +extern gl_state_t *state; + +// All possible combinations of blend functions. Configs that cannot be supported by the RDP are set to 0. +// NOTE: We always set fog alpha to one to support GL_ONE in both factors +// TODO: src = ZERO, dst = ONE_MINUS_SRC_ALPHA could be done with BLEND_RGB * IN_ALPHA + MEMORY_RGB * INV_MUX_ALPHA +static const rdpq_blender_t blend_configs[64] = { + RDPQ_BLENDER((IN_RGB, ZERO, MEMORY_RGB, ZERO)), // src = ZERO, dst = ZERO + RDPQ_BLENDER((IN_RGB, ZERO, MEMORY_RGB, ONE)), // src = ZERO, dst = ONE + RDPQ_BLENDER((MEMORY_RGB, IN_ALPHA, IN_RGB, ZERO)), // src = ZERO, dst = SRC_ALPHA + 0, // src = ZERO, dst = ONE_MINUS_SRC_ALPHA + 0, // src = ZERO, dst = GL_DST_COLOR + 0, // src = ZERO, dst = GL_ONE_MINUS_DST_COLOR + RDPQ_BLENDER((IN_RGB, ZERO, MEMORY_RGB, MEMORY_CVG)), // src = ZERO, dst = DST_ALPHA + 0, // src = ZERO, dst = ONE_MINUS_DST_ALPHA + + RDPQ_BLENDER((IN_RGB, FOG_ALPHA, MEMORY_RGB, ZERO)), // src = ONE, dst = ZERO + RDPQ_BLENDER((IN_RGB, FOG_ALPHA, MEMORY_RGB, ONE)), // src = ONE, dst = ONE + RDPQ_BLENDER((MEMORY_RGB, IN_ALPHA, IN_RGB, ONE)), // src = ONE, dst = SRC_ALPHA + 0, // src = ONE, dst = ONE_MINUS_SRC_ALPHA + 0, // src = ONE, dst = GL_DST_COLOR + 0, // src = ONE, dst = GL_ONE_MINUS_DST_COLOR + RDPQ_BLENDER((IN_RGB, FOG_ALPHA, MEMORY_RGB, MEMORY_CVG)), // src = ONE, dst = DST_ALPHA + 0, // src = ONE, dst = ONE_MINUS_DST_ALPHA + + RDPQ_BLENDER((IN_RGB, IN_ALPHA, MEMORY_RGB, ZERO)), // src = SRC_ALPHA, dst = ZERO + RDPQ_BLENDER((IN_RGB, IN_ALPHA, MEMORY_RGB, ONE)), // src = SRC_ALPHA, dst = ONE + 0, // src = SRC_ALPHA, dst = SRC_ALPHA + RDPQ_BLENDER((IN_RGB, IN_ALPHA, MEMORY_RGB, INV_MUX_ALPHA)), // src = SRC_ALPHA, dst = ONE_MINUS_SRC_ALPHA + 0, // src = SRC_ALPHA, dst = GL_DST_COLOR + 0, // src = SRC_ALPHA, dst = GL_ONE_MINUS_DST_COLOR + RDPQ_BLENDER((IN_RGB, IN_ALPHA, MEMORY_RGB, MEMORY_CVG)), // src = SRC_ALPHA, dst = DST_ALPHA + 0, // src = SRC_ALPHA, dst = ONE_MINUS_DST_ALPHA + + 0, // src = ONE_MINUS_SRC_ALPHA, dst = ZERO + 0, // src = ONE_MINUS_SRC_ALPHA, dst = ONE + RDPQ_BLENDER((MEMORY_RGB, IN_ALPHA, IN_RGB, INV_MUX_ALPHA)), // src = ONE_MINUS_SRC_ALPHA, dst = SRC_ALPHA + 0, // src = ONE_MINUS_SRC_ALPHA, dst = ONE_MINUS_SRC_ALPHA + 0, // src = ONE_MINUS_SRC_ALPHA, dst = GL_DST_COLOR + 0, // src = ONE_MINUS_SRC_ALPHA, dst = GL_ONE_MINUS_DST_COLOR + 0, // src = ONE_MINUS_SRC_ALPHA, dst = DST_ALPHA + 0, // src = ONE_MINUS_SRC_ALPHA, dst = ONE_MINUS_DST_ALPHA + + 0, 0, 0, 0, 0, 0, 0, 0, // src = GL_DST_COLOR, dst = ... + 0, 0, 0, 0, 0, 0, 0, 0, // src = GL_ONE_MINUS_DST_COLOR, dst = ... + + RDPQ_BLENDER((MEMORY_RGB, ZERO, IN_RGB, MEMORY_CVG)), // src = DST_ALPHA, dst = ZERO + RDPQ_BLENDER((MEMORY_RGB, FOG_ALPHA, IN_RGB, MEMORY_CVG)), // src = DST_ALPHA, dst = ONE + RDPQ_BLENDER((MEMORY_RGB, IN_ALPHA, IN_RGB, MEMORY_CVG)), // src = DST_ALPHA, dst = SRC_ALPHA + 0, // src = DST_ALPHA, dst = ONE_MINUS_SRC_ALPHA + 0, // src = DST_ALPHA, dst = GL_DST_COLOR + 0, // src = DST_ALPHA, dst = GL_ONE_MINUS_DST_COLOR + 0, // src = DST_ALPHA, dst = DST_ALPHA + 0, // src = DST_ALPHA, dst = ONE_MINUS_DST_ALPHA + + 0, 0, 0, 0, 0, 0, 0, 0, // src = ONE_MINUS_DST_ALPHA, dst = ... +}; + +void gl_rendermode_init() +{ + state->fog_start = 0.0f; + state->fog_end = 1.0f; + + glEnable(GL_DITHER); + glBlendFunc(GL_ONE, GL_ZERO); + glDepthFunc(GL_LESS); + glDepthMask(GL_TRUE); + glAlphaFunc(GL_ALWAYS, 0.0f); + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); + + GLfloat fog_color[] = {0, 0, 0, 0}; + glFogfv(GL_FOG_COLOR, fog_color); +} + +void gl_update_fog() +{ + float fog_diff = state->fog_start - state->fog_end; + // start == end is undefined, so disable fog by setting the factor to 0 + state->fog_factor = fabsf(fog_diff) < FLT_MIN ? 0.0f : 1.0f / fog_diff; + state->fog_offset = state->fog_end; + + // Convert to s15.16 and premultiply with 1.15 conversion factor + int32_t factor_fx = state->fog_factor * (1<<(16 + 7 + (8 - VTX_SHIFT))); + int16_t offset_fx = state->fog_offset * (1<> 16; + uint16_t factor_f = factor_fx & 0xFFFF; + + uint64_t packed = (((uint64_t)factor_i) << 48) | (((uint64_t)offset_fx) << 32) | (((uint64_t)factor_f) << 16); + + gl_set_long(GL_UPDATE_NONE, offsetof(gl_server_state_t, fog_params), packed); +} + +void gl_set_fog_start(GLfloat param) +{ + state->fog_start = param; + gl_update_fog(); +} + +void gl_set_fog_end(GLfloat param) +{ + state->fog_end = param; + gl_update_fog(); +} + +void glFogi(GLenum pname, GLint param) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (pname) { + case GL_FOG_MODE: + assertf(param == GL_LINEAR, "Only linear fog is supported!"); + break; + case GL_FOG_START: + gl_set_fog_start(param); + break; + case GL_FOG_END: + gl_set_fog_end(param); + break; + case GL_FOG_DENSITY: + case GL_FOG_INDEX: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void glFogf(GLenum pname, GLfloat param) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (pname) { + case GL_FOG_MODE: + assertf(param == GL_LINEAR, "Only linear fog is supported!"); + break; + case GL_FOG_START: + gl_set_fog_start(param); + break; + case GL_FOG_END: + gl_set_fog_end(param); + break; + case GL_FOG_DENSITY: + case GL_FOG_INDEX: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void glFogiv(GLenum pname, const GLint *params) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (pname) { + case GL_FOG_COLOR: + rdpq_set_fog_color(RGBA32( + MAX(params[0]>>23, 0), + MAX(params[1]>>23, 0), + MAX(params[2]>>23, 0), + 0xFF + )); + break; + case GL_FOG_MODE: + case GL_FOG_START: + case GL_FOG_END: + case GL_FOG_DENSITY: + case GL_FOG_INDEX: + glFogi(pname, params[0]); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void glFogfv(GLenum pname, const GLfloat *params) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (pname) { + case GL_FOG_COLOR: + rdpq_set_fog_color(RGBA32( + FLOAT_TO_U8(params[0]), + FLOAT_TO_U8(params[1]), + FLOAT_TO_U8(params[2]), + 0xFF + )); + break; + case GL_FOG_MODE: + case GL_FOG_START: + case GL_FOG_END: + case GL_FOG_DENSITY: + case GL_FOG_INDEX: + glFogf(pname, params[0]); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void glScissor(GLint left, GLint bottom, GLsizei width, GLsizei height) +{ + if (!gl_ensure_no_begin_end()) return; + + if (left < 0) { + gl_set_error(GL_INVALID_VALUE, "Left must not be negative"); + return; + } + + if (bottom < 0) { + gl_set_error(GL_INVALID_VALUE, "Bottom must not be negative"); + return; + } + + uint64_t rect = (((uint64_t)left) << 48) | (((uint64_t)bottom) << 32) | (((uint64_t)width) << 16) | ((uint64_t)height); + gl_set_long(GL_UPDATE_SCISSOR, offsetof(gl_server_state_t, scissor_rect), rect); +} + +void glBlendFunc(GLenum src, GLenum dst) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (src) { + case GL_ZERO: + case GL_ONE: + case GL_SRC_ALPHA: + case GL_ONE_MINUS_SRC_ALPHA: + case GL_DST_ALPHA: + break; + case GL_DST_COLOR: + case GL_ONE_MINUS_DST_COLOR: + case GL_ONE_MINUS_DST_ALPHA: + case GL_SRC_ALPHA_SATURATE: + assertf(0, "Unsupported blend source factor"); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid blend source factor", src); + return; + } + + switch (dst) { + case GL_ZERO: + case GL_ONE: + case GL_SRC_ALPHA: + case GL_ONE_MINUS_SRC_ALPHA: + case GL_DST_ALPHA: + break; + case GL_SRC_COLOR: + case GL_ONE_MINUS_DST_ALPHA: + case GL_ONE_MINUS_SRC_COLOR: + assertf(0, "Unsupported blend destination factor"); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid blend destination factor", dst); + return; + } + + uint32_t config_index = ((src & 0x7) << 3) | (dst & 0x7); + + uint32_t cycle = blend_configs[config_index] | SOM_BLENDING; + assertf(cycle != 0, "Unsupported blend function"); + + // TODO: coalesce these + gl_set_word(GL_UPDATE_NONE, offsetof(gl_server_state_t, blend_src), (((uint32_t)src) << 16) | (uint32_t)dst); + gl_set_word(GL_UPDATE_NONE, offsetof(gl_server_state_t, blend_cycle), cycle); +} + +void glDepthFunc(GLenum func) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (func) { + case GL_LESS: + case GL_ALWAYS: + case GL_EQUAL: + case GL_LESS_INTERPENETRATING_N64: + gl_set_short(GL_UPDATE_NONE, offsetof(gl_server_state_t, depth_func), (uint16_t)func); + break; + case GL_NEVER: + case GL_LEQUAL: + case GL_GREATER: + case GL_NOTEQUAL: + case GL_GEQUAL: + assertf(0, "Depth func not supported: %#04lx", func); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid depth function", func); + return; + } +} + +void glDepthMask(GLboolean mask) +{ + if (!gl_ensure_no_begin_end()) return; + + gl_set_flag(GL_UPDATE_NONE, FLAG_DEPTH_MASK, mask); +} + +void glAlphaFunc(GLenum func, GLclampf ref) +{ + if (!gl_ensure_no_begin_end()) return; + + switch (func) { + case GL_GREATER: + case GL_ALWAYS: + gl_set_short(GL_UPDATE_NONE, offsetof(gl_server_state_t, alpha_func), (uint16_t)func); + gl_set_byte(GL_UPDATE_NONE, offsetof(gl_server_state_t, alpha_ref), FLOAT_TO_U8(ref)); + rdpq_set_blend_color(RGBA32(0, 0, 0, FLOAT_TO_U8(ref))); + break; + case GL_NEVER: + case GL_EQUAL: + case GL_LEQUAL: + case GL_LESS: + case GL_NOTEQUAL: + case GL_GEQUAL: + assertf(0, "Alpha func not supported: %#04lx", func); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid alpha function", func); + return; + } +} + +void glTexEnvi(GLenum target, GLenum pname, GLint param) +{ + if (!gl_ensure_no_begin_end()) return; + + if (target != GL_TEXTURE_ENV) { + gl_set_error(GL_INVALID_ENUM, "Target must be GL_TEXTURE_ENV"); + return; + } + + if (pname != GL_TEXTURE_ENV_MODE) { + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } + + switch (param) { + case GL_MODULATE: + case GL_REPLACE: + gl_set_short(GL_UPDATE_NONE, offsetof(gl_server_state_t, tex_env_mode), (uint16_t)param); + break; + case GL_DECAL: + case GL_BLEND: + assertf(0, "Unsupported Tex Env mode!"); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid tex env mode", param); + return; + } +} +void glTexEnvf(GLenum target, GLenum pname, GLfloat param) +{ + if (!gl_ensure_no_begin_end()) return; + + glTexEnvi(target, pname, param); +} + +void glTexEnviv(GLenum target, GLenum pname, const GLint *params) +{ + if (!gl_ensure_no_begin_end()) return; + + if (target != GL_TEXTURE_ENV) { + gl_set_error(GL_INVALID_ENUM, "Target must be GL_TEXTURE_ENV"); + return; + } + + switch (pname) { + case GL_TEXTURE_ENV_COLOR: + assertf(0, "Tex env color is not supported!"); + break; + default: + glTexEnvi(target, pname, params[0]); + break; + } +} + +void glTexEnvfv(GLenum target, GLenum pname, const GLfloat *params) +{ + if (!gl_ensure_no_begin_end()) return; + + if (target != GL_TEXTURE_ENV) { + gl_set_error(GL_INVALID_ENUM, "Target must be GL_TEXTURE_ENV"); + return; + } + + switch (pname) { + case GL_TEXTURE_ENV_COLOR: + assertf(0, "Tex env color is not supported!"); + break; + default: + glTexEnvf(target, pname, params[0]); + break; + } +} diff --git a/src/GL/rsp_gl.S b/src/GL/rsp_gl.S new file mode 100644 index 000000000..2cec04b02 --- /dev/null +++ b/src/GL/rsp_gl.S @@ -0,0 +1,1231 @@ +############################################################ +# rsp_gl.S +# +# Authors: Dennis Heinze +############################################################ +#include +#include +#include "gl_constants.h" +#include "GL/gl_enums.h" + + .data + + RSPQ_BeginOverlayHeader + RSPQ_DefineCommand GLCmd_SetFlag, 8 # 0x0 + RSPQ_DefineCommand GLCmd_SetByte, 8 # 0x1 + RSPQ_DefineCommand GLCmd_SetShort, 8 # 0x2 + RSPQ_DefineCommand GLCmd_SetWord, 8 # 0x3 + RSPQ_DefineCommand GLCmd_SetLong, 12 # 0x4 + RSPQ_DefineCommand GLCmd_Update, 4 # 0x5 + RSPQ_DefineCommand GLCmd_BindTexture, 8 # 0x6 + RSPQ_DefineCommand GLCmd_GetValue, 8 # 0x7 + RSPQ_DefineCommand GLCmd_CopyFillColor, 4 # 0x8 + RSPQ_DefineCommand GLCmd_SetLightPos, 12 # 0x9 + RSPQ_DefineCommand GLCmd_MatrixPush, 4 # 0xA + RSPQ_DefineCommand GLCmd_MatrixPop, 4 # 0xB + RSPQ_DefineCommand GLCmd_MatrixLoad, 68 # 0xC + RSPQ_DefineCommand GLCmd_PreInitPipe, 4 # 0xD + RSPQ_DefineCommand GLCmd_PreInitPipeTex,4 # 0xE + RSPQ_DefineCommand GLCmd_SetPaletteIdx, 4 # 0xF + RSPQ_DefineCommand GLCmd_MatrixCopy, 4 # 0x10 + RSPQ_EndOverlayHeader + + RSPQ_BeginSavedState + #include "rsp_gl_state.inc" + + # These are only required for RDP state changes + GL_BOUND_TEXTURES: + GL_BOUND_TEXTURE_1D: .ds.b TEXTURE_OBJECT_SIZE + GL_BOUND_TEXTURE_2D: .ds.b TEXTURE_OBJECT_SIZE + GL_STATE_SCISSOR_RECT: .half 0, 0, 0, 0 + GL_STATE_BLEND_CYCLE: .word 0 + GL_STATE_FOG_COLOR: .word 0 + GL_STATE_FLAGS2: .word 0 + GL_STATE_TEXTURE_IDS: .word 0, 0 + GL_STATE_UPLOADED_TEX: .word 0 + GL_STATE_FILL_COLOR: .word 0 + GL_STATE_FILL_DEPTH: .word 0 + GL_STATE_PALETTE_INDEX: .word 0 + GL_STATE_DITHER_MODE: .word 0 + GL_STATE_FB_SIZE: .half 0, 0 + GL_STATE_DEPTH_FUNC: .half 0 + GL_STATE_ALPHA_FUNC: .half 0 + GL_STATE_BLEND_SRC: .half 0 + GL_STATE_BLEND_DST: .half 0 + GL_STATE_TEX_ENV_MODE: .half 0 + GL_STATE_ALPHA_REF: .byte 0 + GL_STATE_PALETTE_DIRTY: .ds.b PALETTE_DIRTY_FLAGS_SIZE + RSPQ_EndSavedState + +COMBINER_TABLE: + # No texture + .quad RDPQ_COMBINER1((0, 0, 0, SHADE), (0, 0, 0, SHADE)) # "modulate" + .quad RDPQ_COMBINER1((0, 0, 0, PRIM), (0, 0, 0, PRIM)) # constant "modulate" + .quad RDPQ_COMBINER1((0, 0, 0, SHADE), (0, 0, 0, SHADE)) # "replace" + .quad RDPQ_COMBINER1((0, 0, 0, PRIM), (0, 0, 0, PRIM)) # constant "replace" + + # Texture enabled + .quad RDPQ_COMBINER1((TEX0, 0, SHADE, 0), (TEX0, 0, SHADE, 0)) # modulate + .quad RDPQ_COMBINER1((TEX0, 0, PRIM, 0), (TEX0, 0, PRIM, 0)) # constant modulate + .quad RDPQ_COMBINER1((0, 0, 0, TEX0), (0, 0, 0, TEX0)) # replace + .quad RDPQ_COMBINER1((0, 0, 0, TEX0), (0, 0, 0, TEX0)) # constant replace + +COMBINER_MIPMAPMASK_TABLE: + .quad RDPQ_COMB1_MASK + .quad RDPQ_COMB1_MASK + .quad RDPQ_COMB1_MASK + .quad RDPQ_COMB1_MASK + + .quad RDPQ_COMB1_MASK ^ (1<<37) ^ (1<<21) + .quad RDPQ_COMB1_MASK ^ (1<<37) ^ (1<<21) + .quad RDPQ_COMB1_MASK ^ (1<<6) ^ (1<<0) + .quad RDPQ_COMB1_MASK ^ (1<<6) ^ (1<<0) + +UPDATE_FUNCTIONS: + .short RSPQ_Loop - _start # Do nothing + .short GL_UpdateScissor - _start + .short GL_UpdateTextureCompleteness - _start + .short GL_UpdateTextureObjects - _start + +CONVERT_CONST: .half 1, 2, 0x8000, 0x200, 0xFF, 134, 0x7F, 0x80 + + .bss + + .align 3 +TEX_UPLOAD_STAGING: .ds.b 0x150 + + .text + + ############################################################# + # GLCmd_SetFlag + # + # Sets or clears a flag + # + # ARGS: + # a0: Bit 31..24: Command id + # Bit 23..12: Offset into UPDATE_FUNCTIONS jump table + # Bit 11..2: Offset of flag value in GL_STATE + # Bit 0: If 1, set the flag, otherwise clear it + # a1: flag mask (inverted if clearing) + ############################################################# + .func GLCmd_SetFlag +GLCmd_SetFlag: + li t0, ~0x3 + and t0, a0, t0 + andi t1, a0, 1 + lw t2, %lo(GL_STATE)(t0) + beqz t1, 1f + and t3, t2, a1 + or t3, t2, a1 + +1: + j GLCmd_Update + sw t3, %lo(GL_STATE)(t0) + .endfunc + + .func GLCmd_SetByte +GLCmd_SetByte: + j GLCmd_Update + sb a1, %lo(GL_STATE)(a0) + .endfunc + + .func GLCmd_SetShort +GLCmd_SetShort: + j GLCmd_Update + sh a1, %lo(GL_STATE)(a0) + .endfunc + + .func GLCmd_SetLong +GLCmd_SetLong: + sw a2, %lo(GL_STATE) + 4(a0) + # fallthrough! + .endfunc + + .func GLCmd_SetWord +GLCmd_SetWord: + sw a1, %lo(GL_STATE) + 0(a0) + # fallthrough! + .endfunc + +GLCmd_Update: + srl t0, a0, 12 + lhu t1, %lo(UPDATE_FUNCTIONS)(t0) + jr t1 + #nop + +GLCmd_GetValue: + srl t0, a0, 12 + addiu s4, a0, %lo(GL_STATE) + andi s4, 0xFFF + andi t0, 0xFFF + j DMAOut + move s0, a1 + + ######################################## + # GLCmd_BindTexture + # + # Args: + # a0: Bit 31..24: Command id + # Bit 23..16: Offset into GL_STATE_TEXTURE_IDS + # Bit 15..0: Offset into GL_BOUND_TEXTURES + # a1: The texture's RDRAM address + ######################################## +GLCmd_BindTexture: + srl t3, a0, 16 + andi t3, 0xFC + lw s0, %lo(GL_STATE_TEXTURE_IDS)(t3) + # Do nothing if texture is already bound + beq s0, a1, RSPQ_Loop + addiu s4, a0, %lo(GL_BOUND_TEXTURES) + + # DMA currently bound texture out + jal DMAOutAsync + li t0, DMA_SIZE(TEXTURE_OBJECT_SIZE, 1) + + # DMA new texture in + jal DMAIn + move s0, a1 + + # Always reset uploaded texture when binding + sw zero, %lo(GL_STATE_UPLOADED_TEX) + j RSPQ_Loop + sw s0, %lo(GL_STATE_TEXTURE_IDS)(t3) + +GLCmd_CopyFillColor: + lw a1, %lo(GL_STATE)(a0) + jal_and_j RDPQ_WriteSetFillColor, RDPQ_Finalize + +GLCmd_SetLightPos: + #define v___ $v01 + + #define vpos $v02 + + #define vmtx0_i $v03 + #define vmtx0_f $v04 + #define vmtx1_i $v05 + #define vmtx1_f $v06 + #define vmtx2_i $v07 + #define vmtx2_f $v08 + #define vmtx3_i $v09 + #define vmtx3_f $v10 + + addi s0, rspq_dmem_buf_ptr, %lo(RSPQ_DMEM_BUFFER) - 8 + li s1, %lo(GL_MATRIX_MODELVIEW) + ldv vpos, 0x00,s0 + ldv vmtx0_i, 0x00,s1 + ldv vmtx1_i, 0x08,s1 + ldv vmtx2_i, 0x10,s1 + ldv vmtx3_i, 0x18,s1 + ldv vmtx0_f, 0x20,s1 + ldv vmtx1_f, 0x28,s1 + ldv vmtx2_f, 0x30,s1 + ldv vmtx3_f, 0x38,s1 + + vmudn v___, vmtx0_f, vpos.e0 + vmadh v___, vmtx0_i, vpos.e0 + vmadn v___, vmtx1_f, vpos.e1 + vmadh v___, vmtx1_i, vpos.e1 + vmadn v___, vmtx2_f, vpos.e2 + vmadh v___, vmtx2_i, vpos.e2 + vmadn v___, vmtx3_f, vpos.e3 + vmadh vpos, vmtx3_i, vpos.e3 + + addi s0, a0, %lo(GL_STATE) + LIGHT_POSITION_OFFSET + jr ra + sdv vpos, 0x00,s0 + + #undef v___ + + #undef vpos + + #undef vmtx0_i + #undef vmtx0_f + #undef vmtx1_i + #undef vmtx1_f + #undef vmtx2_i + #undef vmtx2_f + #undef vmtx3_i + #undef vmtx3_f + + ############################################################# + # GL_MatrixMarkDirty + # + # ARGS: + # t1: Matrix index + ############################################################# + .func GL_MatrixMarkDirty +GL_MatrixMarkDirty: + #define mtx_index t1 + #define flag_offset s7 + #define flag_shift t6 + #define palette_index t7 + #define flag t8 + #define cur_flags t9 + bne mtx_index, PALETTE_MATRIX_INDEX, 1f + lw palette_index, %lo(GL_STATE_PALETTE_INDEX) + + li flag, 1 + srl palette_index, 6 + srl flag_offset, palette_index, 3 + lbu cur_flags, %lo(GL_STATE_PALETTE_DIRTY)(flag_offset) + and flag_shift, palette_index, 0x7 + sllv flag, flag, flag_shift + or cur_flags, flag + jr ra + sb cur_flags, %lo(GL_STATE_PALETTE_DIRTY)(flag_offset) + +1: + lw cur_flags, %lo(GL_STATE_FLAGS) + # mark final matrix as dirty if modelview or projection matrix was changed + blt mtx_index, (GL_TEXTURE & 0xF), 1f + li flag, FLAG_FINAL_MTX_DIRTY + move flag, zero +1: + or cur_flags, flag + jr ra + sw cur_flags, %lo(GL_STATE_FLAGS) + #undef mtx_index + #undef flag_offset + #undef flag_shift + #undef palette_index + #undef flag + #undef cur_flags + .endfunc + +GL_GetMatrixIndex: + lbu t0, %lo(GL_STATE_MATRIX_MODE) + 1 + andi t1, t0, GL_MATRIX_PALETTE_ARB + bnez t1, JrRa + li t1, PALETTE_MATRIX_INDEX + jr ra + move t1, t0 + +GLCmd_MatrixPush: + j GL_MatrixPushPop + li t2, DMA_OUT + +GLCmd_MatrixPop: + li t2, DMA_IN + # fallthrough! +GL_MatrixPushPop: + #define post_incr t0 + #define mtx_index t1 + #define stack_ptr t3 + + # Get matrix pointer for the current matrix stack + jal GL_GetMatrixIndex + nop + sll stack_ptr, mtx_index, 2 + lw s0, %lo(GL_MATRIX_POINTERS)(stack_ptr) + + # Get location in DMEM for the current matrix stack + sll s4, mtx_index, 6 + addi s4, %lo(GL_MATRICES) + + bltz t2, 1f + # If pushing the stack, post-increment the stack pointer after the DMA + li post_incr, MATRIX_SIZE + + # If popping the stack, pre-decrement the stack pointer before the DMA + addi s0, -MATRIX_SIZE + # Also mark the matrix as dirty + jal GL_MatrixMarkDirty + move post_incr, zero + +1: + add s1, s0, post_incr + + # Save new stack pointer + sw s1, %lo(GL_MATRIX_POINTERS)(stack_ptr) + + # Do DMA + li t0, DMA_SIZE(MATRIX_SIZE, 1) + jal_and_j DMAExec, RSPQ_Loop + +GLCmd_MatrixLoad: + #define multiply t0 + #define mtx_index t1 + #define lhs s5 + #define rhs s6 + #define dst s7 + + #define vrhs01_i $v02 + #define vrhs01_f $v03 + #define vrhs23_i $v04 + #define vrhs23_f $v05 + + move ra2, ra + jal GL_GetMatrixIndex + addi rhs, rspq_dmem_buf_ptr, %lo(RSPQ_DMEM_BUFFER) - 64 + andi multiply, a0, 1 + sll dst, mtx_index, 6 + addiu dst, %lo(GL_MATRICES) + +GL_MatrixCopyMultiplyImpl: + # Load the right hand side matrix from command parameters (misaligned) + lqv vrhs01_i, 0x00,rhs + lrv vrhs01_i, 0x10,rhs + lqv vrhs23_i, 0x10,rhs + lrv vrhs23_i, 0x20,rhs + lqv vrhs01_f, 0x20,rhs + lrv vrhs01_f, 0x30,rhs + lqv vrhs23_f, 0x30,rhs + lrv vrhs23_f, 0x40,rhs + + beqz multiply, GL_MtxMultSave # skip multiplication if only loading the matrix + li ra, 1f + + jal GL_MtxMultWithRhs + move lhs, dst + +1: + j GL_MatrixMarkDirty + move ra, ra2 + +GLCmd_MatrixCopy: + ############################################################# + # GLCmd_MatrixCopy + # + # Copies from 1 modelview stack to another + # + # ARGS: + # a0: Bit 31..24: Command id + # Bit 7..6: Source Matrix + ############################################################# + move ra2, ra + jal GL_GetMatrixIndex + addiu rhs, a0, %lo(GL_MATRICES) + sll dst, mtx_index, 6 + addiu dst, %lo(GL_MATRICES) + j GL_MatrixCopyMultiplyImpl + li multiply, 0 + + +GL_UpdateScissor: + lhu a1, %lo(GL_STATE_FB_SIZE) + 0x0 + lhu t7, %lo(GL_STATE_FB_SIZE) + 0x2 + lw a0, %lo(GL_STATE_FLAGS) + andi a0, FLAG_SCISSOR_TEST + beqz a0, 1f + move t2, zero + + lhu t2, %lo(GL_STATE_SCISSOR_RECT) + 0x2 + lhu a0, %lo(GL_STATE_SCISSOR_RECT) + 0x0 + subu t7, t2 + lhu a1, %lo(GL_STATE_SCISSOR_RECT) + 0x4 + lhu t2, %lo(GL_STATE_SCISSOR_RECT) + 0x6 + addu a1, a0 + subu t2, t7, t2 + +1: + sll t2, 2 + sll a0, 14 + or a0, t2 + lui t2, 0xED00 + or a0, t2 + sll t7, 2 + sll a1, 14 + or a1, t7 + jal_and_j RDPQ_WriteSetScissor, RDPQ_Finalize + +GL_UpdateTextureCompleteness: + #define result t7 + #define levels t8 + #define width t1 + #define height t2 + #define loop_max t3 + #define one t6 + #define loop_var t0 + #define image s1 + #define format s2 + move result, zero # complete = false + move levels, zero + + # If either width or height is zero, the texture is incomplete + lhu width, (%lo(GL_BOUND_TEXTURES) + IMAGE_WIDTH_OFFSET)(a0) + beqz width, gl_tex_incomplete + lhu height, (%lo(GL_BOUND_TEXTURES) + IMAGE_HEIGHT_OFFSET)(a0) + beqz height, gl_tex_incomplete + lhu t3, (%lo(GL_BOUND_TEXTURES) + TEXTURE_MIN_FILTER_OFFSET)(a0) + + # Otherwise, if mipmaps are disabled, the texture is complete and has a single level + andi t3, TEXTURE_MIPMAP_MASK + beqz t3, gl_tex_mipmaps_disabled + li one, 1 + + # Init the loop + addiu image, a0, %lo(GL_BOUND_TEXTURES) + lhu format, IMAGE_INTERNAL_FORMAT_OFFSET(image) + move loop_var, zero + li loop_max, MAX_TEXTURE_LEVELS - 1 + +gl_tex_completeness_loop: + # Check that image size and format matches the expected values + lhu t4, IMAGE_WIDTH_OFFSET(image) + bne t4, width, gl_tex_incomplete + lhu t5, IMAGE_HEIGHT_OFFSET(image) + bne t5, height, gl_tex_incomplete + lhu t4, IMAGE_INTERNAL_FORMAT_OFFSET(image) + bne t4, format, gl_tex_incomplete + or t5, width, height + + # If width and height are both 1 we have reached the end of the mipmap chain + beq t5, one, gl_tex_complete + nop + + # Unless width is one, divide it by two + beq width, one, 1f + nop + srl width, 1 +1: + # Unless height is one, divide it by two + beq height, one, 1f + nop + srl height, 1 +1: + # Check if we have reached the maximum number of loops + beq loop_var, loop_max, gl_tex_incomplete + addiu image, TEXTURE_IMAGE_SIZE + + # Loop! + b gl_tex_completeness_loop + addiu loop_var, 1 + +gl_tex_complete: + move levels, loop_var # levels = i (Number of mipmaps minus one) +gl_tex_mipmaps_disabled: + li result, TEX_FLAG_COMPLETE # complete = true +gl_tex_incomplete: + # Save the result + lw t0, (%lo(GL_BOUND_TEXTURES) + TEXTURE_FLAGS_OFFSET)(a0) + sh levels, (%lo(GL_BOUND_TEXTURES) + TEXTURE_LEVELS_COUNT_OFFSET)(a0) + and t0, ~(TEX_FLAG_COMPLETE) + or t0, result + jr ra + sw t0, (%lo(GL_BOUND_TEXTURES) + TEXTURE_FLAGS_OFFSET)(a0) + #undef result + #undef width + #undef height + #undef loop_max + #undef one + #undef loop_var + #undef image + #undef format + #undef ra3 + +GL_UpdateTextureObjects: + lw s0, %lo(GL_STATE_TEXTURE_IDS+0) + beqz s0, 1f + li s4, %lo(GL_BOUND_TEXTURE_1D) + jal DMAOut + li t0, DMA_SIZE(TEXTURE_OBJECT_SIZE, 1) +1: + lw s0, %lo(GL_STATE_TEXTURE_IDS+4) + beqz s0, JrRa + li s4, %lo(GL_BOUND_TEXTURE_2D) + li t0, DMA_SIZE(TEXTURE_OBJECT_SIZE, 1) + jal_and_j DMAOut, RSPQ_Loop + + .func GLCmd_SetPaletteIdx +GLCmd_SetPaletteIdx: + lw s0, %lo(GL_STATE_PALETTE_INDEX) + lw t3, %lo(GL_PALETTE_PTR) + and a0, 0xFFFFFF + beq a0, s0, JrRa + sw a0, %lo(GL_STATE_PALETTE_INDEX) + + add s0, t3 + li s4, %lo(GL_MATRIX_PALETTE) + jal DMAOutAsync + li t0, DMA_SIZE(MATRIX_SIZE, 1) + + add s0, a0, t3 + jal_and_j DMAIn, RSPQ_Loop + .endfunc + + .func GLCmd_PreInitPipe +GLCmd_PreInitPipe: + sh a0, %lo(GL_STATE_PRIM_TYPE) + + #define state_flags k1 + #define state_flags2 k0 + + lw state_flags, %lo(GL_STATE_FLAGS) + lw state_flags2, %lo(GL_STATE_FLAGS2) + + # Update matrix if required + + andi t0, state_flags, FLAG_MATRIX_PALETTE + beqz t0, 1f + li v1, 1 + + #define mtx_dirty t1 + #define mtx_offset t3 + #define palette_size t4 + #define palette_index t5 + #define dirty_flags t6 + #define cur_flag t7 + #define palette_dram s1 + #define mvp_dram s2 + #define flag_offset s3 + #define mtx_lhs s5 + #define mtx_rhs s6 + #define mtx_dst s7 + + lw palette_dram, %lo(GL_PALETTE_PTR) + lw palette_index, %lo(GL_STATE_PALETTE_INDEX) + + # Write back current palette matrix to the palette storage + li s4, %lo(GL_MATRIX_PALETTE) + li t0, DMA_SIZE(MATRIX_SIZE, 1) + jal DMAOutAsync + add s0, palette_dram, palette_index + + lw mvp_dram, %lo(GL_MVP_PTR) + li mtx_lhs, %lo(GL_MATRIX_PROJECTION) + li mtx_rhs, %lo(GL_MATRIX_PALETTE) + li mtx_dst, %lo(GL_MATRIX_MVP) + li palette_index, 0 + li palette_size, MATRIX_PALETTE_SIZE + + # TODO: Load projection matrix into vector regs only once +gl_matrix_palette_loop: + beq palette_index, palette_size, 2f + srl flag_offset, palette_index, 3 + + lbu dirty_flags, %lo(GL_STATE_PALETTE_DIRTY)(flag_offset) + and cur_flag, palette_index, 0x7 + sllv cur_flag, v1, cur_flag + and mtx_dirty, dirty_flags, cur_flag + sll mtx_offset, palette_index, 6 + beqz mtx_dirty, gl_matrix_palette_loop + addi palette_index, 1 + + xor dirty_flags, mtx_dirty + sb dirty_flags, %lo(GL_STATE_PALETTE_DIRTY)(flag_offset) + + move s4, mtx_rhs + jal DMAIn + add s0, palette_dram, mtx_offset + + jal GL_MtxMult + move s4, mtx_dst + + add s0, mvp_dram, mtx_offset + jal_and_j DMAOutAsync, gl_matrix_palette_loop + +1: + and mtx_dirty, state_flags, FLAG_FINAL_MTX_DIRTY + beqz mtx_dirty, 2f + li mtx_lhs, %lo(GL_MATRIX_PROJECTION) + li mtx_rhs, %lo(GL_MATRIX_MODELVIEW) + jal GL_MtxMult + li mtx_dst, %lo(GL_MATRIX_MVP) + and state_flags, ~FLAG_FINAL_MTX_DIRTY + + #undef mtx_dirty + #undef mtx_offset + #undef palette_size + #undef palette_index + #undef dirty_flags + #undef cur_flag + #undef palette_dram + #undef mvp_dram + #undef flag_offset + #undef mtx_lhs + #undef mtx_rhs + #undef mtx_dst + +2: + + # Determine Culling mode + + #define cull_mode t1 + #define front_face t2 + #define cull_enabled t3 + #define tri_cull t4 + + andi cull_enabled, state_flags, FLAG_CULL_FACE + beqz cull_enabled, 1f + # Any non-negative value other than 0 or 1 signifies that no faces should be culled + li tri_cull, 2 + + lhu cull_mode, %lo(GL_STATE_CULL_MODE) + lhu front_face, %lo(GL_STATE_FRONT_FACE) + + # Set TRI_CULL to a negative number to cull all faces + beq cull_mode, GL_FRONT_AND_BACK, 1f + li tri_cull, -1 + + # tri_cull = (cull_mode == GL_BACK) ^ (front_face == GL_CW) + xori cull_mode, GL_FRONT + sltu tri_cull, zero, cull_mode + xori front_face, GL_CCW + sltu front_face, zero, front_face + xor tri_cull, front_face + +1: + sb tri_cull, %lo(GL_TRI_CULL) + + #undef cull_mode + #undef front_face + #undef cull_enabled + #undef tri_cull + + #define modes0 t8 + #define modes1 t9 + #define is_points v0 + #define active_tex s7 + + # Get Active texture + + andi t1, state_flags, FLAG_TEXTURE_2D + bnez t1, 1f + li active_tex, %lo(GL_BOUND_TEXTURE_2D) + andi t1, state_flags, FLAG_TEXTURE_1D + bnez t1, 1f + li active_tex, %lo(GL_BOUND_TEXTURE_1D) + move active_tex, zero +1: + #define tri_cmd t4 + srl tri_cmd, state_flags, TRICMD_ATTR_SHIFT + andi tri_cmd, TRICMD_ATTR_MASK + ori tri_cmd, 0xCC00 + sh tri_cmd, %lo(GL_TRI_CMD) + + #undef tri_cmd + + # Check if eye space transformation is needed in T&L + and t1, state_flags, FLAG_LIGHTING | FLAG_FOG | FLAG_TEXTURE_ACTIVE + sltu t1, zero, t1 + sll t1, NEED_EYE_SPACE_SHIFT + and t2, state_flags, ~FLAG_NEED_EYE_SPACE + or state_flags, t2, t1 + + #define vmodes $v01 + #define vsphere $v02 + #define v___ $v29 + + # Check which tex gen modes are active + srl t0, state_flags, TEX_GEN_S_SHIFT + andi t0, 0xF + ctc2 t0, COP2_CTRL_VCC + li s1, %lo(GL_TEX_GEN) + lqv vmodes, TEX_GEN_MODE_OFFSET, s1 + vmrg vmodes, vzero + vand vsphere, vmodes, K2 + veq v___, vsphere, vzero + vmrg vmodes, vzero + vor vsphere, vsphere.q1 + vor vsphere, vsphere.e2 + vor vmodes, vmodes.q1 + vor vmodes, vmodes.e2 + mfc2 t0, vsphere.e0 + mfc2 t1, vmodes.e0 + sltu t0, zero, t0 + sltu t1, zero, t1 + sll t0, TEX_GEN_SPHERICAL_FLAG_SHIFT + sll t1, TEX_GEN_LINEAR_FLAG_SHIFT + or t0, t1 + and t2, state_flags, ~(FLAG_TEX_GEN_LINEAR | FLAG_TEX_GEN_SPHERICAL) + or state_flags, t2, t0 + + #undef vmodes + #undef vsphere + #undef v___ + + # Multisampling + and t0, state_flags2, FLAG2_REDUCED_ALIASING + or modes0, t0, (SOM_TF0_RGB | SOM_TF1_RGB) >> 32 + and modes1, state_flags2, FLAG2_MULTISAMPLE + + # Alpha test + andi t0, state_flags, FLAG_ALPHA_TEST + beqz t0, 1f + lhu t1, %lo(GL_STATE_ALPHA_FUNC) + xori t1, GL_GREATER + sltu t0, t1, 1 + or modes1, t0 +1: + + # Depth mask + and t0, state_flags, FLAG_DEPTH_TEST | FLAG_DEPTH_MASK + xor t0, FLAG_DEPTH_TEST | FLAG_DEPTH_MASK + sltu t0, 1 + sll t0, SOM_Z_WRITE_SHIFT + or modes1, t0 + + #define depth_func t3 + + # Depth test + and t0, state_flags, FLAG_DEPTH_TEST + beqz t0, 1f + lhu depth_func, %lo(GL_STATE_DEPTH_FUNC) + xori t1, depth_func, GL_ALWAYS + sltu t1, zero, t1 + sll t0, t1, SOM_Z_COMPARE_SHIFT + or modes1, t0 +1: + + # Z mode + xori t1, depth_func, GL_EQUAL + beqz t1, 1f + li t0, SOM_ZMODE_DECAL + xori t1, depth_func, GL_LESS_INTERPENETRATING_N64 + beqz t1, 1f + li t0, SOM_ZMODE_INTERPENETRATING + andi t0, state_flags, FLAG_BLEND + # t0 = (state_flags & FLAG_BLEND) ? SOM_ZMODE_TRANSPARENT : SOM_ZMODE_OPAQUE + sll t0, ZMODE_BLEND_FLAG_SHIFT +1: + or modes1, t0 + + #undef depth_func + + # Dither + andi t1, state_flags, FLAG_DITHER + beqz t1, 1f + li t0, (SOM_RGBDITHER_NONE | SOM_ALPHADITHER_NONE) >> 32 + lw t0, %lo(GL_STATE_DITHER_MODE) +1: + or modes0, t0 + + # Points + lhu t0, %lo(GL_STATE_PRIM_TYPE) + beqz t0, 1f # prim_type == GL_POINTS + li is_points, 1 + + addi t0, -1 + sltu t0, (GL_TRIANGLES - 1) + bne t0, zero, 1f # prim_type == GL_LINES || prim_type == GL_LINE_LOOP || prim_type == GL_LINE_STRIP + move is_points, zero + + lhu t0, %lo(GL_STATE_POLYGON_MODE) + xori t0, GL_POINT + sltu is_points, t0, 1 # polygon_mode == GL_POINT + +1: + bnez is_points, 1f + sll t3, is_points, SOM_ZSOURCE_SHIFT + or modes0, SOM_TEXTURE_PERSP >> 32 +1: + or modes1, t3 + + # * Skip construction of CC/CB if FLAG2_USE_RDPQ_MATERIAL is active + andi t1, state_flags2, FLAG2_USE_RDPQ_MATERIAL + bnez t1, gl_skipcombinerblender + + # Blend cycle + andi t0, state_flags, FLAG_BLEND + beqz t0, 1f + move t1, zero + lw t1, %lo(GL_STATE_BLEND_CYCLE) +1: + sw t1, %lo(RDPQ_MODE_BLENDER_STEPS) + 0x4 + + # Fog cycle + andi t0, state_flags, FLAG_FOG + beqz t0, 1f + sltu t2, zero, t0 + li t0, RDPQ_FOG_STANDARD | SOM_BLENDING +1: + sw t0, %lo(RDPQ_MODE_BLENDER_STEPS) + 0x0 + or modes0, t2 + + # Combiner + move t5, is_points + lw t3, %lo(GL_STATE_TEX_ENV_MODE) + + li t4, GL_REPLACE + bne t3, t4, 1f + nop + ori t5, COMBINER_FLAG_REPLACE +1: + srl t0, state_flags, TEX_ACTIVE_COMBINER_SHIFT + andi t0, COMBINER_FLAG_TEXTURE + or t5, t0 + + sll t5, 3 + lw t0, %lo(COMBINER_TABLE) + 0x0(t5) + lw t1, %lo(COMBINER_TABLE) + 0x4(t5) + lw t2, %lo(COMBINER_MIPMAPMASK_TABLE) + 0x0(t5) + lw t3, %lo(COMBINER_MIPMAPMASK_TABLE) + 0x4(t5) + + # TODO: The following is sort of equivalent to RDPQCmd_ResetMode. Maybe make that callable from ucode? + + sw t0, %lo(RDPQ_COMBINER) + 0x0 + sw t1, %lo(RDPQ_COMBINER) + 0x4 + sw t2, %lo(RDPQ_COMBINER_MIPMAPMASK) + 0x0 + sw t3, %lo(RDPQ_COMBINER_MIPMAPMASK) + 0x4 + +gl_skipcombinerblender: + + # * Skip specific GL texture flags if FLAG2_USE_RDPQ_TEXTURING is active and instead apply RDPQ ones + + andi t1, state_flags2, FLAG2_USE_RDPQ_TEXTURING + bnez t1, gl_mergemask + nop + + # Texture + beqz active_tex, 2f + lw t4, TEXTURE_FLAGS_OFFSET(active_tex) + + andi t5, t4, TEX_FLAG_COMPLETE + beqz t5, 2f + lhu t3, TEXTURE_MIN_FILTER_OFFSET(active_tex) + lhu t2, TEXTURE_MAG_FILTER_OFFSET(active_tex) + + andi t5, t3, TEXTURE_MIPMAP_MASK + or t2, t3 + andi t2, TEXTURE_BILINEAR_MASK + + # If bilinear sampling is active, texture coords need to be offset by half a texel, + # which is 0x10 in s10.5 + andi t7, state_flags2, FLAG2_TEX_FLIP_T + lh t6, %lo(GL_STATE_TEX_SIZE) + 2 + sll t1, t2, TEX_BILINEAR_OFFSET_SHIFT + neg t1, t1 + beqz t7, 1f + sh t1, %lo(GL_STATE_TEX_OFFSET) + 0 + sra t6, 1 + sub t1, t6 +1: + sh t1, %lo(GL_STATE_TEX_OFFSET) + 2 + + beqz t5, 1f + sll t2, TEX_BILINEAR_SHIFT + or t2, SOM_TEXTURE_LOD >> 32 + + andi t5, t4, TEX_FLAG_DETAIL + sll t5, TEX_DETAIL_SHIFT + or t2, t5 + +1: + andi t3, TEXTURE_INTERPOLATE_MASK + beqz t3, 1f + nop # :( + ori t2, SOMX_LOD_INTERPOLATE >> 32 + +1: + # TODO: Combine into a single field? + lbu t0, TEXTURE_LEVELS_COUNT_OFFSET(active_tex) + lbu t1, TEXTURE_TLUT_MODE_OFFSET(active_tex) + + sll t0, SOMX_NUMLODS_SHIFT - 32 + sll t1, SOM_TLUT_SHIFT - 32 + + or t0, t1 + or t7, t0, t2 + or modes0, t7 +2: + + j 1f + nop # :( +gl_mergemask: + # If we have FLAG2_USE_RDPQ_TEXTURING active, we need to use some of the RDPQ params instead of GL's + + lw t1, %lo(RDPQ_OTHER_MODES) + 0x0 + and t1, RDPQ_TEXTURING_MASK + and t2, modes0, ~(RDPQ_TEXTURING_MASK) + or modes0, t1, t2 + + +1: + + lw t0, %lo(RDPQ_OTHER_MODES) + 0x0 + + sw modes0, %lo(RDPQ_OTHER_MODES) + 0x0 + sw modes1, %lo(RDPQ_OTHER_MODES) + 0x4 + + # If cycle mode was copy or fill, update scissor rect + sll t0, ((64-2)-SOM_CYCLE_SHIFT) + bgez t0, 1f + lw a0, %lo(RDPQ_SCISSOR_RECT) + 0x0 + jal RDPQ_WriteSetScissor + lw a1, %lo(RDPQ_SCISSOR_RECT) + 0x4 + +1: + + j RDPQ_UpdateRenderMode + sw state_flags, %lo(GL_STATE_FLAGS) + + #undef modes0 + #undef modes1 + #undef is_points + #undef active_tex + #undef state_flags + .endfunc + + .func GLCmd_PreInitPipeTex +GLCmd_PreInitPipeTex: + #define tex_id s0 + #define uploaded_tex s1 + #define active_tex s7 + #define active_tex_id s6 + #define tex_flags t7 + #define state_flags k1 + #define state_flags2 k0 + + # Skip processing textutre state if we are using rdpq API for texturing + # (FLAG2_USE_RDPQ_TEXTURING) + lw state_flags2, %lo(GL_STATE_FLAGS2) + andi t1, state_flags2, FLAG2_USE_RDPQ_TEXTURING + beqz t1, 2f + lw state_flags, %lo(GL_STATE_FLAGS) + + or state_flags, FLAG_TEXTURE_ACTIVE + sw state_flags, %lo(GL_STATE_FLAGS) + + # If bilinear sampling is active, texture coords need to be offset by half a texel, + # which is 0x10 in s10.5 + lw t2, %lo(RDPQ_OTHER_MODES) + 0x4 + and t2, (SOM_SAMPLE_BILINEAR >> 32) + sll t2, t2, (SOM_SAMPLE_SHIFT - 32 + 1) + + andi t3, state_flags2, FLAG2_TEX_FLIP_T + lh t6, %lo(GL_STATE_TEX_SIZE) + 2 + sll t1, t2, TEX_BILINEAR_OFFSET_SHIFT + neg t1, t1 + beqz t7, 1f + sh t1, %lo(GL_STATE_TEX_OFFSET) + 0 + sra t6, 1 + sub t1, t6 + +1: + sh t1, %lo(GL_STATE_TEX_OFFSET) + 2 + j RSPQ_Loop + +2: + # Get pointer to active texture state in DMEM + li active_tex_id, %lo(GL_STATE_TEXTURE_IDS) + 0x4 + andi t1, state_flags, FLAG_TEXTURE_2D + bnez t1, 1f + li active_tex, %lo(GL_BOUND_TEXTURE_2D) + andi t1, state_flags, FLAG_TEXTURE_1D + li active_tex_id, %lo(GL_STATE_TEXTURE_IDS) + 0x0 + bnez t1, 1f + li active_tex, %lo(GL_BOUND_TEXTURE_1D) + j gl_set_texture_not_active +1: + lw uploaded_tex, %lo(GL_STATE_UPLOADED_TEX) + + # CHeck if the texture is complete (that is, all mipmaps have been loaded) + # otherwise, we consider it not complete and avoid displaying it + lw tex_flags, TEXTURE_FLAGS_OFFSET(active_tex) + andi t0, tex_flags, TEX_FLAG_COMPLETE + beqz t0, gl_set_texture_not_active + + # Copy width/height of the active texture into the state + lh t0, IMAGE_WIDTH_OFFSET(active_tex) + lh t1, IMAGE_HEIGHT_OFFSET(active_tex) + sll t0, TEX_COORD_SHIFT + sll t1, TEX_COORD_SHIFT + andi t2, state_flags2, FLAG2_TEX_FLIP_T + beqz t2, 1f + sh t0, %lo(GL_STATE_TEX_SIZE) + 0 + neg t1, t1 +1: + sh t1, %lo(GL_STATE_TEX_SIZE) + 2 + + # Make sure FLAG_TEXTURE_ACTIVE is set + or state_flags, FLAG_TEXTURE_ACTIVE + sw state_flags, %lo(GL_STATE_FLAGS) + + # Load ID of active texture (that is, RDRAM pointer) + lw tex_id, (active_tex_id) + + # Proceed with the upload if: + # * the requested texture ID is different from the one currently uploaded to TMEM + # * the dirty flag is set + bne tex_id, uploaded_tex, 1f + andi t0, tex_flags, TEX_FLAG_UPLOAD_DIRTY + beqz t0, RSPQ_Loop +# nop + +1: + # OK we are going to do the upload. Update the state: clear dirty flag and + # set the current texture ID. + li t0, ~TEX_FLAG_UPLOAD_DIRTY + and tex_flags, t0 + sw tex_flags, TEXTURE_FLAGS_OFFSET(active_tex) + sw tex_id, %lo(GL_STATE_UPLOADED_TEX) + + # Do the upload: we tail call to the RSPQ block that is within + # the texture state (at offset TEXTURE_LEVELS_BLOCK_OFFSET). This + # block was recorded by glTexImageN64 and contains the commands + # necessary to upload the texture(s) to TMEM. + addi a0, tex_id, TEXTURE_LEVELS_BLOCK_OFFSET + j RSPQCmd_Call + li a1, 1<<2 # nesting level 1 + +gl_set_texture_not_active: + and state_flags, ~FLAG_TEXTURE_ACTIVE + j RSPQ_Loop + sw state_flags, %lo(GL_STATE_FLAGS) + + #undef active_tex + #undef uploaded_tex + #undef tex_id + #undef tex_flags + #undef state_flags + #undef state_flags2 + .endfunc + +/* + ############################################################# + # Vec_Convert + # + # Converts a list of floats to fixed point (in-place) + # + # ARGS: + # s4: Address in DMEM of float list + # t0: Length + ############################################################# +Vec_Convert: + #define mant_i $v01 + #define mant_f $v02 + + #define exp $v03 + #define nexp $v04 + #define bit $v05 + #define sign $v06 + + #define step_i $v07 + #define step_f $v08 + #define fact_i $v09 + #define fact_f $v10 + + #define vconst $v11 + #define vhalf $v12 + #define vtmp $v13 + + li t1, %lo(CONVERT_CONST) + lqv vconst,0, 0,t1 + vor vhalf, vzero, vconst,e(2) + add s1, s4, t0 + +convert_loop: + lsv mant_i,0x0, 0x00,s4 + lsv mant_i,0x2, 0x04,s4 + lsv mant_i,0x4, 0x08,s4 + lsv mant_i,0x6, 0x0C,s4 + lsv mant_i,0x8, 0x10,s4 + lsv mant_i,0xA, 0x14,s4 + lsv mant_i,0xC, 0x18,s4 + lsv mant_i,0xE, 0x1C,s4 + lsv mant_f,0x0, 0x02,s4 + lsv mant_f,0x2, 0x06,s4 + lsv mant_f,0x4, 0x0A,s4 + lsv mant_f,0x6, 0x0E,s4 + lsv mant_f,0x8, 0x12,s4 + lsv mant_f,0xA, 0x16,s4 + lsv mant_f,0xC, 0x1A,s4 + lsv mant_f,0xE, 0x1E,s4 + + vmudm exp, mant_i, vconst,e(3) # exp = ((mant_i >> 7) & 0xFF) + vand exp, exp, vconst,e(4) + + vsub sign, vzero, vconst,e(0) # sign = mant_i < 0 ? -1 : 1 + vlt vtmp, mant_i, vzero + vmrg sign, sign, vconst,e(0) + + veq vtmp, exp, vzero # sign = exp == 0 ? 0 : sign + vmrg sign, vzero, sign + + vsub exp, exp, vconst,e(5) # exp -= 134 + + vsub nexp, vzero, exp # nexp = -exp + vlt vtmp, exp, vzero # c = exp < 0 + vmrg exp, nexp, exp # exp = c ? nexp : exp + vmrg step_i, vzero, vconst,e(1) # step = c ? 0.5 : 2 + vmrg step_f, vhalf, vzero + + vand mant_i, mant_i, vconst,e(6) # mant = (mant & 0x7F) | (1<<7) + vor mant_i, mant_i, vconst,e(7) + + li t0, 5 + +flt_2_fxd_loop: + vand bit, exp, vconst,e(0) # bit = exp & 1 + vlt vtmp, vzero, bit # c = 0 < bit + vmrg fact_i, step_i, vconst,e(0) # fact = c ? step : 1 + vmrg fact_f, step_f, vzero + + vmudl vtmp, mant_f, fact_f # mant *= fact + vmadm vtmp, mant_i, fact_f + vmadn mant_f, mant_f, fact_i + vmadh mant_i, mant_i, fact_i + + vmudl vtmp, step_f, step_f # step *= step + vmadm vtmp, step_i, step_f + vmadn step_f, step_f, step_i; addi t0, -1 + vmadh step_i, step_i, step_i; bgtz t0, flt_2_fxd_loop + + vmudm exp, exp, vhalf # exp = exp >> 1 + + vmudn vtmp, mant_f, sign # mant *= sign + vmadh mant_i, mant_i, sign + vmadn vtmp, mant_f, vzero; addi s4, 0x20 + + sqv mant_i,0, -0x20,s4; blt s4, s1, convert_loop + sqv mant_f,0, -0x10,s4 + + jr ra + nop +*/ +/* +GLCmd_SetLightDir: + #define v___ $v01 + + #define vpos $v02 + + #define vmtx0_i $v03 + #define vmtx0_f $v04 + #define vmtx1_i $v05 + #define vmtx1_f $v06 + #define vmtx2_i $v07 + #define vmtx2_f $v08 + + addi s0, rspq_dmem_buf_ptr, %lo(RSPQ_DMEM_BUFFER) - 4 + li s1, %lo(GL_MATRIX_MODELVIEW) + ldv vpos, 0x00,s0 + ldv vmtx0_i, 0x00,s1 + ldv vmtx1_i, 0x08,s1 + ldv vmtx2_i, 0x10,s1 + ldv vmtx0_f, 0x20,s1 + ldv vmtx1_f, 0x28,s1 + ldv vmtx2_f, 0x30,s1 + + # TODO: verify this + + vmudn v___, vmtx0_f, vpos.e0 + vmadh v___, vmtx0_i, vpos.e0 + vmadn v___, vmtx1_f, vpos.e1 + vmadh v___, vmtx1_i, vpos.e1 + vmadn v___, vmtx2_f, vpos.e2 + vmadh vpos, vmtx2_i, vpos.e2 + + li s0, %lo(RDPQ_CMD_STAGING) + spv vpos, 0,s0 + lbu t0, %lo(GL_STATE) + LIGHT_SPOT_EXPONENT_OFFSET(a0) + lw t1, %lo(RDPQ_CMD_STAGING) + sw t1, %lo(GL_STATE) + LIGHT_DIRECTION_OFFSET(a0) + jr ra + sb t0, %lo(GL_STATE) + LIGHT_SPOT_EXPONENT_OFFSET(a0) + + #undef v___ + + #undef vpos + + #undef vmtx0_i + #undef vmtx0_f + #undef vmtx1_i + #undef vmtx1_f + #undef vmtx2_i + #undef vmtx2_f + #undef vmtx3_i + #undef vmtx3_f +*/ + +#include "rsp_gl_common.inc" +#include diff --git a/src/GL/rsp_gl_clipping.inc b/src/GL/rsp_gl_clipping.inc new file mode 100644 index 000000000..1c929ab09 --- /dev/null +++ b/src/GL/rsp_gl_clipping.inc @@ -0,0 +1,379 @@ +############################################################ +# rsp_gl_clipping.inc +# +# Authors: Giovanni Bajo +############################################################ + + .section .data.gl_clipping + + .align 4 +CLIP_PLANES: + .half 1, 0, 0, GUARD_BAND_FACTOR + .half 0, 1, 0, GUARD_BAND_FACTOR + .half 0, 0, 1, 1 + .half 1, 0, 0, -GUARD_BAND_FACTOR + .half 0, 1, 0, -GUARD_BAND_FACTOR + .half 0, 0, 1, -1 + + .section .bss.gl_clipping + +CLIP_CACHE: .dcb.b SCREEN_VTX_SIZE * CLIPPING_CACHE_SIZE +CLIP_CACHE_END: + +CLIP_LISTS: + CLIP_LIST0: .dcb.w CLIPPING_CACHE_SIZE + CLIP_LIST1: .dcb.w CLIPPING_CACHE_SIZE + + + .section .text.gl_clipping + + ################################################################ + # GL_ClipTriangle + # Clip a triangle against the view-frustum by using the Sutherland-Hodgman algorithm + # https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm + # Args: + # a1-a3 = Vertices + # t5 = OR'd clip flags of the triangle's vertices + # Returns: + # s1 = Pointer to list of output vertices + # s2 = Pointer to end of list + ################################################################ + .func GL_ClipTriangle +GL_ClipTriangle: + #define out_count v1 + #define clip_flags t5 + #define plane_flag t6 + #define in_count t7 + #define in_end t8 + #define in_list s0 + #define out_list s1 + #define plane s2 + #define intersection s3 + #define cur_ptr s4 + #define prev_ptr s5 + #define cur_vtx s6 + #define prev_vtx s7 + #define p0 k0 + #define p1 k1 + #define vtx1 a1 + #define vtx2 a2 + #define vtx3 a3 + + #define vplane $v01 + #define vint_f $v02 + #define vint_i $v03 + #define vdot_i $v04 + #define vdot_f $v05 + #define vdiff_i $v06 + #define vdiff_f $v07 + #define va_i $v08 + #define va_f $v09 + #define vpos_i $v10 + #define vpos_f $v11 + #define vattr0 $v12 + #define vattr1 $v13 + #define voff0 $v14 + #define voff1 $v15 + #define vcache0 $v16 + #define vcache1 $v17 + #define v__ $v29 + + move ra2, ra + + # Init in_list as empty + li in_list, %lo(CLIP_LIST0) + move in_count, zero + + # Put three original vertices in the out_list + # (So after the initial swap they will be in the in_list) + li out_list, %lo(CLIP_LIST1) + sh vtx1, 0(out_list) + sh vtx2, 2(out_list) + sh vtx3, 4(out_list) + li out_count, 3*2 + + li plane, %lo(CLIP_PLANES) + li plane_flag, 1 + + # Load cache offsets + li t0, %lo(CACHE_OFFSETS) + vxor voff1, voff1 + lqv voff0, 0,t0 + lsv voff1, 16,t0 + + # Temporarily use the RDP staging area as a map of which cache slots are used + # Init to zero + li t0, %lo(RDPQ_CMD_STAGING) + sqv vzero, 0,t0 + sqv vzero, 16,t0 + + # Iterate over the 6 clipping planes +gl_clip_plane_loop: + and t0, clip_flags, plane_flag + beqz t0, gl_clip_plane_loop_end + move t1, in_list + + # Swap in and out lists + + # If the out list is empty from the last iteration, + # the triangle has no visible points and we are done + beqz out_count, gl_clip_return + move in_list, out_list + move out_list, t1 + move in_count, out_count + move out_count, zero + + # Iterate over the egdes of the polygon in the input list + # The current edge is between cur_vtx and prev_vtx + move cur_ptr, in_list + add in_end, in_list, in_count + # Init the "previous" vertex to the last in the list for the wrap-around + addi prev_ptr, in_end, -2 + +gl_clip_edge_loop: + #define cur_flag t3 + #define prev_flag t4 + + # Check which side of the plane the two vertices are on + lhu cur_vtx, 0(cur_ptr) + lhu prev_vtx, 0(prev_ptr) + lbu cur_flag, SCREEN_VTX_CLIP_CODE(cur_vtx) + lbu prev_flag, SCREEN_VTX_CLIP_CODE(prev_vtx) + and cur_flag, plane_flag + and prev_flag, plane_flag + + # If they are on opposite sides, there is an intersection + xor t0, cur_flag, prev_flag + beqz t0, gl_clip_no_intersection + move p0, cur_vtx + + # Swap the two points if necessary to make intersection calculation consistent + # This will make sure p0 is always inside and p1 is always outside + bnez prev_flag, gl_clip_no_swap + move p1, prev_vtx + xor p0, p0, p1 + xor p1, p0, p1 + xor p0, p0, p1 + + #undef prev_flag + +gl_clip_no_swap: + # Calculate intersection of the line segment and the plane + + li t0, %lo(RDPQ_CMD_STAGING) + lqv vcache0, 0,t0 + lqv vcache1, 16,t0 + + # Repeat plane coefficients twice + ldv vplane.e0, 0,plane + ldv vplane.e4, 0,plane + + # vpos: x0 y0 z0 w0 x1 y1 z1 w1 + ldv vpos_i.e0, SCREEN_VTX_CS_POSi,p0 + ldv vpos_f.e0, SCREEN_VTX_CS_POSf,p0 + ldv vpos_i.e4, SCREEN_VTX_CS_POSi,p1 + ldv vpos_f.e4, SCREEN_VTX_CS_POSf,p1 + + # vint: x1 y1 z1 w1 + ldv vint_i.e0, SCREEN_VTX_CS_POSi,p1 + ldv vint_f.e0, SCREEN_VTX_CS_POSf,p1 + + # vattr0: r0 g0 b0 a0 s0 t0 + luv vattr0.e0, SCREEN_VTX_RGBA ,p0 + llv vattr0.e4, SCREEN_VTX_S ,p0 + + # vattr1: r1 g1 b1 a1 s1 t1 + luv vattr1.e0, SCREEN_VTX_RGBA ,p1 + llv vattr1.e4, SCREEN_VTX_S ,p1 + + # Find first free slot in clip cache + + # Add the values from the "used slots map" to the cache offsets + # After this, each lane will contain the offset of its corresponding cache slot, + # but only if the slot is not used. If it is used, it will contain some large value. + vaddc vcache0, voff0 + vaddc vcache1, voff1 + + # Look for the smallest value, which will end up in vcache.e0 + # Because used slots are marked as large values, they will never be found. + vlt vcache0, vcache0.q1 + vlt vcache0, vcache0.h2 + vlt vcache0, vcache0.e4 + vlt vcache0, vcache1.e0 + + mfc2 t0, vcache0.e0 + + # Mark slot as used by storing some large value (careful of overflows!) + li t1, 0xFF + sh t1, %lo(RDPQ_CMD_STAGING)-2(t0) + + # t0 is the index multiplied by 2 + # intersection = t0 * 20 = t0 * 16 + t0 * 4 + sll intersection, t0, 4 + sll t1, t0, 2 + add intersection, t1 + + # CAUTION: intersection might point to the same address as either p0 or p1, + # because one of them is the previous point, which could have been marked unused + # in the previous iteration. As long as we don't access p0 or p1 after writing to + # intersection, this is fine. + addi intersection, %lo(CLIP_CACHE) - SCREEN_VTX_SIZE + + # Store the cache offset in unused memory (used later when finding the cache slot to mark as unused) + sb t0, SCREEN_VTX_PADDING(intersection) + + # Compute dot products of both positions with the clip plane + # vdot.e0: d0 = dot(p0, plane) + # vdot.e4: d1 = dot(p1, plane) + vmudn vdot_f, vpos_f, vplane + vmadh vdot_i, vpos_i, vplane + vaddc vdot_f, vdot_f.q1 + vadd vdot_i, vdot_i.q1 + vaddc vdot_f, vdot_f.h2 + vadd vdot_i, vdot_i.h2 + + # d0 - d1 + vsubc vdiff_f, vdot_f, vdot_f.e4 + vsub vdiff_i, vdot_i, vdot_i.e4 + + # 1 / (d0 - d1) + vrcph v__.e0, vdiff_i.e0 + vrcpl va_f.e0, vdiff_f.e0 + vrcph va_i.e0, vzero.e0 + + # a = d0 / (d0 - d1) + vmudl v__, va_f, vdot_f.e0 + vmadm v__, va_i, vdot_f.e0 + vmadn va_f, va_f, vdot_i.e0 + + # Prepare 0x7FFF in va_i.e0 + vsubc va_i, vshift8, K1 + + # a = min(a, 1) + vge v__, va_f, vzero + vmrg va_f, va_f, va_i.e0 + + # Account for right shift introduced by vrcp + vmudn va_f, va_f, K2 + + # p1 - p0 + vsubc vint_f, vpos_f + vsub vint_i, vpos_i + # attr1 - attr0 + vsubc vattr1, vattr0 + + # Result of linear interpolation: + # p0 + a * (p1 - p0) + vmudl v__, vint_f, va_f.e0 + vmadm v__, vint_i, va_f.e0 + vmadn vint_f, vpos_f, K1 + vmadh vint_i, vpos_i, K1 + + # a * (attr1 - attr0) + vmudm vattr1, vattr1, va_f.e0 + + # attr0 + a * (attr1 - attr0) + vaddc vattr0, vattr1 + + # Store results + sdv vint_i.e0, SCREEN_VTX_CS_POSi,intersection + sdv vint_f.e0, SCREEN_VTX_CS_POSf,intersection + suv vattr0.e0, SCREEN_VTX_RGBA ,intersection + jal GL_CalcClipCodes + slv vattr0.e4, SCREEN_VTX_S ,intersection + + # Add intersection to the output list + add t0, out_list, out_count + sh intersection, 0(t0) + addi out_count, 2 + +gl_clip_no_intersection: + # If cur_vtx is inside, add it to the output list + bnez cur_flag, gl_clip_no_current + add t0, out_list, out_count + sh cur_vtx, 0(t0) + b gl_clip_edge_loop_end + addi out_count, 2 + + #undef cur_flag + +gl_clip_no_current: + # Check if the vertex is stored in the clip cache + lbu t0, SCREEN_VTX_PADDING(cur_vtx) + beqz t0, gl_clip_edge_loop_end + # Reset the padding field to zero, so the screen space values won't be recalculated below + sb zero, SCREEN_VTX_PADDING(cur_vtx) + # If so, mark it as unused + sh zero, %lo(RDPQ_CMD_STAGING)-2(t0) + +gl_clip_edge_loop_end: + # Advance to the next edge + addi cur_ptr, 2 + blt cur_ptr, in_end, gl_clip_edge_loop + addi prev_ptr, cur_ptr, -2 + +gl_clip_plane_loop_end: + # Advance to the next clipping plane + sll plane_flag, 1 + blt plane_flag, (1< +############################################################ +#ifndef RSP_GL_COMMON_INC +#define RSP_GL_COMMON_INC + + .section .text.mtxmult + +.func GL_MtxMult +GL_MtxMult: + #define lhs s5 + #define rhs s6 + #define dst s7 + + #define v___ $v01 + + #define vrhs01_i $v02 + #define vrhs01_f $v03 + #define vrhs23_i $v04 + #define vrhs23_f $v05 + + #define vlhs0_i $v06 + #define vlhs0_f $v07 + #define vlhs1_i $v08 + #define vlhs1_f $v09 + #define vlhs2_i $v10 + #define vlhs2_f $v11 + #define vlhs3_i $v12 + #define vlhs3_f $v13 + + lqv vrhs01_i, 0x00,rhs + lqv vrhs23_i, 0x10,rhs + lqv vrhs01_f, 0x20,rhs + lqv vrhs23_f, 0x30,rhs + +GL_MtxMultWithRhs: + ldv vlhs0_i.e0, 0x00,lhs + ldv vlhs0_i.e4, 0x00,lhs + ldv vlhs1_i.e0, 0x08,lhs + ldv vlhs1_i.e4, 0x08,lhs + ldv vlhs2_i.e0, 0x10,lhs + ldv vlhs2_i.e4, 0x10,lhs + ldv vlhs3_i.e0, 0x18,lhs + ldv vlhs3_i.e4, 0x18,lhs + ldv vlhs0_f.e0, 0x20,lhs + ldv vlhs0_f.e4, 0x20,lhs + ldv vlhs1_f.e0, 0x28,lhs + ldv vlhs1_f.e4, 0x28,lhs + ldv vlhs2_f.e0, 0x30,lhs + ldv vlhs2_f.e4, 0x30,lhs + ldv vlhs3_f.e0, 0x38,lhs + ldv vlhs3_f.e4, 0x38,lhs + + vmudl v___, vlhs0_f, vrhs01_f.h0 + vmadm v___, vlhs0_i, vrhs01_f.h0 + vmadn v___, vlhs0_f, vrhs01_i.h0 + vmadh v___, vlhs0_i, vrhs01_i.h0 + + vmadl v___, vlhs1_f, vrhs01_f.h1 + vmadm v___, vlhs1_i, vrhs01_f.h1 + vmadn v___, vlhs1_f, vrhs01_i.h1 + vmadh v___, vlhs1_i, vrhs01_i.h1 + + vmadl v___, vlhs2_f, vrhs01_f.h2 + vmadm v___, vlhs2_i, vrhs01_f.h2 + vmadn v___, vlhs2_f, vrhs01_i.h2 + vmadh v___, vlhs2_i, vrhs01_i.h2 + + vmadl v___, vlhs3_f, vrhs01_f.h3 + vmadm v___, vlhs3_i, vrhs01_f.h3 + vmadn vrhs01_f, vlhs3_f, vrhs01_i.h3 + vmadh vrhs01_i, vlhs3_i, vrhs01_i.h3 + + vmudl v___, vlhs0_f, vrhs23_f.h0 + vmadm v___, vlhs0_i, vrhs23_f.h0 + vmadn v___, vlhs0_f, vrhs23_i.h0 + vmadh v___, vlhs0_i, vrhs23_i.h0 + + vmadl v___, vlhs1_f, vrhs23_f.h1 + vmadm v___, vlhs1_i, vrhs23_f.h1 + vmadn v___, vlhs1_f, vrhs23_i.h1 + vmadh v___, vlhs1_i, vrhs23_i.h1 + + vmadl v___, vlhs2_f, vrhs23_f.h2 + vmadm v___, vlhs2_i, vrhs23_f.h2 + vmadn v___, vlhs2_f, vrhs23_i.h2 + vmadh v___, vlhs2_i, vrhs23_i.h2 + + vmadl v___, vlhs3_f, vrhs23_f.h3 + vmadm v___, vlhs3_i, vrhs23_f.h3 + vmadn vrhs23_f, vlhs3_f, vrhs23_i.h3 + vmadh vrhs23_i, vlhs3_i, vrhs23_i.h3 + +GL_MtxMultSave: + sqv vrhs01_i, 0x00,dst + sqv vrhs23_i, 0x10,dst + sqv vrhs01_f, 0x20,dst + jr ra + sqv vrhs23_f, 0x30,dst + + .endfunc + + .section .text.mtxtransform + + .func GL_MtxTransformDouble +GL_MtxTransformDouble: + #define mtx_ptr s4 + + #define vmtx0_i $v19 // m00 m01 m02 m03 + #define vmtx0_f $v20 + #define vmtx1_i $v21 // m00 m01 m02 m03 + #define vmtx1_f $v22 + #define vmtx2_i $v23 // m00 m01 m02 m03 + #define vmtx2_f $v24 + #define vmtx3_i $v25 // m00 m01 m02 m03 + #define vmtx3_f $v26 + + #define vvec $v27 + #define vresult_i $v28 + #define vresult_f $v29 + + ldv vmtx0_i.e4, 0x00,mtx_ptr + ldv vmtx1_i.e4, 0x08,mtx_ptr + ldv vmtx2_i.e4, 0x10,mtx_ptr + ldv vmtx3_i.e4, 0x18,mtx_ptr + ldv vmtx0_f.e4, 0x20,mtx_ptr + ldv vmtx1_f.e4, 0x28,mtx_ptr + ldv vmtx2_f.e4, 0x30,mtx_ptr + ldv vmtx3_f.e4, 0x38,mtx_ptr + +GL_MtxTransformSingle: + ldv vmtx0_i.e0, 0x00,mtx_ptr + ldv vmtx1_i.e0, 0x08,mtx_ptr + ldv vmtx2_i.e0, 0x10,mtx_ptr + ldv vmtx3_i.e0, 0x18,mtx_ptr + ldv vmtx0_f.e0, 0x20,mtx_ptr + ldv vmtx1_f.e0, 0x28,mtx_ptr + ldv vmtx2_f.e0, 0x30,mtx_ptr + ldv vmtx3_f.e0, 0x38,mtx_ptr + + vmudn v___, vmtx0_f, vvec.h0 + vmadh v___, vmtx0_i, vvec.h0 + vmadn v___, vmtx1_f, vvec.h1 + vmadh v___, vmtx1_i, vvec.h1 + vmadn v___, vmtx2_f, vvec.h2 + vmadh v___, vmtx2_i, vvec.h2 + vmadn v___, vmtx3_f, vvec.h3 + vmadh vresult_i, vmtx3_i, vvec.h3 + jr ra + vmadn vresult_f, vzero, vzero + .endfunc + +#endif diff --git a/src/GL/rsp_gl_lighting.inc b/src/GL/rsp_gl_lighting.inc new file mode 100644 index 000000000..934ff81e9 --- /dev/null +++ b/src/GL/rsp_gl_lighting.inc @@ -0,0 +1,272 @@ +############################################################ +# rsp_gl_lighting.inc +# +# Authors: Dennis Heinze +############################################################ + + .section .text.gl_lighting + + ################################################################ + # GL_VtxLighting + # Perform lighting for a single vertex + # Args: + # $v28 = Eye space position + # $v03 = Eye space normal + # $v04 = Vertex color RGBA + # Returns: + # $v04 = Calculated RGBA + ################################################################ + .func GL_VtxLighting +GL_VtxLighting: + #define v___ $v01 + #define veyepos $v28 + #define veyenormal $v03 + #define vcolor $v04 + #define vmemissive $v05 + #define vmambient $v06 + #define vmdiffuse $v07 + #define vldiffuse $v08 + #define vlout $v09 + #define vlightdir $v10 + #define vlightvec $v11 + #define vsqdist_f $v12 + #define vsqdist_i $v13 + #define vdist_f $v14 + #define vdist_i $v15 + #define vinvdist_f $v16 + #define vinvdist_i $v17 + #define vndl $v18 + #define vattenuation_f $v19 + #define vattenuation_i $v20 + #define venable $v21 + #define vlmambient $v22 + #define vcolortarget $v23 + + #define light_flags t9 + #define light s0 + #define light_end s1 + + #define r e0 + #define g e1 + #define b e2 + #define a e3 + + + li s0, %lo(GL_STATE_LIGHT_AMBIENT) + + vxor vlmambient, vlmambient + ldv vlmambient, 0x00,s0 + + # TODO: Prepare in DMEM? + ldv vmambient.e0, 0x08,s0 + ldv vmambient.e4, 0x08,s0 + ldv vmdiffuse.e0, 0x10,s0 + ldv vmdiffuse.e4, 0x10,s0 + + vxor vmemissive, vmemissive + ldv vmemissive, 0x20,s0 + + vxor vcolortarget, vcolortarget + ldv vcolortarget, 0x28,s0 + + lw light_flags, %lo(GL_STATE_FLAGS) + + # Initialize the third lane of vinvdist to 1.0 + vxor vinvdist_f, vinvdist_f + vmov vinvdist_i.e2, K1 + vmov vinvdist_i.e6, K1 + + andi t0, light_flags, FLAG_COLOR_MATERIAL + beqz t0, gl_light_color_target_disabled + veq v___, vzero, vcolortarget.e0 + vmrg vmambient, vcolor + veq v___, vzero, vcolortarget.e1 + vmrg vmdiffuse, vcolor + veq v___, vzero, vcolortarget.h2 + vmrg vmemissive, vcolor + +gl_light_color_target_disabled: + + li light, %lo(GL_LIGHTS) + addi light_end, light, LIGHT_ATTR_SIZE * LIGHT_COUNT + srl light_flags, LIGHT0_SHIFT + + vmulf vcolor, vlmambient, vmambient + vadd vcolor, vmemissive + +gl_light_loop: + andi t0, light_flags, 3 + beqz t0, gl_light_loop_end + andi t1, t0, 1 + andi t2, t0, 2 + + beqz t1, light0_disabled + mtc2 t1, venable.e0 + lh t1, LIGHT_POSITION_OFFSET+6(light) + +light0_disabled: + beqz t2, light1_disabled + mtc2 t2, venable.e4 + lh t2, LIGHT_POSITION_OFFSET+LIGHT_ATTR_SIZE+6(light) + +light1_disabled: + # If the light is directional, the light vector is simply a direction (pre-normalized on CPU) + lqv vlightdir, LIGHT_POSITION_OFFSET,light + lqv vlout, LIGHT_AMBIENT_OFFSET,light + add t1, t2 + beqz t1, gl_light_directional + lqv vldiffuse, LIGHT_DIFFUSE_OFFSET,light + + # Light is positional: We need to compute light vector, normalize it, and apply attenuation + + # Load attenuation coefficients k0, k1, k2 (constant, linear, quadratic) + # vattenuation: k0 k1 k2 -- -- -- -- -- + lqv vattenuation_i, LIGHT_ATTENUATION_INT_OFFSET,light + lqv vattenuation_f, LIGHT_ATTENUATION_FRAC_OFFSET,light + + # If light is positional, the light vector points from the vertex to the light position + # This is shifted left by 5 because both values are in s10.5 format + vsub vlightvec, vlightdir, veyepos + + # Dot product of light vector with itself + # Product is shifted left by 10 because two s10.5 values were multiplied, + # and then shifted right by 16 because of vsar. + # This means the result is shifted right by 6 + vmudh v___, vlightvec, vlightvec + vsar vsqdist_f, COP2_ACC_MD + vsar vsqdist_i, COP2_ACC_HI + vaddc vdist_f, vsqdist_f, vsqdist_f.h1 + vadd vdist_i, vsqdist_i, vsqdist_i.h1 + vaddc vsqdist_f, vdist_f, vsqdist_f.h2 + vadd vsqdist_i, vdist_i, vsqdist_i.h2 + + # Compute inverse distance (reciprocal square root) + # Result is shifted left by 10: + # - Taking the square root halves the bit-shift, and the reciprocal then inverts it. + # So the original (right) shift of -6 becomes -(-6/2) = 3 + # - vrsq additionally shifts left by 7 + # vinvdist: -- 1/d0 1.0 -- -- 1/d1 1.0 -- + vrsqh v___.e0, vsqdist_i.e0 + vrsql vinvdist_f.e1, vsqdist_f.e0 + vrsqh vinvdist_i.e1, vsqdist_i.e4 + vrsql vinvdist_f.e5, vsqdist_f.e4 + vrsqh vinvdist_i.e5, vzero.e0 + + # Get actual distance by multiplying the inverse with the squared distance: d^-1 * d^2 = d^(2-1) = d + # Because vinvdist.e2 is initialized to 1, the squared distance will be in vdist.e2 + # d is shifted left by 4, d^2 is still shifted right by 6 + # vdist: -- d0 d0^2 -- -- d1 d1^2 -- + vmudl v___, vinvdist_f, vsqdist_f.h0 + vmadm v___, vinvdist_i, vsqdist_f.h0 + vmadn vdist_f, vinvdist_f, vsqdist_i.h0 + vmadh vdist_i, vinvdist_i, vsqdist_i.h0 + + # Multiply with attenuation coefficients + # The coefficients are pre-shifted in such a way that all values end up being shifted right by 1, + # so the final result ends up non-shifted after the reciprocal below. + # - d is shifted left by 4, so k1 is pre-shifted right by 4 on CPU + # - d^2 is shifted right by 6, so k2 is pre-shifted left by 6 on CPU + # vdist: -- k1*d0 k2*d0^2 -- -- k1*d1 k2*d1^2 -- + vmudl v___, vdist_f, vattenuation_f + vmadm v___, vdist_i, vattenuation_f + vmadn vdist_f, vdist_f, vattenuation_i + vmadh vdist_i, vdist_i, vattenuation_i + + # Compute final attenuation factor + # Sum is shifted right by 1 + # k0 + k1*d + k2*d^2 + vaddc vattenuation_f, vdist_f.h1 + vadd vattenuation_i, vdist_i.h1 + vaddc vattenuation_f, vdist_f.h2 + vadd vattenuation_i, vdist_i.h2 + # Final factor is not shifted + # 1 / (k0 + k1*d + k2*d^2) + vrcph v___.e0, vattenuation_i.e0 + vrcpl vattenuation_f.e0, vattenuation_f.e0 + vrcph vattenuation_i.e0, vattenuation_i.e4 + vrcpl vattenuation_f.e4, vattenuation_f.e4 + vrcph vattenuation_i.e4, vzero.e0 + + # Normalize light vector by multiplying the reciprocal distance. + # Light vector is shifted left by 5 and inverse distance is shifted left by 10. + # This means the result is shifted left by 15, which makes the result in vlightdir a signed fraction. + # This happens to match perfectly so we can continue the following calculations without any adjustment. + vmudm v___, vlightvec, vinvdist_f.h1 + vmadh vlightvec, vlightvec, vinvdist_i.h1 + +gl_light_directional: + + vne v___, vzero, vlightdir.h3 + vmrg vlightdir, vlightvec, vlightdir + vmrg vattenuation_i, vattenuation_i, K1 + vmrg vattenuation_f, vattenuation_f, vzero + + # Dot product of light vector with vertex normal + # Both are a signed fraction, so we can just use vmulf + vmulf vndl, veyenormal, vlightdir + vadd v___, vndl, vndl.h1 + vadd vndl, v___, vndl.h2 + vge vndl, vzero + + # Compute diffuse light + vmulf vldiffuse, vndl.h0 + vmulf vldiffuse, vmdiffuse + + # Compute ambient light + vmulf vlout, vmambient + + vadd vlout, vldiffuse + + # If light is positional, apply attenuation factor + vmudm v___, vlout, vattenuation_f.h0 + vmadh vlout, vlout, vattenuation_i.h0 + + # Add final light to output (only those that are enabled) + vne v___, vzero, venable.h0 + vmrg vlout, vzero + vadd vcolor, vlout + +gl_light_loop_end: + addi light, LIGHT_ATTR_SIZE*2 + blt light, light_end, gl_light_loop + srl light_flags, 2 + + vmov v___.e0, vcolor.e4 + vmov v___.e1, vcolor.e5 + vmov v___.e2, vcolor.e6 + vadd vcolor, v___ + + jr ra + vmov vcolor.a, vmdiffuse.a + + #undef v___ + #undef veyepos + #undef veyenormal + #undef vcolor + #undef vmemissive + #undef vmambient + #undef vmdiffuse + #undef vldiffuse + #undef vlout + #undef vlightdir + #undef vlightvec + #undef vsqdist_f + #undef vsqdist_i + #undef vdist_f + #undef vdist_i + #undef vinvdist_f + #undef vinvdist_i + #undef vndl + #undef vattenuation_f + #undef vattenuation_i + #undef venable + #undef vlmambient + #undef vcolortarget + #undef light_flags + #undef light + #undef light_end + #undef r + #undef g + #undef b + #undef a + .endfunc \ No newline at end of file diff --git a/src/GL/rsp_gl_pipeline.S b/src/GL/rsp_gl_pipeline.S new file mode 100644 index 000000000..34d205572 --- /dev/null +++ b/src/GL/rsp_gl_pipeline.S @@ -0,0 +1,851 @@ +############################################################ +# rsp_gl_pipeline.S +# +# Authors: Giovanni Bajo +# Dennis Heinze +############################################################ +#include +#include +#include "gl_constants.h" +#include "GL/gl_enums.h" + + .data + + RSPQ_BeginOverlayHeader + RSPQ_DefineCommand GLCmd_InitPipe, 4 + RSPQ_DefineCommand GLCmd_SetVtxLoader, 12 + VTX_LOADER_MAX_SIZE + RSPQ_DefineCommand GLCmd_SetVtxCmdSize, 8 + RSPQ_DefineCommand GLCmd_DrawTriangle, 8 + RSPQ_DefineCommand GLCmd_SetPrimVertex, 32 + RSPQ_DefineCommand GLCmd_SetByte, 8 + RSPQ_DefineCommand GLCmd_SetWord, 8 + RSPQ_DefineCommand GLCmd_SetLong, 12 + RSPQ_EndOverlayHeader + + .align 4 +BANNER0: .ascii " RSP OpenGL T&L " +BANNER1: .ascii "Rasky & Snacchus" + + RSPQ_BeginSavedState + #include "rsp_gl_state.inc" + +#define SCREEN_VTX_CS_POSi 0 // X, Y, Z, W (all 32-bit) +#define SCREEN_VTX_CS_POSf 8 // X, Y, Z, W (all 32-bit) +#define SCREEN_VTX_X 16 +#define SCREEN_VTX_Y 18 +#define SCREEN_VTX_Z 20 +#define SCREEN_VTX_CLIP_CODE 22 +#define SCREEN_VTX_PADDING 23 +#define SCREEN_VTX_RGBA 24 +#define SCREEN_VTX_S 28 +#define SCREEN_VTX_T 30 +#define SCREEN_VTX_W 32 // FIXME: this is duplicated in CS_POS +#define SCREEN_VTX_INVW 36 // 32-bit +#define SCREEN_VTX_SIZE 40 + + .align 3 +VERTEX_CACHE: .dcb.b PRIM_VTX_SIZE * VERTEX_CACHE_SIZE + + RSPQ_EndSavedState + + .align 4 +DEFAULT_ATTRIBUTES: .half 0, 0, 0, 1<<5, 0, 0, 0, 0x7FFF, 0, 0, 0, 1<<8 + + .align 4 +CACHE_OFFSETS: .half 2,4,6,8,10,12,14,16,18 + +CLIP_CODE_FACTORS: .half 1, 1, GUARD_BAND_FACTOR, GUARD_BAND_FACTOR + + + .text + + .func GLCmd_InitPipe +GLCmd_InitPipe: + move s0, a0 + li s4, %lo(GL_STATE) + j DMAIn + li t0, DMA_SIZE(GL_STATE_SIZE, 1) + .endfunc + + .func GLCmd_SetVtxLoader +GLCmd_SetVtxLoader: + move s0, a1 + li s4, %lo(gl_vtx_loader) + jal DMAInAsync + li t0, DMA_SIZE(VTX_LOADER_MAX_SIZE, 1) + add s0, a0, s4 + jal_and_j DMAOutAsync, RSPQ_Loop + .endfunc + + .func GLCmd_SetVtxCmdSize +GLCmd_SetVtxCmdSize: + li s4, %lo(_RSPQ_OVERLAY_COMMAND_TABLE) + 8 + sh a0, 0(s4) + move s0, a1 + j DMAOutAsync + li t0, DMA_SIZE(8, 1) + .endfunc + + .func GLCmd_SetByte +GLCmd_SetByte: + jr ra + sb a1, %lo(GL_STATE)(a0) + .endfunc + + .func GLCmd_SetLong +GLCmd_SetLong: + sw a2, %lo(GL_STATE) + 4(a0) +GLCmd_SetWord: + jr ra + sw a1, %lo(GL_STATE)(a0) + .endfunc + + ######################################## + # GL_HandleMatrixPalette + # + # Args: + # v1 = Matrix index + # t5 = State flags + # s1 = Matrix pointer offset + # + ######################################## + .func GL_HandleMatrixPalette +GL_HandleMatrixPalette: + #define mtx_index v1 + #define state_flags t5 + #define mtx_pointer s1 + andi t0, state_flags, FLAG_MATRIX_PALETTE + beqz t0, JrRa + lw t0, %lo(GL_LOADED_MTX_INDEX) - 0xC(mtx_pointer) + + lw s0, %lo(GL_MATRIX_POINTERS)(mtx_pointer) + + # Check if this matrix is already loaded + beq t0, mtx_index, JrRa + sw mtx_index, %lo(GL_LOADED_MTX_INDEX) - 0xC(mtx_pointer) + + sll t0, mtx_index, 6 + add s0, t0 + j DMAIn + li t0, DMA_SIZE(MATRIX_SIZE, 1) + #undef mtx_index + #undef state_flags + #undef mtx_pointer + .endfunc + + ######################################## + # GLCmd_SetPrimVertex + # + # Arguments: + # * 0x00 (a0): offset within VERTEX_CACHE + # * 0x04 (a1): object space X, Y (16-bit) + # * 0x08 (a2): object space Z, W (16-bit) + # * 0x0C (a3): RGBA (8-bit each one) + # * 0x10: S, T (16-bit) + # * 0x14: normal X, Y, Z (8-bit each one) (LSB must be 0) + # + ######################################## + .align 3 + .func GLCmd_SetPrimVertex +GLCmd_SetPrimVertex: + #define vtx a0 + #define default s0 + #define current s1 + #define cmd_ptr s4 + #define norm v0 + #define mtxi v1 + #define state_flags t5 + + #define v___ $v01 + + #define vmtx0_i $v16 // m00 m01 m02 m03 + #define vmtx0_f $v17 + #define vmtx1_i $v18 // m10 m11 m12 m13 + #define vmtx1_f $v19 + #define vmtx2_i $v20 // m20 m21 m22 m23 + #define vmtx2_f $v21 + #define vmtx3_i $v22 // m30 m31 m32 m03 + #define vmtx3_f $v23 + + #define vpos $v24 + #define vcol $v25 + #define vtex $v26 + #define vdefault $v27 + #define vcspos_i $v28 + #define vcspos_f $v29 + + #define x e0 + #define y e1 + #define z e2 + #define w e3 + + addi cmd_ptr, rspq_dmem_buf_ptr, %lo(RSPQ_DMEM_BUFFER) + 4 + sub cmd_ptr, rspq_cmd_size + + li default, %lo(DEFAULT_ATTRIBUTES) + li current, %lo(GL_CURRENT_ATTRIBUTES) + + .align 3 +gl_vtx_loader: + .ds.l VTX_LOADER_MAX_COMMANDS + .align 3 + + addi vtx, %lo(VERTEX_CACHE) + sdv vpos, PRIM_VTX_X ,vtx + sdv vcol, PRIM_VTX_R ,vtx + sdv vtex, PRIM_VTX_TEX_S ,vtx + sw norm, PRIM_VTX_NORMAL(vtx) + + move ra2, ra + lw state_flags, %lo(GL_STATE_FLAGS) + + li s4, %lo(GL_MATRIX_MVP) + sb mtxi, PRIM_VTX_MTX_INDEX(vtx) + jal GL_HandleMatrixPalette + li s1, 0x10 + + jal GL_MtxTransformSingle + vcopy $v27, vpos + + # 32-bit right shift by 5, to keep the clip space coordinates unscaled + vmudm vcspos_i, vcspos_i, vshift8.e4 + vmadl vcspos_f, vcspos_f, vshift8.e4 + + sdv vcspos_i, PRIM_VTX_CS_POSi,vtx + sdv vcspos_f, PRIM_VTX_CS_POSf,vtx + + # Calculate and store clipping flags against CS.W. These + # will be used for trivial rejections. + vch v___, vcspos_i, vcspos_i.w + vcl v___, vcspos_f, vcspos_f.w + cfc2 t0, COP2_CTRL_VCC + andi t0, 0x707 # Isolate X/Y/Z flags + # Compress flags to 8 bit + srl t1, t0, 5 + andi t0, 0x7 + or t0, t1 + jr ra2 + sb t0, PRIM_VTX_TRCODE(vtx) + + # FIXME: in a glBegin/glEnd pair, we should also cache the per-vertex material. + + #undef cmd_ptr + #undef vtx + #undef in_xy + #undef in_zw + #undef in_rgba + #undef vtx_id + + #undef x + #undef y + #undef z + #undef w + + #undef v___ + + #undef vmtx0_i + #undef vmtx0_f + #undef vmtx1_i + #undef vmtx1_f + #undef vmtx2_i + #undef vmtx2_f + #undef vmtx3_i + #undef vmtx3_f + + #undef vpos + #undef vcspos_i + #undef vcspos_f + + .endfunc + + ################################################################ + # GL_CalcScreenSpace + # + # Args: + # s3 = Destination vertex address + # $v02 = Clip space position (fractional part) + # $v03 = Clip space position (integer part) + # + ################################################################ + .func GL_CalcScreenSpace +GL_CalcScreenSpace: + #define dst s3 + #define vcspos_f $v02 + #define vcspos_i $v03 + #define vinvw_f $v23 + #define vinvw_i $v24 + #define vviewscale $v25 + #define vviewoff $v26 + #define vscreenpos_i $v27 + #define vscreenpos_f $v28 + #define v___ $v29 + #define w e3 + + # Calculate 32-bit inverse W + # TODO: NR? + vrcph vinvw_i.w, vcspos_i.w + vrcpl vinvw_f.w, vcspos_f.w + vrcph vinvw_i.w, vzero.e0 + + # Calculate screenspace coords + li t0, %lo(GL_VIEWPORT_SCALE) + ldv vviewscale, 0,t0 + ldv vviewoff, 8,t0 + + vmudl v___, vcspos_f, vinvw_f.w + vmadm v___, vcspos_i, vinvw_f.w + vmadn vscreenpos_f, vcspos_f, vinvw_i.w + vmadh vscreenpos_i, vcspos_i, vinvw_i.w + + vmudn vscreenpos_f, vscreenpos_f, vviewscale + vmadh vscreenpos_i, vscreenpos_i, vviewscale + vadd vscreenpos_i, vviewoff + + sdv vscreenpos_i, SCREEN_VTX_X ,dst + ssv vcspos_i.w, SCREEN_VTX_W+0 ,dst + ssv vcspos_f.w, SCREEN_VTX_W+2 ,dst + ssv vinvw_i.w, SCREEN_VTX_INVW+0,dst + ssv vinvw_f.w, SCREEN_VTX_INVW+2,dst + jr ra + sb zero, SCREEN_VTX_PADDING(dst) + + #undef dst + #undef vcspos_f + #undef vcspos_i + #undef vinvw_f + #undef vinvw_i + #undef vviewscale + #undef vviewoff + #undef vscreenpos_i + #undef vscreenpos_f + #undef v___ + #undef w + + .endfunc + + ################################################################ + # GL_CalcClipCodes + # + # Args: + # s3 = Destination vertex address + # $v02 = Clip space position (fractional part) + # $v03 = Clip space position (integer part) + # + ################################################################ + .func GL_CalcClipCodes +GL_CalcClipCodes: + #define dst s3 + #define vcspos_f $v02 + #define vcspos_i $v03 + #define vguard_f $v27 + #define vguard_i $v28 + #define v___ $v29 + #define w e3 + + li t0, %lo(CLIP_CODE_FACTORS) + ldv vguard_i, 0,t0 + + vmudn vguard_f, vcspos_f, vguard_i + vmadh vguard_i, vcspos_i, vguard_i + + vch v___, vguard_i, vguard_i.w + vcl v___, vguard_f, vguard_f.w + cfc2 t0, COP2_CTRL_VCC + andi t0, 0x707 + srl t1, t0, 5 + andi t0, 0x7 + or t0, t1 + jr ra + sb t0, SCREEN_VTX_CLIP_CODE(dst) + + #undef dst + #undef vcspos_i + #undef vcspos_f + #undef vguard_i + #undef vguard_f + #undef v___ + #undef w + + .endfunc + + ################################################################ + # GL_TnL + # + # Args: + # s3 = address of the vertex in DMEM (usually within VERTEX_CACHE) + # + ################################################################ + .func GL_TnL +GL_TnL: + #define tmp_ptr s2 + #define vtx s3 + #define state_flags t5 + #define s e0 + move ra2, ra + + lw state_flags, %lo(GL_STATE_FLAGS) + + #define v___ $v01 + #define veyenormal $v03 + #define vrgba $v04 + #define vobjpos $v27 + #define veyepos $v28 + + ldv vrgba.e0, PRIM_VTX_R, vtx # R + G + B + A + ldv vrgba.e4, PRIM_VTX_R, vtx # R + G + B + A + + andi t0, state_flags, FLAG_NEED_EYE_SPACE + beqz t0, 2f + li s4, %lo(GL_MATRIX_MODELVIEW) + + lbu v1, PRIM_VTX_MTX_INDEX(vtx) + jal GL_HandleMatrixPalette + li s1, 0xC + + addi s5, vtx, PRIM_VTX_NORMAL-4 + lpv vobjpos.e0, 0,s5 # loads NX + NY + NZ into lanes 4-7 + ldv vobjpos.e0, PRIM_VTX_X,vtx # loads X + Y + Z + W into lanes 0-3 + + # TODO: technically we need the inverse transpose matrix, + # but for rigid matrices (translation, rotation, uniform scale) this is fine + + # Transform vertex pos and normal into eye space + jal GL_MtxTransformDouble + vmov vobjpos.e7, vzero.e0 + + li tmp_ptr, %lo(RDPQ_CMD_STAGING) + sqv veyepos, 0,tmp_ptr + ldv veyenormal.e0, 8,tmp_ptr + ldv veyenormal.e4, 8,tmp_ptr + + #define vsqdist_f $v10 + #define vsqdist_i $v11 + #define vtmp_f $v12 + #define vtmp_i $v13 + #define vinvdist_f $v14 + #define vinvdist_i $v15 + + # Re-normalize transformed normal + + andi t0, state_flags, FLAG_NORMALIZE + beqz t0, 1f + vmudh v___, veyenormal, veyenormal + vsar vsqdist_f, COP2_ACC_MD + vsar vsqdist_i, COP2_ACC_HI + vaddc vtmp_f, vsqdist_f, vsqdist_f.h1 + vadd vtmp_i, vsqdist_i, vsqdist_i.h1 + vaddc vsqdist_f, vtmp_f, vsqdist_f.h2 + vadd vsqdist_i, vtmp_i, vsqdist_i.h2 + + vrsqh v___.e0, vsqdist_i.e0 + vrsql vinvdist_f.e0, vsqdist_f.e0 + vrsqh vinvdist_i.e0, vzero.e0 + + vmudm v___, veyenormal, vinvdist_f.e0 + vmadh veyenormal, veyenormal, vinvdist_i.e0 + + #undef vsqdist_f + #undef vsqdist_i + #undef vtmp_f + #undef vtmp_i + #undef vinvdist_f + #undef vinvdist_i + +1: + ldv veyepos.e4, 0,tmp_ptr + +2: + + andi t0, state_flags, FLAG_LIGHTING + bnez t0, GL_VtxLighting + li ra, %lo(1f) +1: + + #define vtmp $v06 + #define vfog_i $v07 + #define vfog_f $v08 + andi t0, state_flags, FLAG_FOG + beqz t0, 1f + li s1, %lo(GL_STATE_FOG_PARAMS) + llv vfog_i, 0,s1 + llv vfog_f, 4,s1 + + # Compute abs(veyepos.z). + # abs(veyepos.z) is an approximation for the distance between the + # vertex and the origin in eye space, as recommended by the GL spec. + vsubc vtmp, vzero, veyepos.e2 + vge vtmp, veyepos.e2 + + # vtmp.e0 = abs(veyepos.z) - fog_end + # Note that fog_end might be negative. In practice this would + # rarely be the case, but it is not forbidden by the GL spec. + vsubc vtmp, vfog_i.e1 + + # vtmp.e0 = (abs(veyepos.z) - fog_end) / (fog_start - fog_end) + # The factor is premultiplied so that combined with VTX_SHIFT + # the product will be in 1.15 precision and saturated to 0x7FFF. + vmudm v___, vtmp, vfog_f.e0 + vmadh vtmp, vtmp, vfog_i.e0 + + # Clamp negative values to 0 + vge vtmp, vzero + + # Save the alpha factor in the vertex color, overwriting the alpha component. + vmov vrgba.e3, vtmp.e0 + #undef vtmp + #undef vfog +1: + + #define vtexsize $v06 + #define vtexoffset $v07 + #define vstrq $v08 + + ldv vstrq, PRIM_VTX_TEX_S,vtx # S + T + R + Q + suv vrgba, SCREEN_VTX_RGBA,vtx + + li s1, %lo(GL_STATE_TEX_SIZE) + llv vtexsize.s, 0,s1 + llv vtexoffset.s, 4,s1 + + #define vmodes $v09 + #define vobjlin $v10 + #define veyelin $v11 + #define vsphmap $v12 + + srl t0, state_flags, TEX_GEN_S_SHIFT + andi t0, 0xF + beqz t0, 2f + ldv vobjpos.e4, 0,tmp_ptr + + # Fill VCC with enable-flags + # We need to OR with 0xF0 so that the mode constants stay in the vmodes register + ori t0, 0x70 + ctc2 t0, COP2_CTRL_VCC + + li s1, %lo(GL_TEX_GEN) + + #define vplanes_s_i $v13 + #define vplanes_s_f $v14 + #define vplanes_t_i $v15 + #define vplanes_t_f $v16 + #define vplanes_r_i $v17 + #define vplanes_r_f $v18 + #define vplanes_q_i $v19 + #define vplanes_q_f $v20 + + lqv vmodes, TEX_GEN_MODE_OFFSET, s1 + lqv vplanes_s_i, TEX_GEN_INTEGER_OFFSET+0x00, s1 + + # If profiling is enabled, this ucode won't fit into IMEM in its entirety. + # In that case, sacrifice linear tex coord generation, a rarely used feature. +#if !RSPQ_PROFILE + # Check if linear tex gen is active for any coordinate + andi t0, state_flags, FLAG_TEX_GEN_LINEAR + beqz t0, 1f + # Select which coords to enable based on the enable-flags that were loaded into VCC above + vmrg vmodes, vmodes, vzero + lqv vplanes_s_f, TEX_GEN_FRACTION_OFFSET+0x00,s1 + lqv vplanes_t_i, TEX_GEN_INTEGER_OFFSET+0x10, s1 + lqv vplanes_t_f, TEX_GEN_FRACTION_OFFSET+0x10,s1 + lqv vplanes_r_i, TEX_GEN_INTEGER_OFFSET+0x20, s1 + lqv vplanes_r_f, TEX_GEN_FRACTION_OFFSET+0x20,s1 + lqv vplanes_q_i, TEX_GEN_INTEGER_OFFSET+0x30, s1 + lqv vplanes_q_f, TEX_GEN_FRACTION_OFFSET+0x30,s1 + + vmudn v___, vplanes_s_f, vobjpos.h0 + vmadh v___, vplanes_s_i, vobjpos.h0 + vmadn v___, vplanes_t_f, vobjpos.h1 + vmadh v___, vplanes_t_i, vobjpos.h1 + vmadn v___, vplanes_r_f, vobjpos.h2 + vmadh v___, vplanes_r_i, vobjpos.h2 + vmadn v___, vplanes_q_f, vobjpos.h3 + vmadh vobjlin, vplanes_q_i, vobjpos.h3 + + sdv vobjlin.e4, 0,tmp_ptr + ldv veyelin.e0, 0,tmp_ptr + + #undef vplanes_s_i + #undef vplanes_s_f + #undef vplanes_t_i + #undef vplanes_t_f + #undef vplanes_r_i + #undef vplanes_r_f + #undef vplanes_q_i + #undef vplanes_q_f + +1: +#endif + + #define vsqdist_i $v13 + #define vsqdist_f $v14 + #define vtmp_i $v15 + #define vtmp_f $v16 + #define vinvdist_i $v17 + #define vinvdist_f $v18 + #define veposnorm $v19 + #define vdot $v20 + #define refl_i $v21 + #define refl_f $v22 + + # Check if spherical tex gen is enabled for any coordinate + andi t0, state_flags, FLAG_TEX_GEN_SPHERICAL + beqz t0, 1f + # veposnorm = normalize(veyepos) + vmudh v___, veyepos, veyepos + vsar vsqdist_f, COP2_ACC_MD + vsar vsqdist_i, COP2_ACC_HI + vaddc vtmp_f, vsqdist_f, vsqdist_f.h1 + vadd vtmp_i, vsqdist_i, vsqdist_i.h1 + vaddc vsqdist_f, vtmp_f, vsqdist_f.h2 + vadd vsqdist_i, vtmp_i, vsqdist_i.h2 + + vrsqh v___.e0, vsqdist_i.e0 + vrsql vinvdist_f.e0, vsqdist_f.e0 + vrsqh vinvdist_i.e0, vzero.e0 + + vmudm v___, veyepos, vinvdist_f.e0 + vmadh veposnorm, veyepos, vinvdist_i.e0 + + # vdot = dot(veposnorm, veyenormal) + vmulf vdot, veposnorm, veyenormal + vaddc vtmp_f, vdot, vdot.h1 + vaddc vdot, vtmp_f, vdot.h2 + + # negate + vsubc vdot, vzero, vdot + + vcopy vtmp_i, vzero + vmov vtmp_i.e2, K16384 + + # refl = veposnorm - 2 * vdot * veyenormal + (0, 0, 1) + vmulf v___, veyenormal, vdot.e0 + vmacf v___, veyenormal, vdot.e0 # Add twice to account for multiplication by 2 + vmadh v___, veposnorm, K1 + vmadh v___, vtmp_i, K2 + + vsar refl_f, COP2_ACC_MD + vsar refl_i, COP2_ACC_HI + + # m = 1 / 2 * sqrt(dot(refl, refl)) + vmudl v___, refl_f, refl_f + vmadm v___, refl_i, refl_f + vmadn vsqdist_f, refl_f, refl_i + vmadh vsqdist_i, refl_i, refl_i + + vaddc vtmp_f, vsqdist_f, vsqdist_f.h1 + vadd vtmp_i, vsqdist_i, vsqdist_i.h1 + vaddc vsqdist_f, vtmp_f, vsqdist_f.h2 + vadd vsqdist_i, vtmp_i, vsqdist_i.h2 + + vaddc vtmp_i, vzero, K128 + + vrsqh v___.e0, vsqdist_i.e0 + vrsql vinvdist_f.e0, vsqdist_f.e0 + vrsqh vinvdist_i.e0, vzero.e0 + + # vshmap = refl * m + 0.5 + vmudh v___, vtmp_i, K1 + vmadl v___, refl_f, vinvdist_f.e0 + vmadm v___, refl_i, vinvdist_f.e0 + vmadn v___, refl_f, vinvdist_i.e0 + vmadh vsphmap, refl_i, vinvdist_i.e0 + +1: + veq v___, vmodes, vmodes.e4 + vmrg vstrq, vobjlin, vstrq + veq v___, vmodes, vmodes.e5 + vmrg vstrq, veyelin, vstrq + veq v___, vmodes, vmodes.e6 + vmrg vstrq, vsphmap, vstrq + + #undef vsqdist_i + #undef vsqdist_f + #undef vtmp_i + #undef vtmp_f + #undef vinvdist_i + #undef vinvdist_f + #undef veposnorm + #undef vdot + #undef refl_i + #undef refl_f + #undef vobjlin + #undef veyelin + #undef vmodes + +2: + #define vinvq_i $v26 + #define vinvq_f $v27 + #define vstrq_i $v28 + #define vstrq_f $v29 + #define q e3 + + # Apply texture matrix + vcopy $v27, vstrq + jal GL_MtxTransformSingle + li s4, %lo(GL_MATRIX_TEXTURE) + + # Perform perspective division + vrcph vinvq_i.q, vstrq_i.q + vrcpl vinvq_f.q, vstrq_f.q + vrcph vinvq_i.q, vzero.e0 + + vmudl v___, vstrq_f, vinvq_f.q + vmadm v___, vstrq_i, vinvq_f.q + vmadn vstrq_f, vstrq_f, vinvq_i.q + vmadh vstrq_i, vstrq_i, vinvq_i.q + + # vmudm vstrq_i, vstrq, K256 # >> 8 + # vmadl vstrq_f, vzero, vzero + + # Scale texcoord by texsize and subtract offset (to correct for bilinear sampling if active) + vmudn v___, vstrq_f, vtexsize + vmadh v___, vstrq_i, vtexsize + vmadh vstrq, vtexoffset, K1 + + #undef vinvq_i + #undef vinvq_f + #undef vstrq_i + #undef vstrq_f + #undef q + + lbu t0, PRIM_VTX_TRCODE(vtx) + + #define vcspos_f $v02 + #define vcspos_i $v03 + + ldv vcspos_f, PRIM_VTX_CS_POSf,vtx + ldv vcspos_i, PRIM_VTX_CS_POSi,vtx + + # Mark this vertex as having T&L applied + ori t0, 0x80 + sb t0, PRIM_VTX_TRCODE(vtx) + + jal GL_CalcScreenSpace + slv vstrq.s, SCREEN_VTX_S,vtx + + j GL_CalcClipCodes + move ra, ra2 + + #undef vcspos_f + #undef vcspos_i + #undef vtexsize + #undef vtexoffset + + #undef state_flags + #undef vtx + #undef vtx + + #undef v___ + #undef vrgba + #undef vst + #undef s + #undef vobjpos + #undef veyepos + #undef veyenormal + + .endfunc + + + ################################################################ + # GLCmd_DrawTriangle + # + # Arguments: + # a0: Bit 31..24: Command id + # Bit 11..0: Offset into vertex cache of vtx1 + # a1: Bit 27..16: Offset into vertex cache of vtx2 + # Bit 11..0: Offset into vertex cache of vtx3 + # + ################################################################ + .func GLCmd_DrawTriangle +GLCmd_DrawTriangle: + #define vtx1 a1 + #define vtx2 a2 + #define vtx3 a3 + #define trcode1 t6 + #define trcode2 t7 + #define trcode3 t8 + + # If GL_FONT_AND_BACK is set (GL_TRI_CULL==-1), there's nothing to do + lb v0, %lo(GL_TRI_CULL) + bltz v0, RSPQ_Loop + + addi vtx3, a1, %lo(VERTEX_CACHE) + srl vtx2, a1, 16 + addi vtx2, %lo(VERTEX_CACHE) + addi vtx1, a0, %lo(VERTEX_CACHE) + + # Trivial reject: if all the vertices are out of the same plane (at least one), + # the triangle is out of the viewport. + # NOTE: This deliberately uses lb instead of lbu so the sign bit is extended. + # The MSB of each TR-code is a bit flag that is set if the vertex has already + # had T&L applied once. + lb trcode1, PRIM_VTX_TRCODE(vtx1) + lb trcode2, PRIM_VTX_TRCODE(vtx2) + lb trcode3, PRIM_VTX_TRCODE(vtx3) + and t0, trcode1, trcode2 + and t0, trcode3 + andi t0, 0x3F + bnez t0, JrRa + nop + + # Perform T&L for each vertex if we haven't already + bgezal trcode1, GL_TnL + move s3, vtx1 + + bgezal trcode2, GL_TnL + move s3, vtx2 + + bgezal trcode3, GL_TnL + move s3, vtx3 + + lbu t0, SCREEN_VTX_CLIP_CODE(vtx1) + lbu t1, SCREEN_VTX_CLIP_CODE(vtx2) + lbu t2, SCREEN_VTX_CLIP_CODE(vtx3) + or t5, t0, t1 + or t5, t2 + + move s1, zero + beqz t5, gl_draw_single_triangle + move s2, zero + + jal GL_ClipTriangle + nop + + beqz v1, gl_draw_triangle_end + addi s2, -6 + lhu s5, 0(s1) +gl_draw_clipped_triangles_loop: + move vtx1, s5 + lhu vtx2, 2(s1) + lhu vtx3, 4(s1) + +gl_draw_single_triangle: + addi vtx1, SCREEN_VTX_X + addi vtx2, SCREEN_VTX_X + addi vtx3, SCREEN_VTX_X + + lhu a0, %lo(GL_TRI_CMD) + lb v0, %lo(GL_TRI_CULL) + jal RDPQ_Triangle + li s3, %lo(RDPQ_CMD_STAGING) + + jal RDPQ_Send + li s4, %lo(RDPQ_CMD_STAGING) + + blt s1, s2, gl_draw_clipped_triangles_loop + addi s1, 2 + +gl_draw_triangle_end: + j RSPQ_Loop + nop + + #undef vtx1 + #undef vtx2 + #undef vtx3 + + .endfunc + +#include "rsp_gl_common.inc" +#include "rsp_gl_lighting.inc" +#include "rsp_gl_clipping.inc" +#include diff --git a/src/GL/rsp_gl_state.inc b/src/GL/rsp_gl_state.inc new file mode 100644 index 000000000..ae171c17b --- /dev/null +++ b/src/GL/rsp_gl_state.inc @@ -0,0 +1,55 @@ +############################################################ +# rsp_gl_state.inc +# +# Authors: Giovanni Bajo +# Dennis Heinze +############################################################ +GL_STATE: + # This is the GL state that is also used by the pipeline. + GL_MATRICES: + GL_MATRIX_MODELVIEW: .ds.b MATRIX_SIZE + GL_MATRIX_PROJECTION: .ds.b MATRIX_SIZE + GL_MATRIX_TEXTURE: .ds.b MATRIX_SIZE + GL_MATRIX_PALETTE: .ds.b MATRIX_SIZE + GL_MATRIX_MVP: .ds.b MATRIX_SIZE + GL_LIGHTS: .ds.b LIGHT_STRUCT_SIZE + GL_TEX_GEN: .ds.b TEX_GEN_STRUCT_SIZE + GL_VIEWPORT_SCALE: .half 0,0,0,0 + GL_VIEWPORT_OFFSET: .half 0,0,0,0 + GL_STATE_LIGHT_AMBIENT: .half 0,0,0,0 + GL_MAT_AMBIENT: .half 0,0,0,0 + GL_MAT_DIFFUSE: .half 0,0,0,0 + GL_MAT_SPECULAR: .half 0,0,0,0 + GL_MAT_EMISSIVE: .half 0,0,0,0 + GL_MAT_COLOR_TARGET: .half 0,0,0 + GL_MAT_SHININESS: .half 0 + GL_CURRENT_ATTRIBUTES: + GL_CUR_COLOR: .half 0,0,0,0 + GL_CUR_TEX_COORDS: .half 0,0,0,0 + GL_CUR_NORMAL: .byte 0,0,0 + GL_CUR_MTX_INDEX: .byte 0 + GL_MATRIX_POINTERS: + GL_MODELVIEW_PTR: .word 0 + GL_PROJECTION_PTR: .word 0 + GL_TEXTURE_PTR: .word 0 + GL_PALETTE_PTR: .word 0 + GL_MVP_PTR: .word 0 + GL_LOADED_MTX_INDEX: .word 0,0 + GL_STATE_FLAGS: .word 0 + GL_STATE_FOG_PARAMS: .half 0,0,0,0 + GL_STATE_TEX_SIZE: .half 0,0 + GL_STATE_TEX_OFFSET: .half 0,0 + GL_STATE_POLYGON_MODE: .half 0 + GL_STATE_PRIM_TYPE: .half 0 + GL_STATE_CULL_MODE: .half 0 + GL_STATE_FRONT_FACE: .half 0 + GL_STATE_SHADE_MODEL: .half 0 + GL_STATE_POINT_SIZE: .half 0 + GL_STATE_LINE_WIDTH: .half 0 + GL_STATE_MATRIX_MODE: .half 0 + GL_TRI_CMD: .half 0 + GL_TRI_CULL: .byte 0,0 + .align 3 +GL_STATE_END: + +#define GL_STATE_SIZE (GL_STATE_END - GL_STATE) diff --git a/src/GL/rsp_pipeline.c b/src/GL/rsp_pipeline.c new file mode 100644 index 000000000..901578d24 --- /dev/null +++ b/src/GL/rsp_pipeline.c @@ -0,0 +1,584 @@ +/** + * @file rsp_pipeline.c + * @author Dennis Heinze + * @brief OpenGL RSP-based rendering pipeline implementation. + */ +#include + +#include "gl_internal.h" +#include "rsp_asm.h" + +extern gl_state_t *state; + +#define DEFINE_BYTE_READ_FUNC(name, src_type, convert) \ + static void name(gl_cmd_stream_t *s, const src_type *src, uint32_t count) \ + { \ + for (uint32_t i = 0; i < count; i++) gl_cmd_stream_put_byte(s, convert(src[i])); \ + } + +#define DEFINE_HALF_READ_FUNC(name, src_type, convert) \ + static void name(gl_cmd_stream_t *s, const src_type *src, uint32_t count) \ + { \ + for (uint32_t i = 0; i < count; i++) gl_cmd_stream_put_half(s, convert(src[i])); \ + } + +static void read_fixed_point(gl_cmd_stream_t *s, const int16u_t *src, uint32_t count, int shift) +{ + if (shift > 0) { + for (uint32_t i = 0; i < count; i++) { + int16_t value = src[i]; + assertf(value <= SHRT_MAX>>shift && value >= SHRT_MIN>>shift, "Fixed point overflow: %d << %d", value, shift); + gl_cmd_stream_put_half(s, value << shift); + } + } else { + for (uint32_t i = 0; i < count; i++) { + gl_cmd_stream_put_half(s, src[i] >> -shift); + } + } +} + +#define DEFINE_HALF_FIXED_READ_FUNC(name, precision) \ + static void name(gl_cmd_stream_t *s, const int16u_t *src, uint32_t count) \ + { \ + read_fixed_point(s, src, count, precision.shift_amount); \ + } + +#define VTX_CONVERT_INT(v) ((v) << VTX_SHIFT) +#define VTX_CONVERT_FLT(v) ((v) * (1<vertex_halfx_precision) + +#define COL_CONVERT_U8(v) ((v) << 7) +#define COL_CONVERT_I8(v) ((v) << 8) +#define COL_CONVERT_U16(v) ((v) >> 1) +#define COL_CONVERT_I16(v) ((v)) +#define COL_CONVERT_U32(v) ((v) >> 17) +#define COL_CONVERT_I32(v) ((v) >> 16) +#define COL_CONVERT_F32(v) (FLOAT_TO_I16(v)) +#define COL_CONVERT_F64(v) (FLOAT_TO_I16(v)) + +DEFINE_HALF_READ_FUNC(col_read_u8, uint8_t, COL_CONVERT_U8) +DEFINE_HALF_READ_FUNC(col_read_i8, int8_t, COL_CONVERT_I8) +DEFINE_HALF_READ_FUNC(col_read_u16, uint16u_t, COL_CONVERT_U16) +DEFINE_HALF_READ_FUNC(col_read_i16, int16u_t, COL_CONVERT_I16) +DEFINE_HALF_READ_FUNC(col_read_u32, uint32u_t, COL_CONVERT_U32) +DEFINE_HALF_READ_FUNC(col_read_i32, int32u_t, COL_CONVERT_I32) +DEFINE_HALF_READ_FUNC(col_read_f32, floatu, COL_CONVERT_F32) +DEFINE_HALF_READ_FUNC(col_read_f64, doubleu, COL_CONVERT_F64) + +#define TEX_CONVERT_INT(v) ((v) << TEX_SHIFT) +#define TEX_CONVERT_FLT(v) ((v) * (1<texcoord_halfx_precision) + +#define NRM_CONVERT_U8(v) ((v) >> 1) +#define NRM_CONVERT_I8(v) ((v)) +#define NRM_CONVERT_U16(v) ((v) >> 9) +#define NRM_CONVERT_I16(v) ((v) >> 8) +#define NRM_CONVERT_U32(v) ((v) >> 25) +#define NRM_CONVERT_I32(v) ((v) >> 24) +#define NRM_CONVERT_F32(v) ((v) * 0x7F) +#define NRM_CONVERT_F64(v) ((v) * 0x7F) + +DEFINE_BYTE_READ_FUNC(nrm_read_i8, int8_t, NRM_CONVERT_I8) +DEFINE_BYTE_READ_FUNC(nrm_read_i16, int16u_t, NRM_CONVERT_I16) +DEFINE_BYTE_READ_FUNC(nrm_read_i32, int32u_t, NRM_CONVERT_I32) +DEFINE_BYTE_READ_FUNC(nrm_read_f32, floatu, NRM_CONVERT_F32) +DEFINE_BYTE_READ_FUNC(nrm_read_f64, doubleu, NRM_CONVERT_F64) + +#define MTX_INDEX_CONVERT(v) (v) + +DEFINE_BYTE_READ_FUNC(mtx_index_read_u8, uint8_t, MTX_INDEX_CONVERT) +DEFINE_BYTE_READ_FUNC(mtx_index_read_u16, uint16u_t, MTX_INDEX_CONVERT) +DEFINE_BYTE_READ_FUNC(mtx_index_read_u32, uint32u_t, MTX_INDEX_CONVERT) + +const rsp_read_attrib_func rsp_read_funcs[ATTRIB_COUNT][ATTRIB_TYPE_COUNT] = { + { + (rsp_read_attrib_func)vtx_read_i8, + NULL, + (rsp_read_attrib_func)vtx_read_i16, + NULL, + (rsp_read_attrib_func)vtx_read_i32, + NULL, + (rsp_read_attrib_func)vtx_read_f32, + (rsp_read_attrib_func)vtx_read_f64, + (rsp_read_attrib_func)vtx_read_x16, + }, + { + (rsp_read_attrib_func)col_read_i8, + (rsp_read_attrib_func)col_read_u8, + (rsp_read_attrib_func)col_read_i16, + (rsp_read_attrib_func)col_read_u16, + (rsp_read_attrib_func)col_read_i32, + (rsp_read_attrib_func)col_read_u32, + (rsp_read_attrib_func)col_read_f32, + (rsp_read_attrib_func)col_read_f64, + NULL, + }, + { + (rsp_read_attrib_func)tex_read_i8, + NULL, + (rsp_read_attrib_func)tex_read_i16, + NULL, + (rsp_read_attrib_func)tex_read_i32, + NULL, + (rsp_read_attrib_func)tex_read_f32, + (rsp_read_attrib_func)tex_read_f64, + (rsp_read_attrib_func)tex_read_x16, + }, + { + (rsp_read_attrib_func)nrm_read_i8, + NULL, + (rsp_read_attrib_func)nrm_read_i16, + NULL, + (rsp_read_attrib_func)nrm_read_i32, + NULL, + (rsp_read_attrib_func)nrm_read_f32, + (rsp_read_attrib_func)nrm_read_f64, + NULL, + }, + { + NULL, + (rsp_read_attrib_func)mtx_index_read_u8, + NULL, + (rsp_read_attrib_func)mtx_index_read_u16, + NULL, + (rsp_read_attrib_func)mtx_index_read_u32, + NULL, + NULL, + NULL, + }, +}; + +static const gl_array_t dummy_arrays[ATTRIB_COUNT] = { + { .enabled = true, .size = 4 } +}; + +typedef enum { + BEGIN_END_INDETERMINATE, + BEGIN_END_VERTEX, + BEGIN_END_ARRAY_ELEMENT, +} begin_end_type_t; + +static begin_end_type_t begin_end_type; +static uint32_t vtx_cmd_size; + +static void upload_current_attributes(const gl_array_t *arrays) +{ + if (arrays[ATTRIB_COLOR].enabled) { + gl_set_current_color(state->current_attributes.color); + } + + if (arrays[ATTRIB_TEXCOORD].enabled) { + gl_set_current_texcoords(state->current_attributes.texcoord); + } + + if (arrays[ATTRIB_NORMAL].enabled) { + gl_set_current_normal(state->current_attributes.normal); + } + + if (arrays[ATTRIB_MTX_INDEX].enabled) { + gl_set_current_mtx_index(state->current_attributes.mtx_index); + } +} + +static void load_attribs_at_index(const gl_array_t *arrays, uint32_t index) +{ + gl_fill_all_attrib_defaults(arrays); + gl_load_attribs(arrays, index); +} + +static void load_last_attributes(const gl_array_t *arrays, uint32_t last_index) +{ + load_attribs_at_index(arrays, last_index); + upload_current_attributes(arrays); +} + +static void glp_set_attrib(gl_array_type_t array_type, const void *value, GLenum type, uint32_t size) +{ + static const glp_command_t cmd_table[] = { GLP_CMD_SET_LONG, GLP_CMD_SET_LONG, GLP_CMD_SET_WORD, GLP_CMD_SET_BYTE }; + static const uint32_t cmd_size_table[] = { 3, 3, 2, 2 }; + static const uint32_t offset_table[] = { + offsetof(gl_server_state_t, color), + offsetof(gl_server_state_t, tex_coords), + offsetof(gl_server_state_t, normal), + offsetof(gl_server_state_t, mtx_index) + }; + static const int16_t default_value_table[][4] = { + { 0, 0, 0, 0x7FFF }, + { 0, 0, 0, 1 } + }; + + uint32_t table_index = array_type - 1; + + const uint32_t offset = offset_table[table_index]; + + const rsp_read_attrib_func *read_funcs = rsp_read_funcs[array_type]; + const rsp_read_attrib_func read_func = read_funcs[gl_type_to_index(type)]; + + gl_cmd_stream_t s = gl_cmd_stream_begin(glp_overlay_id, cmd_table[table_index], cmd_size_table[table_index]); + gl_cmd_stream_put_half(&s, offset); + + switch (array_type) { + case ATTRIB_COLOR: + case ATTRIB_TEXCOORD: + read_func(&s, value, size); + read_funcs[gl_type_to_index(GL_SHORT)](&s, default_value_table[table_index] + size, 4 - size); + break; + case ATTRIB_NORMAL: + read_func(&s, value, size); + break; + case ATTRIB_MTX_INDEX: + for (uint32_t i = 0; i < 3; i++) gl_cmd_stream_put_byte(&s, 0); + read_func(&s, value, size); + break; + default: + assert(!"Unexpected array type"); + break; + } + + gl_cmd_stream_end(&s); +} + +static void set_attrib(gl_array_type_t array_type, const void *value, GLenum type, uint32_t size) +{ + glp_set_attrib(array_type, value, type, size); + gl_read_attrib(array_type, value, type, size); +} + +static bool check_last_array_element(int32_t *index) +{ + if (state->last_array_element >= 0) { + *index = state->last_array_element; + state->last_array_element = -1; + return true; + } + + return false; +} + +static void require_array_element(const gl_array_t *arrays) +{ + int32_t index; + if (check_last_array_element(&index)) { + for (uint32_t i = 0; i < ATTRIB_COUNT; i++) + { + const gl_array_t *array = &arrays[i]; + const void *value = gl_get_attrib_element(array, index); + set_attrib(i, value, array->type, array->size); + } + } +} + +static inline gl_cmd_stream_t write_vertex_begin(uint32_t cache_index) +{ + gl_cmd_stream_t s = gl_cmd_stream_begin(glp_overlay_id, GLP_CMD_SET_PRIM_VTX, vtx_cmd_size>>2); + gl_cmd_stream_put_half(&s, cache_index * PRIM_VTX_SIZE); + return s; +} + +static inline void write_vertex_end(gl_cmd_stream_t *s) +{ + gl_cmd_stream_end(s); +} + +static void write_vertex_from_arrays(const gl_array_t *arrays, uint32_t index, uint8_t cache_index) +{ + gl_cmd_stream_t s = write_vertex_begin(cache_index); + + for (uint32_t i = 0; i < ATTRIB_COUNT; i++) + { + const gl_array_t *array = &arrays[i]; + if (!array->enabled) { + continue; + } + + const void *src = gl_get_attrib_element(array, index); + array->rsp_read_func(&s, src, array->size); + } + + write_vertex_end(&s); +} + +static void submit_vertex(uint32_t cache_index) +{ + uint8_t indices[3]; + if (gl_prim_assembly(cache_index, indices)) + { + glpipe_draw_triangle(indices[0], indices[1], indices[2]); + } +} + +static void draw_vertex_from_arrays(const gl_array_t *arrays, uint32_t id, uint32_t index) +{ + uint8_t cache_index; + if (gl_get_cache_index(id, &cache_index)) + { + write_vertex_from_arrays(arrays, index, cache_index); + } + + submit_vertex(cache_index); +} + +static void gl_asm_vtx_loader(const gl_array_t *arrays) +{ + extern uint8_t rsp_gl_pipeline_text_start[]; + + rspq_write_t w = rspq_write_begin(glp_overlay_id, GLP_CMD_SET_VTX_LOADER, 3 + VTX_LOADER_MAX_COMMANDS); + rspq_write_arg(&w, PhysicalAddr(rsp_gl_pipeline_text_start) - 0x1000); + + uint32_t pointer = PhysicalAddr(w.pointer); + bool aligned = (pointer & 0x7) == 0; + + rspq_write_arg(&w, aligned ? pointer + 8 : pointer + 4); + + if (aligned) { + rspq_write_arg(&w, 0); + } + + const uint8_t default_reg = 16; + const uint8_t current_reg = 17; + const uint8_t cmd_ptr_reg = 20; + const uint8_t norm_reg = 2; + const uint8_t mtx_index_reg = 3; + const uint8_t dst_vreg_base = 24; + const uint32_t current_normal_offset = offsetof(gl_server_state_t, normal) - offsetof(gl_server_state_t, color); + const uint32_t current_mtx_index_offset = offsetof(gl_server_state_t, mtx_index) - offsetof(gl_server_state_t, color); + + uint32_t cmd_offset = 0; + + for (uint32_t i = 0; i < ATTRIB_NORMAL; i++) + { + const uint32_t dst_vreg = dst_vreg_base + i; + const gl_array_t *array = &arrays[i]; + + if (!array->enabled) { + rspq_write_arg(&w, rsp_asm_lwc2(VLOAD_DOUBLE, dst_vreg, 0, i-1, current_reg)); + } else { + uint32_t cmd_size = array->size * 2; + uint32_t alignment = next_pow2(cmd_size); + if (cmd_offset & (alignment-1)) { + rspq_write_arg(&w, rsp_asm_addi(cmd_ptr_reg, cmd_ptr_reg, cmd_offset)); + cmd_offset = 0; + } + + switch (array->size) + { + case 1: + rspq_write_arg(&w, rsp_asm_lwc2(VLOAD_DOUBLE, dst_vreg, 0, (i*8)>>3, default_reg)); + rspq_write_arg(&w, rsp_asm_lwc2(VLOAD_HALF, dst_vreg, 0, cmd_offset>>1, cmd_ptr_reg)); + break; + case 2: + rspq_write_arg(&w, rsp_asm_lwc2(VLOAD_LONG, dst_vreg, 0, cmd_offset>>2, cmd_ptr_reg)); + rspq_write_arg(&w, rsp_asm_lwc2(VLOAD_LONG, dst_vreg, 4, ((i*8)>>2) + 1, default_reg)); + break; + case 3: + rspq_write_arg(&w, rsp_asm_lwc2(VLOAD_DOUBLE, dst_vreg, 0, cmd_offset>>3, cmd_ptr_reg)); + rspq_write_arg(&w, rsp_asm_lwc2(VLOAD_HALF, dst_vreg, 6, ((i*8)>>1) + 3, default_reg)); + break; + case 4: + rspq_write_arg(&w, rsp_asm_lwc2(VLOAD_DOUBLE, dst_vreg, 0, cmd_offset>>3, cmd_ptr_reg)); + break; + } + + cmd_offset += cmd_size; + } + } + + // TODO: optimize for when both normal and matrix index com from the same source (They fit into a single word) + + if (!arrays[ATTRIB_NORMAL].enabled) { + rspq_write_arg(&w, rsp_asm_lw(norm_reg, current_normal_offset, current_reg)); + } else { + rspq_write_arg(&w, rsp_asm_lw(norm_reg, cmd_offset, cmd_ptr_reg)); + cmd_offset += 3; + } + + if (!arrays[ATTRIB_MTX_INDEX].enabled) { + rspq_write_arg(&w, rsp_asm_lbu(mtx_index_reg, current_mtx_index_offset, current_reg)); + } else { + rspq_write_arg(&w, rsp_asm_lbu(mtx_index_reg, cmd_offset, cmd_ptr_reg)); + } + + rspq_write_end(&w); +} + +static uint32_t get_vertex_cmd_size(const gl_array_t *arrays) +{ + uint32_t cmd_size = 4; + + for (uint32_t i = 0; i < ATTRIB_NORMAL; i++) + { + if (arrays[i].enabled) { + cmd_size += arrays[i].size * 2; + } + } + if (arrays[ATTRIB_NORMAL].enabled) { + cmd_size += 3; + } + if (arrays[ATTRIB_MTX_INDEX].enabled) { + cmd_size += 1; + } + + return ROUND_UP(cmd_size, 4); +} + +static void gl_update_vertex_cmd_size(const gl_array_t *arrays) +{ + vtx_cmd_size = get_vertex_cmd_size(arrays); + + // TODO: This is dependent on the layout of data structures internal to rspq. + // How can we make it more robust? + + extern uint8_t rsp_queue_data_start[]; + extern uint8_t rsp_queue_data_end[0]; + extern uint8_t rsp_gl_pipeline_data_start[]; + + uint32_t ovl_data_offset = rsp_queue_data_end - rsp_queue_data_start; + uint8_t *rsp_gl_pipeline_ovl_header = rsp_gl_pipeline_data_start + ovl_data_offset; + + #define CMD_DESC_SIZE 2 + + uint16_t *cmd_descriptor = (uint16_t*)(rsp_gl_pipeline_ovl_header + RSPQ_OVERLAY_HEADER_SIZE + GLP_CMD_SET_PRIM_VTX*CMD_DESC_SIZE); + uint16_t patched_cmd_descriptor = (*cmd_descriptor & 0x3FF) | ((vtx_cmd_size & 0xFC) << 8); + + data_cache_hit_writeback_invalidate(cmd_descriptor, CMD_DESC_SIZE); + glpipe_set_vtx_cmd_size(patched_cmd_descriptor, cmd_descriptor); +} + +static void gl_prepare_vtx_cmd(const gl_array_t *arrays) +{ + gl_asm_vtx_loader(arrays); + gl_update_vertex_cmd_size(arrays); +} + +static void gl_rsp_begin() +{ + glpipe_init(); + state->last_array_element = -1; + begin_end_type = BEGIN_END_INDETERMINATE; +} + +static void gl_rsp_end() +{ + int32_t index; + if (check_last_array_element(&index)) { + load_last_attributes(state->array_object->arrays, index); + } + + if (state->begin_end_active) { + // TODO: Load from arrays + gl_set_current_color(state->current_attributes.color); + gl_set_current_texcoords(state->current_attributes.texcoord); + gl_set_current_normal(state->current_attributes.normal); + gl_set_current_mtx_index(state->current_attributes.mtx_index); + } +} + +static void gl_rsp_vertex(const void *value, GLenum type, uint32_t size) +{ + if (begin_end_type != BEGIN_END_VERTEX) { + gl_prepare_vtx_cmd(dummy_arrays); + begin_end_type = BEGIN_END_VERTEX; + } + + static const int16_t default_values[] = { 0, 0, 0, 1 }; + + uint8_t cache_index; + if (gl_get_cache_index(next_prim_id(), &cache_index)) + { + require_array_element(state->array_object->arrays); + + rsp_read_attrib_func read_func = rsp_read_funcs[ATTRIB_VERTEX][gl_type_to_index(type)]; + + gl_cmd_stream_t s = write_vertex_begin(cache_index); + read_func(&s, value, size); + vtx_read_i16(&s, default_values + size, 4 - size); + write_vertex_end(&s); + } + + submit_vertex(cache_index); +} + +static void gl_rsp_color(const void *value, GLenum type, uint32_t size) +{ + set_attrib(ATTRIB_COLOR, value, type, size); +} + +static void gl_rsp_tex_coord(const void *value, GLenum type, uint32_t size) +{ + set_attrib(ATTRIB_TEXCOORD, value, type, size); +} + +static void gl_rsp_normal(const void *value, GLenum type, uint32_t size) +{ + set_attrib(ATTRIB_NORMAL, value, type, size); +} + +static void gl_rsp_mtx_index(const void *value, GLenum type, uint32_t size) +{ + set_attrib(ATTRIB_MTX_INDEX, value, type, size); +} + +static void gl_rsp_array_element(uint32_t index) +{ + if (begin_end_type != BEGIN_END_ARRAY_ELEMENT) { + gl_prepare_vtx_cmd(state->array_object->arrays); + begin_end_type = BEGIN_END_ARRAY_ELEMENT; + } + + draw_vertex_from_arrays(state->array_object->arrays, index, index); + state->last_array_element = index; +} + +static void gl_rsp_draw_arrays(uint32_t first, uint32_t count) +{ + if (state->array_object->arrays[ATTRIB_VERTEX].enabled) { + gl_prepare_vtx_cmd(state->array_object->arrays); + for (uint32_t i = 0; i < count; i++) + { + draw_vertex_from_arrays(state->array_object->arrays, next_prim_id(), first + i); + } + } + + load_last_attributes(state->array_object->arrays, first + count - 1); +} + +static void gl_rsp_draw_elements(uint32_t count, const void* indices, read_index_func read_index) +{ + gl_fill_all_attrib_defaults(state->array_object->arrays); + + if (state->array_object->arrays[ATTRIB_VERTEX].enabled) { + gl_prepare_vtx_cmd(state->array_object->arrays); + for (uint32_t i = 0; i < count; i++) + { + uint32_t index = read_index(indices, i); + draw_vertex_from_arrays(state->array_object->arrays, index, index); + } + } + + load_last_attributes(state->array_object->arrays, read_index(indices, count - 1)); +} + +const gl_pipeline_t gl_rsp_pipeline = (gl_pipeline_t) { + .begin = gl_rsp_begin, + .end = gl_rsp_end, + .vertex = gl_rsp_vertex, + .color = gl_rsp_color, + .tex_coord = gl_rsp_tex_coord, + .normal = gl_rsp_normal, + .mtx_index = gl_rsp_mtx_index, + .array_element = gl_rsp_array_element, + .draw_arrays = gl_rsp_draw_arrays, + .draw_elements = gl_rsp_draw_elements, +}; diff --git a/src/GL/texture.c b/src/GL/texture.c new file mode 100644 index 000000000..8b7d9f4a8 --- /dev/null +++ b/src/GL/texture.c @@ -0,0 +1,1302 @@ +/** + * @file texture.c + * @author Dennis Heinze + * @brief OpenGL texture object management and image operations. + */ +#include "gl_internal.h" +#include "../rspq/rspq_internal.h" +#include "../rdpq/rdpq_sprite_internal.h" +#include "rdpq.h" +#include "rdpq_mode.h" +#include "rdpq_tex.h" +#include "rdpq_sprite.h" +#include "sprite.h" +#include "debug.h" +#include +#include +#include +#include + +_Static_assert(TEXTURE_BILINEAR_MASK << TEX_BILINEAR_SHIFT == SOM_SAMPLE_BILINEAR >> 32); +_Static_assert(TEXTURE_BILINEAR_MASK << TEX_BILINEAR_OFFSET_SHIFT == HALF_TEXEL); +_Static_assert((1<> 32) >> BILINEAR_TEX_OFFSET_SHIFT == HALF_TEXEL); +_Static_assert(TEX_FLAG_DETAIL << TEX_DETAIL_SHIFT == SOM_TEXTURE_DETAIL >> 32); + +extern gl_state_t *state; +static inline void texture_get_texparms(gl_texture_object_t *obj, GLint level, rdpq_texparms_t *parms); + +void gl_texture_set_min_filter(gl_texture_object_t *obj, uint32_t offset, GLenum param); + +void gl_init_texture_object(gl_texture_object_t *obj) +{ + gl_srv_texture_object_t *srv_obj = malloc_uncached(sizeof(gl_srv_texture_object_t)); + assertf(srv_obj, "Out of memory"); + *srv_obj = (gl_srv_texture_object_t){ + .min_filter = GL_NEAREST_MIPMAP_LINEAR, + .mag_filter = GL_LINEAR, + }; + + // Fill the levels block with NOOPs, and terminate it with a RET. + for (int i=0; ilevels_block[i] = RSPQ_CMD_WRITE_STATUS << 24; + } + srv_obj->levels_block[MAX_TEXTURE_LEVELS*2] = (RSPQ_CMD_RET << 24) | (1<<2); + + *obj = (gl_texture_object_t) { + .wrap_s = GL_REPEAT, + .wrap_t = GL_REPEAT, + .srv_object = srv_obj, + }; +} + +void surface_free_safe(surface_t *surface) +{ + if (surface_has_owned_buffer(surface)) { + rdpq_call_deferred(free_uncached, surface->buffer); + } + memset(surface, 0, sizeof(surface_t)); +} + +void texture_image_free_safe(gl_texture_object_t *obj, uint32_t level) +{ + if (obj->blocks[level] != NULL) { + rdpq_call_deferred((void (*)(void*))rspq_block_free, obj->blocks[level]); + obj->blocks[level] = NULL; + } + + surface_free_safe(&obj->surfaces[level]); +} + +void texture_image_free(gl_texture_object_t *obj, uint32_t level) +{ + #if 0 + gl_srv_texture_object_t *srv_obj = obj->srv_object; + if ((srv_obj->levels_block[level*2] >> 24) == RSPQ_CMD_CALL) { + rspq_block_t *mem = (rspq_block_t*)((srv_obj->levels_block[level*2] & 0xFFFFFF) | 0xA0000000); + rspq_block_free(mem); + } + #else + if (obj->blocks[level] != NULL) { + rspq_block_free(obj->blocks[level]); + obj->blocks[level] = NULL; + } + #endif + + surface_free(&obj->surfaces[level]); +} + +void gl_cleanup_texture_object(gl_texture_object_t *obj) +{ + for (uint32_t i = 0; i < MAX_TEXTURE_LEVELS; i++) + { + texture_image_free(obj, i); + } + + free_uncached(obj->srv_object); + obj->srv_object = NULL; +} + +void gl_texture_init() +{ + state->default_textures = malloc_uncached(sizeof(gl_texture_object_t) * 2); + assertf(state->default_textures, "Out of memory"); + + gl_init_texture_object(&state->default_textures[0]); + gl_init_texture_object(&state->default_textures[1]); + + state->default_textures[0].dimensionality = GL_TEXTURE_1D; + state->default_textures[1].dimensionality = GL_TEXTURE_2D; + + state->default_textures[0].flags |= TEX_IS_DEFAULT; + state->default_textures[1].flags |= TEX_IS_DEFAULT; + + state->texture_1d_object = &state->default_textures[0]; + state->texture_2d_object = &state->default_textures[1]; +} + +void gl_texture_close() +{ + gl_cleanup_texture_object(&state->default_textures[0]); + gl_cleanup_texture_object(&state->default_textures[1]); + + free_uncached(state->default_textures); +} + +uint32_t gl_log2(uint32_t s) +{ + uint32_t log = 0; + while (s >>= 1) ++log; + return log; +} + +tex_format_t gl_tex_format_to_rdp(GLenum format) +{ + switch (format) { + case GL_RGB5_A1: + return FMT_RGBA16; + case GL_RGBA8: + return FMT_RGBA32; + case GL_LUMINANCE4_ALPHA4: + return FMT_IA8; + case GL_LUMINANCE8_ALPHA8: + return FMT_IA16; + case GL_INTENSITY4: + return FMT_I4; + case GL_INTENSITY8: + return FMT_I8; + default: + return FMT_NONE; + } +} + +GLenum rdp_tex_format_to_gl(tex_format_t format) +{ + switch (format) { + case FMT_RGBA16: + return GL_RGB5_A1; + case FMT_RGBA32: + return GL_RGBA8; + case FMT_IA8: + return GL_LUMINANCE4_ALPHA4; + case FMT_IA16: + return GL_LUMINANCE8_ALPHA8; + case FMT_I4: + return GL_INTENSITY4; + case FMT_I8: + return GL_INTENSITY8; + default: + return 0; + } +} + +gl_texture_object_t * gl_get_active_texture() +{ + if (state->texture_2d) { + return state->texture_2d_object; + } + + if (state->texture_1d) { + return state->texture_1d_object; + } + + return NULL; +} + +uint32_t gl_texture_get_offset(GLenum target) +{ + switch (target) { + case GL_TEXTURE_1D: + return offsetof(gl_server_state_t, bound_textures) + sizeof(gl_srv_texture_object_t) * 0; + case GL_TEXTURE_2D: + return offsetof(gl_server_state_t, bound_textures) + sizeof(gl_srv_texture_object_t) * 1; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid texture target", target); + return 0; + } +} + +gl_texture_object_t * gl_get_texture_object(GLenum target) +{ + switch (target) { + case GL_TEXTURE_1D: + return state->texture_1d_object; + case GL_TEXTURE_2D: + return state->texture_2d_object; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid texture target", target); + return NULL; + } +} + +inline bool texture_is_sprite(gl_texture_object_t *obj) +{ + return obj->sprite != NULL; +} + +inline bool texture_has_image(gl_texture_object_t *obj) +{ + return (obj->flags & TEX_HAS_IMAGE) != 0; +} + +inline bool texture_is_default(gl_texture_object_t *obj) +{ + return (obj->flags & TEX_IS_DEFAULT) != 0; +} + +void gl_texture_set_upload_block(uint32_t offset, int level, int width, int height, tex_format_t fmt, rspq_block_t *texup_block) +{ + assertf(texup_block->nesting_level == 0, "texture loader: nesting level is %d", texup_block->nesting_level); + + uint32_t img_offset = offset + level * sizeof(gl_texture_image_t); + gl_set_word (GL_UPDATE_NONE, img_offset + IMAGE_WIDTH_OFFSET, (width << 16) | height); + gl_set_short(GL_UPDATE_NONE, img_offset + IMAGE_INTERNAL_FORMAT_OFFSET, fmt); + + uint32_t cmd0 = (RSPQ_CMD_CALL << 24) | PhysicalAddr(texup_block->cmds); + uint32_t cmd1 = texup_block->nesting_level << 2; + gl_set_long(GL_UPDATE_TEXTURE_OBJECTS, offset + TEXTURE_LEVELS_BLOCK_OFFSET + level*8, ((uint64_t)cmd0 << 32) | cmd1); + + gl_set_flag_raw(GL_UPDATE_NONE, offset + TEXTURE_FLAGS_OFFSET, TEX_FLAG_UPLOAD_DIRTY, true); +} + +void glSpriteTextureN64(GLenum target, sprite_t *sprite, rdpq_texparms_t *texparms) +{ + gl_assert_no_display_list(); + if (!gl_ensure_no_begin_end()) return; + + uint32_t offset = gl_texture_get_offset(target); + if (offset == 0) return; + + gl_texture_object_t *obj = gl_get_texture_object(target); + if (texture_is_default(obj)) { + gl_set_error(GL_INVALID_OPERATION, "Cannot assign sprite to a default texture"); + return; + } + + if (target == GL_TEXTURE_1D && sprite->height != 1) { + gl_set_error(GL_INVALID_VALUE, "Sprite must have height 1 when using target GL_TEXTURE_1D"); + return; + } + + for (uint32_t i = 0; i < MAX_TEXTURE_LEVELS; i++) + { + texture_image_free_safe(obj, i); + } + + rspq_block_begin(); + rdpq_tex_multi_begin(); + __rdpq_sprite_upload(TILE0, sprite, texparms, false); + rdpq_tex_multi_end(); + rspq_block_t *texup_block = rspq_block_end(); + + obj->flags |= TEX_HAS_IMAGE; + obj->sprite = sprite; + obj->blocks[0] = texup_block; + + // Set tlut mode and level count + rdpq_tlut_t tlut_mode = rdpq_tlut_from_format(sprite_get_format(sprite)); + int lod_count = sprite_get_lod_count(sprite) - 1; + gl_set_short(GL_UPDATE_NONE, offset + TEXTURE_LEVELS_COUNT_OFFSET, (lod_count << 8) | tlut_mode); + + // Set min filter + GLenum min_filter = lod_count > 0 ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR; + gl_texture_set_min_filter(obj, offset, min_filter); + + // Set detail mode + surface_t detailsurf = sprite_get_detail_pixels(sprite, NULL, NULL); + bool use_detail = detailsurf.buffer != NULL; + gl_set_flag_raw(GL_UPDATE_NONE, offset + TEXTURE_FLAGS_OFFSET, TEX_FLAG_DETAIL, use_detail); + + // Mark texture as complete because sprites are complete by definition + gl_set_flag_raw(GL_UPDATE_NONE, offset + TEXTURE_FLAGS_OFFSET, TEX_FLAG_COMPLETE, true); + + gl_texture_set_upload_block(offset, 0, sprite->width, sprite->height, sprite_get_format(sprite), texup_block); +} + +void gl_surface_image(gl_texture_object_t *obj, uint32_t offset, GLint level, surface_t *surface, rdpq_texparms_t *parms) +{ + rspq_block_begin(); + rdpq_tex_multi_begin(); + rdpq_tex_upload(TILE0+level, surface, parms); + rdpq_tex_multi_end(); + rspq_block_t *texup_block = rspq_block_end(); + + obj->flags |= TEX_HAS_IMAGE; + obj->blocks[level] = texup_block; + + // FIXME: This is kind of a hack because it sets the TLUT mode for the entire texture object. + // But since all levels need to have the same format for the texture to be complete, this happens to work. + rdpq_tlut_t tlut_mode = rdpq_tlut_from_format(surface_get_format(surface)); + gl_set_byte(GL_UPDATE_NONE, offset + TEXTURE_TLUT_MODE_OFFSET, tlut_mode); + + gl_texture_set_upload_block(offset, level, surface->width, surface->height, surface_get_format(surface), texup_block); + gl_update_texture_completeness(offset); +} + +void glSurfaceTexImageN64(GLenum target, GLint level, surface_t *surface, rdpq_texparms_t *texparms) +{ + tex_format_t fmt = surface_get_format(surface); + assertf(fmt != FMT_CI4 && fmt != FMT_CI8, "CI textures are not supported by glSurfaceTexImageN64 yet"); + + gl_assert_no_display_list(); + if (!gl_ensure_no_begin_end()) return; + + if (level >= MAX_TEXTURE_LEVELS || level < 0) { + gl_set_error(GL_INVALID_VALUE, "Invalid level number (must be in [0, %d])", MAX_TEXTURE_LEVELS-1); + return; + } + + uint32_t offset = gl_texture_get_offset(target); + if (offset == 0) return; + + gl_texture_object_t *obj = gl_get_texture_object(target); + if (texture_is_sprite(obj)) { + gl_set_error(GL_INVALID_OPERATION, "Cannot apply image to a sprite texture"); + return; + } + + if (target == GL_TEXTURE_1D && surface->height != 1) { + gl_set_error(GL_INVALID_VALUE, "Surface must have height 1 when using target GL_TEXTURE_1D"); + return; + } + + rdpq_texparms_t parms; + if (texparms != NULL) { + parms = *texparms; + parms.s.scale_log = level; + parms.t.scale_log = level; + } else { + texture_get_texparms(obj, level, &parms); + } + + texture_image_free_safe(obj, level); + + // Store the surface. We duplicate the surface structure (not the pixels) + // using surface_make_sub so that we get a variant in which the owned bit + // is not set; this in turns will make sure texture deletion would not free + // the original surface (whose lifetime is left to the caller). + obj->surfaces[level] = surface_make_sub(surface, 0, 0, surface->width, surface->height); + + gl_surface_image(obj, offset, level, &obj->surfaces[level], &parms); +} + +void gl_texture_set_wrap_s(gl_texture_object_t *obj, GLenum param) +{ + switch (param) { + case GL_CLAMP: + case GL_REPEAT: + case GL_MIRRORED_REPEAT_ARB: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid wrapping mode", param); + return; + } + + if (texture_has_image(obj)) { + gl_set_error(GL_INVALID_OPERATION, "Cannot set wrapping mode on a texture that has at least one image applied"); + } + + obj->wrap_s = param; +} + +void gl_texture_set_wrap_t(gl_texture_object_t *obj, GLenum param) +{ + switch (param) { + case GL_CLAMP: + case GL_REPEAT: + case GL_MIRRORED_REPEAT_ARB: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid wrapping mode", param); + return; + } + + if (texture_has_image(obj)) { + gl_set_error(GL_INVALID_OPERATION, "Cannot set wrapping mode on a texture that has at least one image applied"); + } + + obj->wrap_t = param; +} + +void gl_texture_set_min_filter(gl_texture_object_t *obj, uint32_t offset, GLenum param) +{ + switch (param) { + case GL_NEAREST: + case GL_LINEAR: + case GL_NEAREST_MIPMAP_NEAREST: + case GL_LINEAR_MIPMAP_NEAREST: + case GL_NEAREST_MIPMAP_LINEAR: + case GL_LINEAR_MIPMAP_LINEAR: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid minification filter", param); + return; + } + + gl_set_short(GL_UPDATE_NONE, offset + offsetof(gl_srv_texture_object_t, min_filter), (uint16_t)param); + + // TODO: is this correct? + if (!texture_is_sprite(obj)) { + gl_update_texture_completeness(offset); + } +} + +void gl_texture_set_mag_filter(uint32_t offset, GLenum param) +{ + switch (param) { + case GL_NEAREST: + case GL_LINEAR: + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid magnification filter", param); + return; + } + + gl_set_short(GL_UPDATE_NONE, offset + offsetof(gl_srv_texture_object_t, mag_filter), (uint16_t)param); +} + +void glTexParameteri(GLenum target, GLenum pname, GLint param) +{ + if (!gl_ensure_no_begin_end()) return; + + uint32_t offset = gl_texture_get_offset(target); + if (offset == 0) { + return; + } + + gl_texture_object_t *obj = gl_get_texture_object(target); + + switch (pname) { + case GL_TEXTURE_WRAP_S: + gl_texture_set_wrap_s(obj, param); + break; + case GL_TEXTURE_WRAP_T: + gl_texture_set_wrap_t(obj, param); + break; + case GL_TEXTURE_MIN_FILTER: + gl_texture_set_min_filter(obj, offset, param); + break; + case GL_TEXTURE_MAG_FILTER: + gl_texture_set_mag_filter(offset, param); + break; + case GL_TEXTURE_PRIORITY: + // Ignored + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void glTexParameterf(GLenum target, GLenum pname, GLfloat param) +{ + if (!gl_ensure_no_begin_end()) return; + + uint32_t offset = gl_texture_get_offset(target); + if (offset == 0) { + return; + } + + gl_texture_object_t *obj = gl_get_texture_object(target); + + switch (pname) { + case GL_TEXTURE_WRAP_S: + gl_texture_set_wrap_s(obj, param); + break; + case GL_TEXTURE_WRAP_T: + gl_texture_set_wrap_t(obj, param); + break; + case GL_TEXTURE_MIN_FILTER: + gl_texture_set_min_filter(obj, offset, param); + break; + case GL_TEXTURE_MAG_FILTER: + gl_texture_set_mag_filter(offset, param); + break; + case GL_TEXTURE_PRIORITY: + // Ignored + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void glTexParameteriv(GLenum target, GLenum pname, const GLint *params) +{ + if (!gl_ensure_no_begin_end()) return; + + uint32_t offset = gl_texture_get_offset(target); + if (offset == 0) { + return; + } + + gl_texture_object_t *obj = gl_get_texture_object(target); + + switch (pname) { + case GL_TEXTURE_WRAP_S: + gl_texture_set_wrap_s(obj, params[0]); + break; + case GL_TEXTURE_WRAP_T: + gl_texture_set_wrap_t(obj, params[0]); + break; + case GL_TEXTURE_MIN_FILTER: + gl_texture_set_min_filter(obj, offset, params[0]); + break; + case GL_TEXTURE_MAG_FILTER: + gl_texture_set_mag_filter(offset, params[0]); + break; + case GL_TEXTURE_BORDER_COLOR: + assertf(0, "Texture border color is not supported!"); + break; + case GL_TEXTURE_PRIORITY: + // Ignored + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +void glTexParameterfv(GLenum target, GLenum pname, const GLfloat *params) +{ + if (!gl_ensure_no_begin_end()) return; + + uint32_t offset = gl_texture_get_offset(target); + if (offset == 0) { + return; + } + + gl_texture_object_t *obj = gl_get_texture_object(target); + + switch (pname) { + case GL_TEXTURE_WRAP_S: + gl_texture_set_wrap_s(obj, params[0]); + break; + case GL_TEXTURE_WRAP_T: + gl_texture_set_wrap_t(obj, params[0]); + break; + case GL_TEXTURE_MIN_FILTER: + gl_texture_set_min_filter(obj, offset, params[0]); + break; + case GL_TEXTURE_MAG_FILTER: + gl_texture_set_mag_filter(offset, params[0]); + break; + case GL_TEXTURE_BORDER_COLOR: + assertf(0, "Texture border color is not supported!"); + break; + case GL_TEXTURE_PRIORITY: + // Ignored + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid parameter name for this function", pname); + return; + } +} + +GLboolean glIsTexture(GLuint texture) +{ + if (!gl_ensure_no_begin_end()) return 0; + + // FIXME: This doesn't actually guarantee that it's a valid texture object, but just uses the heuristic of + // "is it somewhere in the heap memory?". This way we can at least rule out arbitrarily chosen integer constants, + // which used to be valid texture IDs in legacy OpenGL. + return is_valid_object_id(texture); +} + +void glBindTexture(GLenum target, GLuint texture) +{ + if (!gl_ensure_no_begin_end()) return; + assertf(texture == 0 || is_valid_object_id(texture), + "Not a valid texture object: %#lx. Make sure to allocate IDs via glGenTextures", texture); + + gl_texture_object_t **target_obj = NULL; + + switch (target) { + case GL_TEXTURE_1D: + target_obj = &state->texture_1d_object; + break; + case GL_TEXTURE_2D: + target_obj = &state->texture_2d_object; + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid texture target", target); + return; + } + + if (texture == 0) { + switch (target) { + case GL_TEXTURE_1D: + *target_obj = &state->default_textures[0]; + break; + case GL_TEXTURE_2D: + *target_obj = &state->default_textures[1]; + break; + } + } else { + gl_texture_object_t *obj = (gl_texture_object_t*)texture; + + if (obj->dimensionality == 0) { + obj->dimensionality = target; + } + + if (obj->dimensionality != target) { + gl_set_error(GL_INVALID_OPERATION, "Texture object has already been bound to another texture target"); + return; + } + + *target_obj = obj; + } + + gl_bind_texture(target, *target_obj); +} + +void glGenTextures(GLsizei n, GLuint *textures) +{ + if (!gl_ensure_no_begin_end()) return; + + for (uint32_t i = 0; i < n; i++) + { + gl_texture_object_t *new_object = malloc_uncached(sizeof(gl_texture_object_t)); + assertf(new_object, "Out of memory"); + gl_init_texture_object(new_object); + textures[i] = (GLuint)new_object; + } +} + +void texture_free(gl_texture_object_t* obj) +{ + gl_cleanup_texture_object(obj); + free_uncached(obj); +} + +void glDeleteTextures(GLsizei n, const GLuint *textures) +{ + if (!gl_ensure_no_begin_end()) return; + + for (uint32_t i = 0; i < n; i++) + { + assertf(textures[i] == 0 || is_valid_object_id(textures[i]), + "Not a valid texture object: %#lx. Make sure to allocate IDs via glGenTextures", textures[i]); + + gl_texture_object_t *obj = (gl_texture_object_t*)textures[i]; + if (obj == NULL) { + continue; + } + + if (obj == state->texture_1d_object) { + glBindTexture(GL_TEXTURE_1D, 0); + } else if (obj == state->texture_2d_object) { + glBindTexture(GL_TEXTURE_2D, 0); + } + + rdpq_call_deferred((void (*)(void*))texture_free, obj); + } +} + +uint32_t gl_get_format_element_count(GLenum format) +{ + switch (format) { + case GL_RED: + case GL_GREEN: + case GL_BLUE: + case GL_ALPHA: + case GL_LUMINANCE: + return 1; + case GL_LUMINANCE_ALPHA: + return 2; + case GL_RGB: + return 3; + case GL_RGBA: + return 4; + case GL_COLOR_INDEX: + assertf(0, "Color index format is not supported!"); + return 0; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid pixel data format", format); + return 0; + } +} + +GLint gl_choose_internalformat(GLint requested) +{ + switch (requested) { + case 1: + case GL_LUMINANCE: + case GL_LUMINANCE4: + case GL_LUMINANCE8: + case GL_LUMINANCE12: + case GL_LUMINANCE16: + assertf(0, "Luminance-only textures are not supported!"); + return -1; + + case GL_ALPHA: + case GL_ALPHA4: + case GL_ALPHA8: + case GL_ALPHA12: + case GL_ALPHA16: + assertf(0, "Alpha-only textures are not supported!"); + return -1; + + case GL_INTENSITY4: + return GL_INTENSITY4; + + case GL_INTENSITY: + case GL_INTENSITY8: + case GL_INTENSITY12: + case GL_INTENSITY16: + return GL_INTENSITY8; + + case 2: + case GL_LUMINANCE4_ALPHA4: + case GL_LUMINANCE6_ALPHA2: + return GL_LUMINANCE4_ALPHA4; + + case GL_LUMINANCE_ALPHA: + case GL_LUMINANCE8_ALPHA8: + case GL_LUMINANCE12_ALPHA4: + case GL_LUMINANCE12_ALPHA12: + case GL_LUMINANCE16_ALPHA16: + return GL_LUMINANCE8_ALPHA8; + + case 3: + case 4: + case GL_RGB: + case GL_R3_G3_B2: + case GL_RGB4: + case GL_RGB5: + case GL_RGBA: + case GL_RGBA2: + case GL_RGBA4: + case GL_RGB5_A1: + return GL_RGB5_A1; + + case GL_RGB8: + case GL_RGB10: + case GL_RGB12: + case GL_RGB16: + case GL_RGBA8: + case GL_RGB10_A2: + case GL_RGBA12: + case GL_RGBA16: + return GL_RGBA8; + + default: + abort(); + } +} + +#define BYTE_SWAP_16(x) ((((x)&0xFF)<<8) | (((x)&0xFF00)>>8)) +#define BYTE_SWAP_32(x) ((((x)&0xFF)<<24) | (((x)&0xFF00)<<8) | (((x)&0xFF0000)>>8) | (((x)&0xFF000000)>>24)) + +#define COND_BYTE_SWAP_16(x, c) ((c) ? BYTE_SWAP_16(x) : (x)) +#define COND_BYTE_SWAP_32(x, c) ((c) ? BYTE_SWAP_32(x) : (x)) + +void gl_unpack_pixel_byte(GLfloat *result, uint32_t num_elements, bool swap, const GLvoid *data) +{ + for (uint32_t i = 0; i < num_elements; i++) + { + result[i] = I8_TO_FLOAT(((const GLbyte*)data)[i]); + } +} + +void gl_unpack_pixel_ubyte(GLfloat *result, uint32_t num_elements, bool swap, const GLvoid *data) +{ + for (uint32_t i = 0; i < num_elements; i++) + { + result[i] = U8_TO_FLOAT(((const GLubyte*)data)[i]); + } +} + +void gl_unpack_pixel_short(GLfloat *result, uint32_t num_elements, bool swap, const GLvoid *data) +{ + for (uint32_t i = 0; i < num_elements; i++) + { + result[i] = I16_TO_FLOAT(COND_BYTE_SWAP_16(((const GLshort*)data)[i], swap)); + } +} + +void gl_unpack_pixel_ushort(GLfloat *result, uint32_t num_elements, bool swap, const GLvoid *data) +{ + for (uint32_t i = 0; i < num_elements; i++) + { + result[i] = U16_TO_FLOAT(COND_BYTE_SWAP_16(((const GLushort*)data)[i], swap)); + } +} + +void gl_unpack_pixel_int(GLfloat *result, uint32_t num_elements, bool swap, const GLvoid *data) +{ + for (uint32_t i = 0; i < num_elements; i++) + { + result[i] = I32_TO_FLOAT(COND_BYTE_SWAP_32(((const GLint*)data)[i], swap)); + } +} + +void gl_unpack_pixel_uint(GLfloat *result, uint32_t num_elements, bool swap, const GLvoid *data) +{ + for (uint32_t i = 0; i < num_elements; i++) + { + result[i] = U32_TO_FLOAT(COND_BYTE_SWAP_32(((const GLuint*)data)[i], swap)); + } +} + +void gl_unpack_pixel_float(GLfloat *result, uint32_t num_elements, bool swap, const GLvoid *data) +{ + for (uint32_t i = 0; i < num_elements; i++) + { + result[i] = ((const GLfloat*)data)[i]; + } +} + +void gl_unpack_pixel_ubyte_3_3_2(GLfloat *result, uint32_t num_elements, bool swap, const GLvoid *data) +{ + GLubyte value = *(const GLubyte*)data; + result[0] = (value>>5) / (float)(0x7); + result[1] = ((value>>2)&0x7) / (float)(0x7); + result[2] = (value&0x3) / (float)(0x3); +} + +void gl_unpack_pixel_ushort_4_4_4_4(GLfloat *result, uint32_t num_elements, bool swap, const GLvoid *data) +{ + GLushort value = COND_BYTE_SWAP_16(*(const GLushort*)data, swap); + result[0] = (value>>12) / (float)(0xF); + result[1] = ((value>>8)&0xF) / (float)(0xF); + result[2] = ((value>>4)&0xF) / (float)(0xF); + result[3] = (value&0xF) / (float)(0xF); +} + +void gl_unpack_pixel_ushort_5_5_5_1(GLfloat *result, uint32_t num_elements, bool swap, const GLvoid *data) +{ + GLushort value = COND_BYTE_SWAP_16(*(const GLushort*)data, swap); + result[0] = (value>>11) / (float)(0x1F); + result[1] = ((value>>6)&0x1F) / (float)(0x1F); + result[2] = ((value>>1)&0x1F) / (float)(0x1F); + result[3] = value & 0x1; +} + +void gl_unpack_pixel_uint_8_8_8_8(GLfloat *result, uint32_t num_elements, bool swap, const GLvoid *data) +{ + GLuint value = COND_BYTE_SWAP_32(*(const GLuint*)data, swap); + result[0] = U8_TO_FLOAT((value>>24)); + result[1] = U8_TO_FLOAT((value>>16)&0xFF); + result[2] = U8_TO_FLOAT((value>>8)&0xFF); + result[3] = U8_TO_FLOAT(value&0xFF); +} + +void gl_unpack_pixel_uint_10_10_10_2(GLfloat *result, uint32_t num_elements, bool swap, const GLvoid *data) +{ + GLuint value = COND_BYTE_SWAP_32(*(const GLuint*)data, swap); + result[0] = (value>>22) / (float)(0x3FF); + result[1] = ((value>>12)&0x3FF) / (float)(0x3FF); + result[2] = ((value>>2)&0x3FF) / (float)(0x3FF); + result[3] = (value & 0x3) / (float)(0x3); +} + +void gl_pack_pixel_rgb5a1(GLvoid *dest, uint32_t x, const GLfloat *components) +{ + *((GLushort*)dest) = ((GLushort)roundf(components[0]*0x1F) << 11) | + ((GLushort)roundf(components[1]*0x1F) << 6) | + ((GLushort)roundf(components[2]*0x1F) << 1) | + ((GLushort)roundf(components[3])); +} + +void gl_pack_pixel_rgba8(GLvoid *dest, uint32_t x, const GLfloat *components) +{ + *((GLuint*)dest) = ((GLuint)roundf(components[0]*0xFF) << 24) | + ((GLuint)roundf(components[1]*0xFF) << 16) | + ((GLuint)roundf(components[2]*0xFF) << 8) | + ((GLuint)roundf(components[3]*0xFF)); +} + +void gl_pack_pixel_luminance4_alpha4(GLvoid *dest, uint32_t x, const GLfloat *components) +{ + *((GLubyte*)dest) = ((GLubyte)roundf(components[0]*0xF) << 4) | + ((GLubyte)roundf(components[3]*0xF)); +} + +void gl_pack_pixel_luminance8_alpha8(GLvoid *dest, uint32_t x, const GLfloat *components) +{ + *((GLushort*)dest) = ((GLushort)roundf(components[0]*0xFF) << 8) | + ((GLushort)roundf(components[3]*0xFF)); +} + +void gl_pack_pixel_intensity4(GLvoid *dest, uint32_t x, const GLfloat *components) +{ + GLubyte c = (GLubyte)roundf(components[0]*0xF); + + if (x & 1) { + *((GLubyte*)dest) = (*((GLubyte*)dest) & 0xF0) | c; + } else { + *((GLubyte*)dest) = (*((GLubyte*)dest) & 0xF) | (c << 4); + } +} + +void gl_pack_pixel_intensity8(GLvoid *dest, uint32_t x, const GLfloat *components) +{ + *((GLubyte*)dest) = (GLubyte)roundf(components[0]*0xFF); +} + +bool gl_do_formats_match(GLint dst_fmt, GLenum src_fmt, GLenum src_type) +{ + switch (dst_fmt) { + case GL_RGB5_A1: + if (src_fmt == GL_RGBA && src_type == GL_UNSIGNED_SHORT_5_5_5_1_EXT) { + return true; + } + break; + case GL_RGBA8: + if (src_fmt == GL_RGBA && (src_type == GL_UNSIGNED_BYTE || src_type == GL_BYTE || src_type == GL_UNSIGNED_INT_8_8_8_8_EXT)) { + return true; + } + break; + case GL_LUMINANCE8_ALPHA8: + if (src_fmt == GL_LUMINANCE_ALPHA && (src_type == GL_UNSIGNED_BYTE || src_type == GL_BYTE)) { + return true; + } + break; + case GL_INTENSITY8: + if ((src_fmt == GL_LUMINANCE || src_fmt == GL_INTENSITY || src_fmt == GL_RED) && (src_type == GL_UNSIGNED_BYTE || src_type == GL_BYTE)) { + return true; + } + break; + } + + return false; +} + +void gl_transfer_pixels(GLvoid *dest, GLenum dest_format, GLsizei dest_stride, GLsizei width, GLsizei height, uint32_t num_elements, GLenum format, GLenum type, uint32_t xoffset, const GLvoid *data) +{ + uint32_t src_pixel_size; + void (*unpack_func)(GLfloat*,uint32_t,bool,const GLvoid*); + void (*pack_func)(GLvoid*,uint32_t,const GLfloat*); + + switch (type) { + case GL_BYTE: + src_pixel_size = sizeof(GLbyte) * num_elements; + unpack_func = gl_unpack_pixel_byte; + break; + case GL_UNSIGNED_BYTE: + src_pixel_size = sizeof(GLubyte) * num_elements; + unpack_func = gl_unpack_pixel_ubyte; + break; + case GL_SHORT: + src_pixel_size = sizeof(GLshort) * num_elements; + unpack_func = gl_unpack_pixel_short; + break; + case GL_UNSIGNED_SHORT: + src_pixel_size = sizeof(GLushort) * num_elements; + unpack_func = gl_unpack_pixel_ushort; + break; + case GL_INT: + src_pixel_size = sizeof(GLint) * num_elements; + unpack_func = gl_unpack_pixel_int; + break; + case GL_UNSIGNED_INT: + src_pixel_size = sizeof(GLuint) * num_elements; + unpack_func = gl_unpack_pixel_uint; + break; + case GL_FLOAT: + src_pixel_size = sizeof(GLfloat) * num_elements; + unpack_func = gl_unpack_pixel_float; + break; + case GL_UNSIGNED_BYTE_3_3_2_EXT: + src_pixel_size = sizeof(GLubyte); + unpack_func = gl_unpack_pixel_ubyte_3_3_2; + break; + case GL_UNSIGNED_SHORT_4_4_4_4_EXT: + src_pixel_size = sizeof(GLushort); + unpack_func = gl_unpack_pixel_ushort_4_4_4_4; + break; + case GL_UNSIGNED_SHORT_5_5_5_1_EXT: + src_pixel_size = sizeof(GLushort); + unpack_func = gl_unpack_pixel_ushort_5_5_5_1; + break; + case GL_UNSIGNED_INT_8_8_8_8_EXT: + src_pixel_size = sizeof(GLuint); + unpack_func = gl_unpack_pixel_uint_8_8_8_8; + break; + case GL_UNSIGNED_INT_10_10_10_2_EXT: + src_pixel_size = sizeof(GLuint); + unpack_func = gl_unpack_pixel_uint_10_10_10_2; + break; + default: + assertf(0, "Invalid type"); + abort(); + } + + switch (dest_format) { + case GL_RGB5_A1: + pack_func = gl_pack_pixel_rgb5a1; + break; + case GL_RGBA8: + pack_func = gl_pack_pixel_rgba8; + break; + case GL_LUMINANCE4_ALPHA4: + pack_func = gl_pack_pixel_luminance4_alpha4; + break; + case GL_LUMINANCE8_ALPHA8: + pack_func = gl_pack_pixel_luminance8_alpha8; + break; + case GL_INTENSITY4: + pack_func = gl_pack_pixel_intensity4; + break; + case GL_INTENSITY8: + pack_func = gl_pack_pixel_intensity8; + break; + default: + assertf(0, "Unsupported destination format!"); + abort(); + } + + tex_format_t dest_tex_fmt = gl_tex_format_to_rdp(dest_format); + + uint32_t row_length = state->unpack_row_length > 0 ? state->unpack_row_length : width; + + uint32_t src_stride = ROUND_UP(row_length * src_pixel_size, state->unpack_alignment); + + const GLvoid *src_ptr = data + src_stride * state->unpack_skip_rows + src_pixel_size * state->unpack_skip_pixels; + GLvoid *dest_ptr = dest; + + uint32_t component_offset = 0; + switch (format) { + case GL_GREEN: + component_offset = 1; + break; + case GL_BLUE: + component_offset = 2; + break; + case GL_ALPHA: + component_offset = 3; + break; + } + + bool formats_match = gl_do_formats_match(dest_format, format, type); + bool can_mempcy = formats_match && state->transfer_is_noop; + + for (uint32_t r = 0; r < height; r++) + { + if (can_mempcy) { + memcpy(dest_ptr + TEX_FORMAT_PIX2BYTES(dest_tex_fmt, xoffset), src_ptr, TEX_FORMAT_PIX2BYTES(dest_tex_fmt, width)); + } else { + for (uint32_t c = 0; c < width; c++) + { + GLfloat components[4] = { 0, 0, 0, 1 }; + unpack_func(&components[component_offset], num_elements, state->unpack_swap_bytes, src_ptr + c * src_pixel_size); + + if (format == GL_LUMINANCE) { + components[2] = components[1] = components[0]; + } else if (format == GL_LUMINANCE_ALPHA) { + components[3] = components[1]; + components[2] = components[1] = components[0]; + } + + for (uint32_t i = 0; i < 4; i++) + { + components[i] = CLAMP01(components[i] * state->transfer_scale[i] + state->transfer_bias[i]); + } + + if (state->map_color) { + for (uint32_t i = 0; i < 4; i++) + { + uint32_t index = floorf(components[i]) * (state->pixel_maps[i].size - 1); + components[i] = CLAMP01(state->pixel_maps[i].entries[index]); + } + } + + uint32_t x = xoffset + c; + pack_func(dest_ptr + TEX_FORMAT_PIX2BYTES(dest_tex_fmt, x), x, components); + } + } + + src_ptr += src_stride; + dest_ptr += dest_stride; + } +} + +gl_texture_image_t * gl_get_texture_image(gl_texture_object_t *obj, GLint level) +{ + if (level < 0 || level > MAX_TEXTURE_LEVELS) { + gl_set_error(GL_INVALID_VALUE, "%ld is not a valid texture image level (Must be in [0, %d])", level, MAX_TEXTURE_LEVELS); + return NULL; + } + + return &obj->srv_object->levels[level]; +} + +bool gl_get_texture_object_and_image(GLenum target, GLint level, gl_texture_object_t **obj, gl_texture_image_t **image) +{ + gl_texture_object_t *tmp_obj = gl_get_texture_object(target); + if (tmp_obj == NULL) { + return false; + } + + gl_texture_image_t *tmp_img = gl_get_texture_image(tmp_obj, level); + if (tmp_img == NULL) { + return false; + } + + if (obj != NULL) { + *obj = tmp_obj; + } + + if (image != NULL) { + *image = tmp_img; + } + + return true; +} + +bool gl_validate_upload_image(GLenum format, GLenum type, uint32_t *num_elements) +{ + *num_elements = gl_get_format_element_count(format); + if (*num_elements == 0) { + return false; + } + + switch (type) { + case GL_UNSIGNED_BYTE: + case GL_BYTE: + case GL_UNSIGNED_SHORT: + case GL_SHORT: + case GL_UNSIGNED_INT: + case GL_INT: + case GL_FLOAT: + break; + case GL_UNSIGNED_BYTE_3_3_2_EXT: + if (*num_elements != 3) { + gl_set_error(GL_INVALID_OPERATION, "GL_UNSIGNED_BYTE_3_3_2_EXT must be used with GL_RGB"); + return false; + } + break; + case GL_UNSIGNED_SHORT_4_4_4_4_EXT: + if (*num_elements != 4) { + gl_set_error(GL_INVALID_OPERATION, "GL_UNSIGNED_SHORT_4_4_4_4_EXT must be used with GL_RGBA"); + return false; + } + case GL_UNSIGNED_SHORT_5_5_5_1_EXT: + if (*num_elements != 4) { + gl_set_error(GL_INVALID_OPERATION, "GL_UNSIGNED_SHORT_5_5_5_1_EXT must be used with GL_RGBA"); + return false; + } + case GL_UNSIGNED_INT_8_8_8_8_EXT: + if (*num_elements != 4) { + gl_set_error(GL_INVALID_OPERATION, "GL_UNSIGNED_INT_8_8_8_8_EXT must be used with GL_RGBA"); + return false; + } + case GL_UNSIGNED_INT_10_10_10_2_EXT: + if (*num_elements != 4) { + gl_set_error(GL_INVALID_OPERATION, "GL_UNSIGNED_INT_10_10_10_2_EXT must be used with GL_RGBA"); + return false; + } + break; + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid pixel data type", type); + return false; + } + + return true; +} + +static inline float wrap_mode_to_repeats(GLenum wrap_mode) +{ + switch (wrap_mode) { + case GL_REPEAT: + case GL_MIRRORED_REPEAT_ARB: + return REPEAT_INFINITE; + case GL_CLAMP: + default: + return 0; + } +} + +static inline void texture_get_texparms(gl_texture_object_t *obj, GLint level, rdpq_texparms_t *parms) +{ + *parms = (rdpq_texparms_t){ + .s.scale_log = level, + .t.scale_log = level, + .s.mirror = obj->wrap_s == GL_MIRRORED_REPEAT_ARB, + .t.mirror = obj->wrap_t == GL_MIRRORED_REPEAT_ARB, + .s.repeats = wrap_mode_to_repeats(obj->wrap_s), + .t.repeats = wrap_mode_to_repeats(obj->wrap_t), + }; +} + +void gl_tex_image(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *data) +{ + assertf(border == 0, "Texture border is not supported!"); + if (level >= MAX_TEXTURE_LEVELS || level < 0) { + gl_set_error(GL_INVALID_VALUE, "Invalid level number (must be in [0, %d])", MAX_TEXTURE_LEVELS-1); + return; + } + + uint32_t offset = gl_texture_get_offset(target); + if (offset == 0) return; + + gl_texture_object_t *obj = gl_get_texture_object(target); + if (texture_is_sprite(obj)) { + gl_set_error(GL_INVALID_OPERATION, "Cannot apply image to a sprite texture"); + return; + } + + GLint preferred_format = gl_choose_internalformat(internalformat); + if (preferred_format < 0) { + gl_set_error(GL_INVALID_VALUE, "Internal format %#04lx is not supported", internalformat); + return; + } + + uint32_t num_elements; + if (!gl_validate_upload_image(format, type, &num_elements)) { + return; + } + + texture_image_free_safe(obj, level); + + surface_t *surface = &obj->surfaces[level]; + + uint32_t rdp_format = gl_tex_format_to_rdp(preferred_format); + *surface = surface_alloc(rdp_format, width, height); + if (surface->buffer == NULL) { + gl_set_error(GL_OUT_OF_MEMORY, "Failed to allocate texture image"); + return; + } + + if (data != NULL) { + gl_transfer_pixels(surface->buffer, preferred_format, surface->stride, width, height, num_elements, format, type, 0, data); + } + + rdpq_texparms_t parms; + texture_get_texparms(obj, level, &parms); + gl_surface_image(obj, offset, level, surface, &parms); +} + +void glTexImage1D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLint border, GLenum format, GLenum type, const GLvoid *data) +{ + gl_assert_no_display_list(); + if (!gl_ensure_no_begin_end()) return; + + switch (target) { + case GL_TEXTURE_1D: + break; + case GL_PROXY_TEXTURE_1D: + assertf(0, "Proxy texture targets are not supported!"); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid target for glTexImage1D", target); + return; + } + + gl_tex_image(target, level, internalformat, width, 1, border, format, type, data); +} + +void glTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *data) +{ + gl_assert_no_display_list(); + if (!gl_ensure_no_begin_end()) return; + + switch (target) { + case GL_TEXTURE_2D: + break; + case GL_PROXY_TEXTURE_2D: + assertf(0, "Proxy texture targets are not supported!"); + break; + default: + gl_set_error(GL_INVALID_ENUM, "%#04lx is not a valid target for glTexImage2D", target); + return; + } + + gl_tex_image(target, level, internalformat, width, height, border, format, type, data); +} + +GLboolean glAreTexturesResident(GLsizei n, const GLuint *textures, const GLboolean *residences) +{ + return GL_FALSE; +} + +void glPrioritizeTextures(GLsizei n, const GLuint *textures, const GLclampf *priorities) +{ + // Priorities are ignored +} diff --git a/src/a3d.c b/src/a3d.c new file mode 100644 index 000000000..3e6bc82fc --- /dev/null +++ b/src/a3d.c @@ -0,0 +1,13 @@ +/** + * @file a3d.c + * @author Giovanni Bajo + * @brief Analogue3D support + */ + +#include "a3d.h" +#include "mi.h" + +bool a3d_detect(void) +{ + return (*MI_VERSION & 0xFF) == 0x3; +} diff --git a/src/accounting.c b/src/accounting.c new file mode 100644 index 000000000..90faf9208 --- /dev/null +++ b/src/accounting.c @@ -0,0 +1,42 @@ +/** + * @file accounting.c + * @author Giovanni Bajo + * @brief Internal bucket-based time accounting (see accounting_internal.h) + */ +#include "accounting_internal.h" +#include "n64sys.h" +#include "cop0.h" +#include "debug.h" +#include "interrupt_internal.h" + +static int8_t __acct_current_category = ACCT_CAT_USER; +static uint32_t __acct_last_tick = 0; +static uint64_t __acct_ticks[ACCT_CAT_MAX] = {0}; +/** @brief Accumulated system ticks (time spent outside user code) */ +uint64_t __acct_system_ticks = 0; + +/** + * @brief Enter a category (IRQ-safe version, briefly disables interrupts). + */ +acct_category_t acct_switch(acct_category_t new_cat) +{ + uint32_t sr = __disable_interrupts(); + acct_category_t prev_cat = __acct_current_category; + if (prev_cat != new_cat) { + uint32_t now = TICKS_READ(); + uint32_t elapsed = TICKS_DISTANCE(__acct_last_tick, now); + __acct_ticks[prev_cat] += elapsed; + __acct_last_tick = now; + if (prev_cat > 0) + __acct_system_ticks += elapsed; + __acct_current_category = new_cat; + } + __enable_interrupts(sr); + return prev_cat; +} + +uint64_t acct_get_ticks(acct_category_t cat) +{ + assertf(cat < ACCT_CAT_MAX, "invalid category: %d", cat); + return __acct_ticks[cat]; +} diff --git a/src/accounting_internal.h b/src/accounting_internal.h new file mode 100644 index 000000000..d0371e025 --- /dev/null +++ b/src/accounting_internal.h @@ -0,0 +1,76 @@ +/** + * @file accounting_internal.h + * @author Giovanni Bajo + * @brief Internal time accounting helpers (not part of the public API) + * + * This module implements a very low-overhead bucket-based "CPU/system time" + * accounting using the COP0 count register (C0_COUNT). + * + * Key property: at any instant, time is attributed to exactly one bucket + * (the "current category"). Interrupt handler switches category to IRQ and + * restores it on exit, preventing double accounting (eg: IRQ time inside a + * busy-wait loop is not counted twice). + */ +#ifndef LIBDRAGON_ACCOUNTING_INTERNAL_H +#define LIBDRAGON_ACCOUNTING_INTERNAL_H + +#include + +/** @brief Accounting categories */ +typedef enum { + ACCT_CAT_USER = 0, ///< User time. This is the default category at boot. + ACCT_CAT_IRQ = 1, ///< IRQ time + ACCT_CAT_RSP = 2, ///< RSP time (#rsp_wait) + ACCT_CAT_DISPLAY = 3, ///< Display time (#display_get) + ACCT_CAT_RSPQ = 4, ///< RSPQ time (various spinwaits) + ACCT_CAT_VI = 5, ///< VI time (#vi_wait_vblank) + ACCT_CAT_JOYBUS = 6, ///< Joybus time (#joybus_exec) + ACCT_CAT_PI = 7, ///< PI wait time (DMA and I/O) + + /** Keep this last. Buckets are addressed by index. */ + ACCT_CAT_MAX = 8, +} acct_category_t; + +/** + * @brief Switch to a new accounting category. + * + * This function switches the current accounting category to the specified + * one, returning the previous category. + * + * From this moment on, clock time will be attributed to the new category. + * Normally, most code will want to use #ACCT_SCOPE macro instead, which + * works as a RAII-like scope guard. + * + * @param new_cat New category to switch to + * @return Previous category + */ +acct_category_t acct_switch(acct_category_t new_cat); + +/** + * @brief Create a scope that attributes time to a specific category. + * + * This macro creates a scope in which time is attributed to the specified + * category, until the scope is exited. For instance: + * + * \code{.c} + * ACCT_SCOPE(ACCT_CAT_VI) { + * // time spent here is attributed to ACCT_CAT_VI + * } + * \endcode + * + * Internally, this macro uses #acct_switch to change category on scope entry, + * and restores the previous category on scope exit. + * + * \hideinitializer + */ +#define ACCT_SCOPE(cat) \ + for (uint8_t __acct_prev = acct_switch((uint8_t)(cat)), __acct_once = 0; \ + __acct_once == 0; \ + __acct_once = 1, acct_switch(__acct_prev)) + +/** + * @brief Read the accumulated ticks for a category. + */ +uint64_t acct_get_ticks(acct_category_t cat); + +#endif diff --git a/src/asset.c b/src/asset.c index 20148e974..9adde28b9 100644 --- a/src/asset.c +++ b/src/asset.c @@ -4,9 +4,11 @@ */ #include "asset.h" #include "asset_internal.h" +#include "debug.h" #include "compress/aplib_dec_internal.h" #include "compress/lz4_dec_internal.h" #include "compress/shrinkler_dec_internal.h" +#include "utils.h" #include #include #include @@ -32,7 +34,6 @@ #include /// @cond #define memalign(a, b) malloc(b) -#define assertf(x, ...) assert(x) /// @endcond #endif @@ -42,41 +43,53 @@ * Only level 1 (LZ4) is always initialized. The other algorithm (LZH5) * must be initialized manually via #asset_init_compression. */ -static asset_compression_t algos[3] = { - { +__attribute__((used)) +static asset_compression_full_t algos_full[3] = { +{ + #ifdef N64 + .decompress_full = decompress_lz4_full_inplace, + #endif + }, +}; + +static asset_compression_stream_t algos_stream[3] = { +{ .state_size = DECOMPRESS_LZ4_STATE_SIZE, .decompress_init = decompress_lz4_init, .decompress_read = decompress_lz4_read, .decompress_reset = decompress_lz4_reset, - .decompress_full_inplace = decompress_lz4_full_inplace, - } + }, }; /** @brief Initialize compression level 2 (APLIB) */ void __asset_init_compression_lvl2(void) { - algos[1] = (asset_compression_t){ + #ifdef N64 + algos_full[1] = (asset_compression_full_t){ + .decompress_full = decompress_aplib_full_inplace, + }; + #endif + algos_stream[1] = (asset_compression_stream_t){ .state_size = DECOMPRESS_APLIB_STATE_SIZE, .decompress_init = decompress_aplib_init, .decompress_read = decompress_aplib_read, .decompress_reset = decompress_aplib_reset, - #if DECOMPRESS_APLIB_FULL_USE_ASM - .decompress_full_inplace = decompress_aplib_full_inplace, - #else - .decompress_full = decompress_aplib_full, - #endif }; } /** @brief Initialize compression level 3 (SHRINKLER) */ void __asset_init_compression_lvl3(void) { - algos[2] = (asset_compression_t){ - #if DECOMPRESS_SHRINKLER_FULL_USE_ASM - .decompress_full_inplace = decompress_shrinkler_full_inplace, - #else - .decompress_full = decompress_shrinkler_full, - #endif + #ifdef N64 + algos_full[2] = (asset_compression_full_t){ + .decompress_full = decompress_shrinkler_full_inplace, + }; + #endif + algos_stream[2] = (asset_compression_stream_t){ + .state_size = DECOMPRESS_SHRINKLER_STATE_SIZE, + .decompress_init = decompress_shrinkler_init, + .decompress_read = decompress_shrinkler_read, + .decompress_reset = decompress_shrinkler_reset, }; } @@ -111,34 +124,51 @@ FILE *must_fopen(const char *fn) return fdopen(must_open(fn), "rb"); } -static void* decompress_inplace(asset_compression_t *algo, const char *fn, int fd, size_t cmp_size, size_t size, int margin) +__attribute__((used)) +static bool decompress_full_stream(asset_compression_stream_t *algo, int fd, uint16_t flags, size_t size, void *buf, int *buf_size) +{ + if (buf == NULL || *buf_size < (int)size) { + *buf_size = (int)size; + return false; + } + #ifdef N64 + assertf(((uintptr_t)(buf) & (ASSET_ALIGNMENT_MIN-1)) == 0, "Asset buffer incorrectly aligned."); + #endif + + assertf(algo->decompress_init && algo->decompress_read, + "asset: compression level does not support streaming decompression"); + + int winsize = asset_winsize_from_flags(flags); + uint8_t *state = malloc(algo->state_size + winsize); + assertf(state, "Out of memory"); + algo->decompress_init(state, fd, winsize); + int n = algo->decompress_read(state, buf, size); + assertf(n == size, "asset: decompression error: corrupted? (%d/%d)", n, (int)size); + free(state); + return true; +} + +__attribute__((used)) +static bool decompress_full(asset_compression_full_t *algo, int fd, size_t cmp_size, size_t size, int margin, void *buf, int *buf_size) { // Consistency check on input data assert(margin >= 0); - - // add 8 because the assembly decompressors do writes up to 8 bytes out-of-bounds, - // that could overwrite the input data. - margin += 8; - - int bufsize = size + margin; - int cmp_offset = bufsize - cmp_size; - // Align the source buffer to 4 bytes, so that we can use 32-bit loads (required by shrinkler). - // Notice that we need at least 2-byte alignment anyway, for DMA. - while (cmp_offset & 3) { - cmp_offset++; - bufsize++; - } - if (bufsize & 15) { - // In case we need to call invalidate (see below), we need an aligned buffer - bufsize += 16 - (bufsize & 15); + int cmp_offset; + int bufsize = asset_buf_size(size, cmp_size, margin, &cmp_offset); + if(buf == NULL || *buf_size < bufsize) { + *buf_size = bufsize; + return false; + } else { + #ifdef N64 + assertf(((uintptr_t)(buf) & (ASSET_ALIGNMENT_MIN-1)) == 0, "Asset buffer incorrectly aligned."); + #endif } - - void *s = memalign(ASSET_ALIGNMENT, bufsize); - assertf(s, "asset_load: out of memory"); + void *s = buf; int n; #ifdef N64 - if (fn && strncmp(fn, "rom:/", 5) == 0) { + pi_addr_t rom_addr = 0; + if (ioctl(fd, IODFS_GET_ROM_BASE, &rom_addr) >= 0) { // Invalid the portion of the buffer where we are going to load // the compressed data. This is needed in case the buffer returned // by memalign happens to be in cached already. @@ -148,11 +178,13 @@ static void* decompress_inplace(asset_compression_t *algo, const char *fn, int f // Loading from ROM. This is a common enough situation that we want to optimize it. // Start an asynchronous DMA transfer, so that we can start decompressing as the // data flows in. - uint32_t addr = dfs_rom_addr(fn+5) & 0x1FFFFFFF; - dma_read_async(s+cmp_offset, addr+sizeof(asset_header_t), cmp_size); + pi_addr_t addr = rom_addr+lseek(fd, 0, SEEK_CUR); + assertf(addr % 2 == 0, "asset_load requires ROM data to be 2-byte aligned"); + uint64_t ticket = dma_read_async(s+cmp_offset, addr, cmp_size); + dma_wait_started(ticket); // Run the decompression racing with the DMA. - n = algo->decompress_full_inplace(s+cmp_offset, cmp_size, s, size); (void)n; + n = algo->decompress_full(s+cmp_offset, cmp_size, s, size); (void)n; #else if (false) { #endif @@ -161,68 +193,143 @@ static void* decompress_inplace(asset_compression_t *algo, const char *fn, int f read(fd, s+cmp_offset, cmp_size); // Run the decompression. - n = algo->decompress_full_inplace(s+cmp_offset, cmp_size, s, size); (void)n; + n = algo->decompress_full(s+cmp_offset, cmp_size, s, size); (void)n; } - assertf(n == size, "asset: decompression error on file %s: corrupted? (%d/%d)", fn, n, size); - void *ptr = realloc(s, size); (void)ptr; - assertf(s == ptr, "asset: realloc moved the buffer"); // guaranteed by newlib - return ptr; + assertf(n == size, "asset: decompression error: corrupted? (%d/%d)", n, (int)size); + return true; } -void *asset_load(const char *fn, int *sz) +static int asset_read_header(int fd, asset_parsed_header_t *header, int *sz) { - uint8_t *s; int size; - int fd = must_open(fn); - - // Check if file is compressed - asset_header_t header; - read(fd, &header, sizeof(asset_header_t)); - if (!memcmp(header.magic, ASSET_MAGIC, 3)) { - if (header.version != '3') { - assertf(0, "unsupported asset version: %c\nMake sure to rebuild libdragon tools and your assets", header.version); - return NULL; + int rdhead = read(fd, &header->base, sizeof(asset_header_t)); + + if (memcmp(header->base.magic, ASSET_MAGIC, 3) == 0) { + if (header->base.version != '5') { + assertf(0, "unsupported asset version: %c\nMake sure to rebuild libdragon tools and your assets", header->base.version); } - #ifndef N64 - header.algo = __builtin_bswap16(header.algo); - header.flags = __builtin_bswap16(header.flags); - header.cmp_size = __builtin_bswap32(header.cmp_size); - header.orig_size = __builtin_bswap32(header.orig_size); - header.inplace_margin = __builtin_bswap32(header.inplace_margin); + const uint8_t *ptr = header->base.varints; + header->cmp_size = __read_varint_u64(&ptr); + header->orig_size = __read_varint_u64(&ptr); + header->inplace_margin = __read_varint_u64(&ptr); + int header_size = (void*)ptr - (void*)header; + if (header_size & 1) header_size++; + assertf(header_size < sizeof(asset_header_t), "header size too large"); + + // Seek back to the actual end of the header + int cur = lseek(fd, header_size - rdhead, SEEK_CUR); (void)cur; + assertf((cur & 1) == 0, "asset_read_header: header not 2-byte aligned (pos=%d)", cur); + + int compressed_size = header->cmp_size + header_size; + assertf(compressed_size == *sz, "Wrong compressed size (%d/%d)", *sz, compressed_size); + + int algo = ASSET_FLAG_ALGO(header->base.flags); + assertf(algo >= 1 && algo <= 3, + "unsupported compression algorithm: %d", algo); + #ifdef N64 + assertf(algos_full[algo-1].decompress_full, + "asset: compression level %d not initialized. Call asset_init_compression(%d) at initialization time", algo, algo); + #else + assertf(algos_stream[algo-1].decompress_init && algos_stream[algo-1].decompress_read, + "asset: compression level %d not initialized. Call asset_init_compression(%d) at initialization time", algo, algo); #endif + return asset_buf_size(header->orig_size, header->cmp_size, header->inplace_margin, NULL); + } else { + // Seek back before the header. + lseek(fd, -rdhead, SEEK_CUR); + assertf(*sz >= 0, "Invalid uncompressed size"); + return *sz; + } +} - assertf(header.algo >= 1 || header.algo <= 3, - "unsupported compression algorithm: %d", header.algo); - assertf(algos[header.algo-1].decompress_full || algos[header.algo-1].decompress_full_inplace, - "asset: compression level %d not initialized. Call asset_init_compression(%d) at initialization time", header.algo, header.algo); +static bool asset_read(int fd, asset_parsed_header_t *header, int *sz, void *buf, int *buf_size) +{ + if(memcmp(header->base.magic, ASSET_MAGIC, 3) == 0) { + bool ret; + int algo = ASSET_FLAG_ALGO(header->base.flags); - size = header.orig_size; - if ((header.flags & ASSET_FLAG_INPLACE) && algos[header.algo-1].decompress_full_inplace) - s = decompress_inplace(&algos[header.algo-1], fn, fd, header.cmp_size, size, header.inplace_margin); - else - s = algos[header.algo-1].decompress_full(fn, fd, header.cmp_size, size); + #ifdef N64 + ret = decompress_full(&algos_full[algo-1], fd, header->cmp_size, header->orig_size, header->inplace_margin, buf, buf_size); + #else + // On PC, full decompression is implemented by consuming the streaming decoder. + ret = decompress_full_stream(&algos_stream[algo-1], fd, header->base.flags, header->orig_size, buf, buf_size); + #endif + if(ret) { + *sz = header->orig_size; + } + return ret; } else { - // Allocate a buffer big enough to hold the file. - // We force a 32-byte alignment for the buffer so that it's aligned to instruction cache lines. - // This might or might not be useful, but if a binary file is laid out so that it - // matters, at least we guarantee that. - size = lseek(fd, 0, SEEK_END); - s = memalign(ASSET_ALIGNMENT, size); - - lseek(fd, 0, SEEK_SET); - read(fd, s, size); + if(buf == NULL || *buf_size < *sz) { + *buf_size = *sz; + return false; + } else { + #ifdef N64 + assertf(((uintptr_t)(buf) & (ASSET_ALIGNMENT_MIN-1)) == 0, "Asset buffer incorrectly aligned."); + #endif + } + read(fd, buf, *sz); + return true; } +} - close(fd); +bool asset_loadfd_into(int fd, int *sz, void *buf, int *buf_size) +{ + asset_parsed_header_t header; + asset_read_header(fd, &header, sz); + return asset_read(fd, &header, sz, buf, buf_size); +} + +bool asset_loadf_into(FILE *f, int *sz, void *buf, int *buf_size) +{ + int fd = fileno(f); + fflush(f); + assertf(ftell(f) == lseek(fd, 0, SEEK_CUR), "Flushing has data remaining in buffer"); + return asset_loadfd_into(fd, sz, buf, buf_size); +} + +void *asset_loadfd(int fd, int *sz) +{ + void *buf = NULL; int buf_size = 0; + asset_parsed_header_t header; + buf_size = asset_read_header(fd, &header, sz); + buf = memalign(ASSET_ALIGNMENT, buf_size); + assertf(buf, "Out of memory"); + asset_read(fd, &header, sz, buf, &buf_size); + return buf; +} + +void *asset_loadf(FILE *f, int *sz) +{ + int fd = fileno(f); + fflush(f); + assertf(ftell(f) == lseek(fd, 0, SEEK_CUR), "Flushing has data remaining in buffer"); + return asset_loadfd(fd, sz); +} + +void *asset_load(const char *fn, int *sz) +{ + void *buf = NULL; int buf_size = 0; + int size; + int fd = must_open(fn); + struct stat stat; + fstat(fd, &stat); + size = stat.st_size; + asset_parsed_header_t header; + buf_size = asset_read_header(fd, &header, &size); + buf = memalign(ASSET_ALIGNMENT, buf_size); + assertf(buf, "Out of memory"); + asset_read(fd, &header, &size, buf, &buf_size); if (sz) *sz = size; - return s; + close(fd); + return buf; } #ifdef N64 +/** @brief Uncompressed file cookie for funopen() */ typedef struct { - int fd; - bool seeked; + int fd; ///< Open file descriptor + bool seeked; ///< True if the file has been seeked once } cookie_none_t; static fpos_t seekfn_none(void *c, fpos_t pos, int whence) @@ -257,15 +364,19 @@ static int closefn_none(void *c) return 0; } +/** @brief Compression cookie for funopen() */ typedef struct { - int fd; - int pos; - bool seeked; - void (*reset)(void *state); - ssize_t (*read)(void *state, void *buf, size_t len); - uint8_t alignas(8) state[]; + int fd; ///< Open File descriptor + int pos; ///< Current position in the file + bool seeked; ///< True if the file has been seeked once + int header_size; ///< Size of the header + void (*reset)(void *state); ///< Reset function for the decompression state + ssize_t (*read)(void *state, void *buf, size_t len); ///< Read function for the decompression state + uint8_t alignas(16) state[]; ///< Decompression state (16-byte aligned) } cookie_cmp_t; +_Static_assert(offsetof(cookie_cmp_t, state) % 16 == 0, "cookie_cmp_t.state must be 16-byte aligned"); + static int readfn_cmp(void *c, char *buf, int sz) { cookie_cmp_t *cookie = (cookie_cmp_t*)c; @@ -285,7 +396,7 @@ static fpos_t seekfn_cmp(void *c, fpos_t pos, int whence) if (whence == SEEK_SET && pos == 0 && cookie->reset) { cookie->seeked = false; cookie->pos = 0; - lseek(cookie->fd, sizeof(asset_header_t), SEEK_SET); + lseek(cookie->fd, cookie->header_size, SEEK_SET); cookie->reset(cookie->state); return 0; } @@ -310,51 +421,59 @@ FILE *asset_fopen(const char *fn, int *sz) // Open the file. We use buffering on the outer file created by funopen, // so we don't actually need buffering on the underlying one. int fd = must_open(fn); + return asset_fdopen(fd, sz); +} +FILE *asset_fdopen(int fd, int *sz) +{ // Check if file is compressed - asset_header_t header; - read(fd, &header, sizeof(asset_header_t)); - if (!memcmp(header.magic, ASSET_MAGIC, 3)) { - if (header.version != '3') { - assertf(0, "unsupported asset version: %c\nMake sure to rebuild libdragon tools and your assets", header.version); + asset_parsed_header_t header; + int rdhead = read(fd, &header.base, sizeof(asset_header_t)); + if (memcmp(header.base.magic, ASSET_MAGIC, 3) == 0) { + if (header.base.version != '5') { + assertf(0, "unsupported asset version: %c\nMake sure to rebuild libdragon tools and your assets", header.base.version); return NULL; } - if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) { // for mkasset running on PC - header.algo = __builtin_bswap16(header.algo); - header.flags = __builtin_bswap16(header.flags); - header.cmp_size = __builtin_bswap32(header.cmp_size); - header.orig_size = __builtin_bswap32(header.orig_size); - header.inplace_margin = __builtin_bswap32(header.inplace_margin); - } + const uint8_t *ptr = header.base.varints; + header.cmp_size = __read_varint_u64(&ptr); + header.orig_size = __read_varint_u64(&ptr); + header.inplace_margin = __read_varint_u64(&ptr); + int header_size = (void*)ptr - (void*)&header; + if (header_size & 1) header_size++; + + // Seek back to the actual end of the header + lseek(fd, header_size - rdhead, SEEK_CUR); cookie_cmp_t *cookie; - assertf(header.algo >= 1 || header.algo <= 2, - "unsupported compression algorithm: %d", header.algo); - assertf(algos[header.algo-1].decompress_full || algos[header.algo-1].decompress_full_inplace, - "asset: compression level %d not initialized. Call asset_init_compression(%d) at initialization time", header.algo, header.algo); - assertf(algos[header.algo-1].decompress_init, - "asset: compression level %d does not currently support asset_fopen()", header.algo); - - int winsize = asset_winsize_from_flags(header.flags); - cookie = malloc(sizeof(cookie_cmp_t) + algos[header.algo-1].state_size + winsize); - cookie->read = algos[header.algo-1].decompress_read; - cookie->reset = algos[header.algo-1].decompress_reset; - algos[header.algo-1].decompress_init(cookie->state, fd, winsize); - + int algo = ASSET_FLAG_ALGO(header.base.flags); + assertf(algo >= 1 && algo <= 3, + "unsupported compression algorithm: %d", algo); + assertf(algos_stream[algo-1].decompress_init, + "asset: compression level %d not initialized. Call asset_init_compression(%d) at initialization time", algo, algo); + + int winsize = asset_winsize_from_flags(header.base.flags); + cookie = memalign(16, sizeof(cookie_cmp_t) + algos_stream[algo-1].state_size + winsize); + assertf(cookie, "Out of memory"); + cookie->read = algos_stream[algo-1].decompress_read; + cookie->reset = algos_stream[algo-1].decompress_reset; + algos_stream[algo-1].decompress_init(cookie->state, fd, winsize); cookie->fd = fd; cookie->pos = 0; cookie->seeked = false; + cookie->header_size = header_size; if (sz) *sz = header.orig_size; return funopen(cookie, readfn_cmp, NULL, seekfn_cmp, closefn_cmp); } // Not compressed. Return a wrapped FILE* without the seeking capability, // so that it matches the behavior of the compressed file. + int pos = lseek(fd, 0, SEEK_CUR); if (sz) *sz = lseek(fd, 0, SEEK_END); - lseek(fd, 0, SEEK_SET); + lseek(fd, pos - sizeof(asset_header_t), SEEK_SET); cookie_none_t *cookie = malloc(sizeof(cookie_none_t)); + assertf(cookie, "Out of memory"); cookie->fd = fd; cookie->seeked = false; return funopen(cookie, readfn_none, NULL, seekfn_none, closefn_none); diff --git a/src/asset_internal.h b/src/asset_internal.h index 6f18a5838..d71f84a91 100644 --- a/src/asset_internal.h +++ b/src/asset_internal.h @@ -1,26 +1,55 @@ /** * @file asset_internal.h * @author Giovanni Bajo + * @author Liam Coleman */ #ifndef __LIBDRAGON_ASSET_INTERNAL_H #define __LIBDRAGON_ASSET_INTERNAL_H #include +#include + #include #define ASSET_MAGIC "DCA" ///< Magic compressed asset header -#define ASSET_FLAG_WINSIZE_MASK 0x0007 ///< Mask to isolate the window size in the flags -#define ASSET_FLAG_WINSIZE_16K 0x0000 ///< 16 KiB window size -#define ASSET_FLAG_WINSIZE_8K 0x0001 ///< 8 KiB window size -#define ASSET_FLAG_WINSIZE_4K 0x0002 ///< 4 KiB window size -#define ASSET_FLAG_WINSIZE_2K 0x0003 ///< 2 KiB window size -#define ASSET_FLAG_WINSIZE_32K 0x0004 ///< 32 KiB window size -#define ASSET_FLAG_WINSIZE_64K 0x0005 ///< 64 KiB window size -#define ASSET_FLAG_WINSIZE_128K 0x0006 ///< 128 KiB window size -#define ASSET_FLAG_WINSIZE_256K 0x0007 ///< 256 KiB window size -#define ASSET_FLAG_INPLACE 0x0100 ///< Decompress in-place +#define ASSET_FLAG_WINSIZE_MASK 0x07 ///< Mask to isolate the window size in the flags +#define ASSET_FLAG_WINSIZE_16K 0x00 ///< 16 KiB window size +#define ASSET_FLAG_WINSIZE_8K 0x01 ///< 8 KiB window size +#define ASSET_FLAG_WINSIZE_4K 0x02 ///< 4 KiB window size +#define ASSET_FLAG_WINSIZE_2K 0x03 ///< 2 KiB window size +#define ASSET_FLAG_WINSIZE_32K 0x04 ///< 32 KiB window size +#define ASSET_FLAG_WINSIZE_64K 0x05 ///< 64 KiB window size +#define ASSET_FLAG_WINSIZE_128K 0x06 ///< 128 KiB window size +#define ASSET_FLAG_WINSIZE_256K 0x07 ///< 256 KiB window size +#define ASSET_FLAG_ALGO_SHIFT 4 ///< Shift to isolate the compression algorithm #define ASSET_ALIGNMENT 32 ///< Aligned to instruction cacheline +#define ASSET_FLAG_ALGO(flag) (((flag) >> ASSET_FLAG_ALGO_SHIFT) & 3) ///< Get compression algorithm from flags + +__attribute__((used)) +static inline int asset_buf_size(int size, int cmp_size, int margin, int *cmp_offset_dst) +{ + // add 8 because the assembly decompressors do writes up to 8 bytes out-of-bounds, + // that could overwrite the input data. + margin += 8; + int bufsize = size + margin; + int cmp_offset = bufsize - cmp_size; + // Align the source buffer to 4 bytes, so that we can use 32-bit loads (required by shrinkler). + // Notice that we need at least 2-byte alignment anyway, for DMA. + while (cmp_offset & 3) { + cmp_offset++; + bufsize++; + } + if(cmp_offset_dst) { + *cmp_offset_dst = cmp_offset; + } + if (bufsize & 15) { + // In case we need to call invalidate (see below), we need an aligned buffer + bufsize += 16 - (bufsize & 15); + } + return bufsize; +} + __attribute__((used)) static inline int asset_winsize_from_flags(uint16_t flags) { flags &= ASSET_FLAG_WINSIZE_MASK; @@ -47,20 +76,37 @@ static int asset_winsize_to_flags(int winsize) { typedef struct { char magic[3]; ///< Magic header uint8_t version; ///< Version of the asset header - uint16_t algo; ///< Compression algorithm - uint16_t flags; ///< Flags - uint32_t cmp_size; ///< Compressed size in bytes - uint32_t orig_size; ///< Original size in bytes - uint32_t inplace_margin; ///< Margin for in-place decompression + uint8_t flags; ///< Compression algorithm + uint8_t varints[12]; ///< Varint-encoded header fields } asset_header_t; -_Static_assert(sizeof(asset_header_t) == 20, "invalid sizeof(asset_header_t)"); +/** Asset header, with parsed fields */ +typedef struct { + asset_header_t base; ///< Base header data + uint32_t cmp_size; ///< Compressed size (decoded varint) + uint32_t orig_size; ///< Original size (decoded varint) + uint32_t inplace_margin; ///< In-place decompression margin (decoded varint) +} asset_parsed_header_t; /** @brief A decompression algorithm used by the asset library */ +typedef struct { + /** + * @brief Decompress a full file already available in memory (possibly racing with DMA on N64) + * + * Optional fast-path, typically implemented in assembly on N64. + * When unavailable, callers can always fall back to the streaming API + * (decompress_init/decompress_read) to load the full file. + * + * @return number of bytes written, or <0 on error + */ + int (*decompress_full)(const uint8_t *in, size_t cmp_size, uint8_t *out, size_t len); +} asset_compression_full_t; + +/** @brief A streaming decompression algorithm used by the asset library */ typedef struct { int state_size; ///< Basic size of the decompression state (without ringbuffer) - /** @brief Initialize the decompression state */ + /** @brief Initialize the decompression state. State is guaranteed to be 16-byte aligned */ void (*decompress_init)(void *state, int fd, int winsize); /** @brief Partially read a decompressed file from a state */ @@ -68,17 +114,17 @@ typedef struct { /** @brief Reset decompression state after rewind */ void (*decompress_reset)(void *state); - - /** @brief Decompress a full file in one go */ - void* (*decompress_full)(const char *fn, int fd, size_t cmp_size, size_t len); - - /** @brief Decompress a full file in-place */ - int (*decompress_full_inplace)(const uint8_t *in, size_t cmp_size, uint8_t *out, size_t len); -} asset_compression_t; +} asset_compression_stream_t; /** @brief Open a file as FILE* and assert on error */ FILE *must_fopen(const char *fn); /** @brief Open a file and assert on error */ int must_open(const char *fn); +/** @brief Load an asset from file descriptor with automatic memory allocation */ +void *asset_loadfd(int fd, int *sz); +/** @brief Load an asset from file descriptor into a provided buffer */ +bool asset_loadfd_into(int fd, int *sz, void *buf, int *buf_size); +/** @brief Open a file descriptor as a FILE* with asset support */ +FILE *asset_fdopen(int fd, int *sz); #endif diff --git a/src/audio.c b/src/audio.c index d562a01a7..3d65066cd 100644 --- a/src/audio.c +++ b/src/audio.c @@ -10,10 +10,12 @@ #include "interrupt.h" #include "n64sys.h" #include "utils.h" +#include "debug.h" #include #include #include #include +#include /** * @name DAC rates for different regions @@ -37,30 +39,37 @@ #define AI_STATUS_FULL ( 1 << 31 ) /** @} */ -/** @brief Number of buffers the audio subsytem allocates and manages */ -#define NUM_BUFFERS 4 /** @brief How many different audio buffers we want to schedule in one second. */ -#define BUFFERS_PER_SECOND 25 +#define BUFFERS_PER_SECOND 50 /** * @brief Macro that calculates the size of a buffer based on frequency * * @param[in] x * Frequency the AI is running at * - * @return The size of the buffer in bytes rounded to an 8 byte boundary + * @return The number of stereo samples per buffer, rounded up to a + * multiple of 16 (~64 bytes) to simplify code producing batches + * of samples. */ -#define CALC_BUFFER(x) ( ( ( ( x ) / BUFFERS_PER_SECOND ) >> 3 ) << 3 ) +#define CALC_BUFFER(x) ( ( ( ( x ) / BUFFERS_PER_SECOND ) + 15 ) & ~15 ) + +/** @brief Maximum number of AI buffers (width of the buf_full bitmask) */ +#define AUDIO_MAX_BUFFERS 32 /** @brief The actual frequency the AI will run at */ static int _frequency = 0; -/** @brief The number of buffers currently allocated */ -static int _num_buf = NUM_BUFFERS; -/** @brief The buffer size in bytes for each buffer allocated */ +/** @brief Required multiple for buffer length, or 0 for default */ +static int _granularity = 0; +/** @brief The number of buffers currently carved from #pool */ +static int _num_buf = 0; +/** @brief Stereo samples per carved buffer */ static int _buf_size = 0; -/** @brief Array of pointers to the allocated buffers */ -static short **buffers = NULL; -/** @brief Array of pointers to the allocated buffers (original pointers to free) */ -static short **buffers_orig = NULL; +/** @brief Total stereo samples reserved for the latency headroom */ +static int _pool_samples = 0; +/** @brief Single uncached allocation holding all AI buffers */ +static short *pool = NULL; +/** @brief Pointers into #pool for each carved buffer */ +static short *buffers[AUDIO_MAX_BUFFERS]; static audio_fill_buffer_callback _fill_buffer_callback = NULL; static audio_fill_buffer_callback _orig_fill_buffer_callback = NULL; @@ -78,6 +87,8 @@ static volatile int now_writing = 0; /** @brief Bitmask of buffers indicating which buffers are full */ static volatile int buf_full = 0; +_Static_assert(sizeof(buf_full) * 8 == AUDIO_MAX_BUFFERS, "buf_full width"); + /** @brief Structure used to interact with the AI registers */ static volatile struct AI_regs_s * const AI_regs = (struct AI_regs_s *)0xa4500000; @@ -110,7 +121,7 @@ static volatile inline int __full() static void audio_callback() { /* Do not copy more data if we've freed the audio system */ - if(!buffers) + if(!pool) { return; } @@ -174,7 +185,43 @@ static void audio_callback() enable_interrupts(); } -void audio_init(const int frequency, int numbuffers) +/** + * @brief Carve #buffers[] as slices of #pool for the current granularity. + * + * Each slice is followed by 8 bytes of padding so the AI DMA boundary + * workaround can shift a pointer without overlapping the next slice. + */ +static void audio_carve_buffers(void) +{ + int ideal = CALC_BUFFER(_frequency); + if (_granularity > 0) + _buf_size = MAX(_granularity, (int)lroundf(ideal / (float)_granularity) * _granularity); + else + _buf_size = ideal; + + _num_buf = _pool_samples / _buf_size; + assertf(_num_buf >= 2, "buffer granularity %d too large for configured latency", _granularity); + if (_num_buf > (int)AUDIO_MAX_BUFFERS) + _num_buf = AUDIO_MAX_BUFFERS; + + short *p = pool; + for (int i = 0; i < _num_buf; i++) { + /* Stereo buffer, plus 8 bytes of padding for the AI DMA bug. */ + buffers[i] = p; + if (((uint32_t)(p + 2 * _buf_size) & 0x1FFF) == 0) + buffers[i] = p + 4; + memset(buffers[i], 0, sizeof(short) * 2 * _buf_size); + p += 2 * _buf_size + 4; + } + + now_playing = 0; + playing_queue = 0; + now_empty = 0; + now_writing = 0; + buf_full = 0; +} + +void audio_init(const int frequency, float latency) { int clockrate; @@ -195,18 +242,11 @@ void audio_init(const int frequency, int numbuffers) break; } - /* Make sure we don't choose too many buffers */ - if( numbuffers > (sizeof(buf_full) * 8) ) - { - /* This is a bit mask, so we can only have as many - * buffers as we have bits. */ - numbuffers = sizeof(buf_full) * 8; - } - /* Calculate DAC dacrate. This is based on the VI clock rate (as the VI clock is also used for the AI output), divided by the requested frequency, rounding up. */ int dacrate = ((2 * clockrate / frequency) + 1) / 2; + assertf(dacrate <= (1 << 14), "Requested frequency %d is too low", frequency); /* Bitrate is the half period for each bit of the sample. We need to send 32 bits, so 64 periods, but the datasheet of the DAC suggests to allow for 66 periods instead. So calculate the bitrate as dacrate / 66. We can @@ -229,39 +269,39 @@ void audio_init(const int frequency, int numbuffers) register_AI_handler(audio_callback); set_AI_interrupt(1); - /* Set up buffers */ - _buf_size = CALC_BUFFER(_frequency); - _num_buf = (numbuffers > 1) ? numbuffers : NUM_BUFFERS; - buffers = malloc(_num_buf * sizeof(short *)); - buffers_orig = malloc(_num_buf * sizeof(short *)); + if (latency <= 1.0f) + latency = AUDIO_DEFAULT_LATENCY; + _granularity = 0; - for(int i = 0; i < _num_buf; i++) - { - /* Stereo buffers, interleaved, plus 8 bytes of padding */ - buffers_orig[i] = buffers[i] = - malloc_uncached(sizeof(short) * 2 * _buf_size + 8); - - /* Workaround AI DMA hardware bug. If a buffer ends exactly - * at a 0x2000 address boundary, AI DMA gets confused because - * of a delayed internal carry. Avoid using such buffers, - * and since we allocated 8 bytes of padding, we can move - * our pointer a bit. - */ - if (((uint32_t)(buffers[i] + 2 * _buf_size) & 0x1FFF) == 0) - buffers[i] += 4; + /* Size the pool for the default carving so later granularity changes only + re-slice it. Keep 8 bytes of pad per possible buffer for the AI DMA bug. */ + int ideal = CALC_BUFFER(_frequency); + _num_buf = (int)ceilf(latency * _frequency / (25.0f * (float)ideal)); + if (_num_buf < 2) + _num_buf = 2; + if (_num_buf > (int)AUDIO_MAX_BUFFERS) + _num_buf = AUDIO_MAX_BUFFERS; + _pool_samples = _num_buf * ideal; - memset(buffers[i], 0, sizeof(short) * 2 * _buf_size); - } + pool = malloc_uncached(sizeof(short) * 2 * _pool_samples + AUDIO_MAX_BUFFERS * 8); + assertf(pool, "Out of memory"); - /* Set up ring buffer pointers */ - now_playing = 0; - playing_queue = 0; - now_empty = 0; - now_writing = 0; - buf_full = 0; + audio_carve_buffers(); _paused = false; } +void audio_set_buffer_granularity(int nsamples) +{ + assertf(pool, "audio_init() must be called first"); + assertf(nsamples > 0 && (nsamples & 15) == 0, + "buffer granularity must be a positive multiple of 16"); + assertf(audio_get_queued_buffers() == 0 && playing_queue == 0, + "audio_set_buffer_granularity() requires an empty AI queue"); + + _granularity = nsamples; + audio_carve_buffers(); +} + void audio_set_buffer_callback(audio_fill_buffer_callback fill_buffer_callback) { disable_interrupts(); @@ -278,31 +318,22 @@ void audio_close() unregister_AI_handler(audio_callback); /* Stop audio DMA and clocks */ + while (AI_regs->status & AI_STATUS_BUSY) {} AI_regs->control = 0; AI_regs->dacrate = 0; AI_regs->bitrate = 0; + AI_regs->status = 0; - if(buffers) - { - for(int i = 0; i < _num_buf; i++) - { - /* Nuke anything that isn't freed */ - if(buffers_orig[i]) - { - free_uncached(buffers_orig[i]); - buffers_orig[i] = buffers[i] = 0; - } - } - - /* Nuke array of buffers we init'd earlier */ - free(buffers); - buffers = 0; - free(buffers_orig); - buffers_orig = 0; + if (pool) { + free_uncached(pool); + pool = NULL; } _frequency = 0; + _granularity = 0; _buf_size = 0; + _pool_samples = 0; + _num_buf = 0; } static void audio_paused_callback(short *buffer, size_t numsamples) @@ -346,7 +377,7 @@ void audio_pause(bool pause) { */ void audio_write(const short * const buffer) { - if(!buffers) + if(!pool) { return; } @@ -373,7 +404,7 @@ void audio_write(const short * const buffer) short* audio_write_begin(void) { - if(!buffers) + if(!pool) { return NULL; } @@ -407,7 +438,7 @@ void audio_write_end(void) void audio_write_silence() { - if(!buffers) + if(!pool) { return; } @@ -434,7 +465,7 @@ void audio_write_silence() volatile int audio_can_write() { - if(!buffers) + if(!pool) { return 0; } @@ -483,3 +514,22 @@ int audio_get_buffer_length() { return _buf_size; } + +int audio_get_num_buffers() +{ + return pool ? _num_buf : 0; +} + +int audio_get_queued_buffers() +{ + if (!pool) + return 0; + + uint32_t mask = buf_full; + int count = 0; + while (mask) { + count += mask & 1; + mask >>= 1; + } + return count; +} diff --git a/src/audio/ay8910.c b/src/audio/ay8910.c index 3469fc4c3..1ba2dae3a 100644 --- a/src/audio/ay8910.c +++ b/src/audio/ay8910.c @@ -6,6 +6,7 @@ #include "ay8910.h" #include #include +#include "../rand_internal.h" /** @brief Enable AY8910 tracing */ #define AY8910_TRACE 0 @@ -147,20 +148,6 @@ int ay8910_gen(AY8910 *ay, int16_t *out, int nsamples) { #else -static uint32_t fastrand() { - /* Algorithm "xor" from p. 4 of Marsaglia, "Xorshift RNGs" */ - static int state = 1; - uint32_t x = state; - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - return state = x; -} - -static float fastrandf() { - return fastrand() * 2.3283064365386963e-10f; -} - // Optimized implementation, much faster. // This implementation is more complex compared to the reference once. It // inspects the internal state of the AY8910 and decides when the next state @@ -350,7 +337,7 @@ int ay8910_gen(AY8910 *ay, int16_t *out, int nsamples) { #endif if (sample_accum_n) { - float fr = fastrandf(); + float fr = __randf32(); int sa = AY8910_DECIMATE-sample_accum_n; if (sa > next) { #if AY8910_OUTPUT_STEREO @@ -378,7 +365,7 @@ int ay8910_gen(AY8910 *ay, int16_t *out, int nsamples) { int nn = next / AY8910_DECIMATE; if (fastnoise) { for (int i=0; i>8); _W8 (x); }) - #define _W32(x) ({ _W16((x)>>16); _W16(x); }) - #define _W64(x) ({ _W32((x)>>32); _W32(x); }) - #define _WA(x,n) ({ fwrite(x, 1, n,out); }) - - #define W8(x) ({ _CHKSZ(x,1); _W8((uint8_t)(x)); }) - #define W16(x) ({ _CHKSZ(x,2); _W16((uint16_t)(x)); }) - #define W32(x) ({ _CHKSZ(x,4); _W32((uint32_t)(x)); }) - #define W64(x) ({ _CHKSZ(x,8); _W64((uint64_t)(x)); }) - #define WA(x,n) ({ _CHKSZ((x)[0],1); _WA((const uint8_t*)(x),n); }) - #define WB(x) ({ bool *b = &x; _W8(*b); }) - #define WF(x) ({ _CHKSZ(x,4); uint8_t *f = (uint8_t*)(&x); uint32_t fx = *(uint32_t*)f; _W32(fx); }) - #define WALIGN() ({ while (ftell(out) % 8) _W8(0); }) - - - const uint8_t version = 6; - WA("XM64", 4); - W8(version); - W32(ctx->ctx_size); - W32(ctx->ctx_size_all_patterns); - W32(ctx->ctx_size_all_samples); - W32(ctx->ctx_size_stream_pattern_buf); - for (int i=0; i<32; i++) W32(ctx->ctx_size_stream_sample_buf[i]); - - W16(ctx->module.tempo); - W16(ctx->module.bpm); - -#if XM_STRINGS - WA(ctx->module.name, sizeof(ctx->module.name)); - WA(ctx->module.trackername, sizeof(ctx->module.trackername)); -#else - char name[MODULE_NAME_LENGTH+1] = {0}; char trackername[TRACKER_NAME_LENGTH+1] = {0}; - WA(name, sizeof(name)); WA(trackername, sizeof(trackername)); -#endif - W16(ctx->module.length); - W16(ctx->module.restart_position); - W16(ctx->module.num_channels); - W16(ctx->module.num_patterns); - W16(ctx->module.num_instruments); - W32(ctx->module.frequency_type); - WA(ctx->module.pattern_table, sizeof(ctx->module.pattern_table)); - - int totsamples = 0; - for (int i=0;imodule.num_instruments;i++) { - xm_instrument_t *ins = &ctx->module.instruments[i]; - totsamples += ins->num_samples; - } - uint32_t pat_off[ctx->module.num_patterns]; - uint32_t sam_off[totsamples]; - int sam_off_idx=0, pat_off_idx=0; - - for (int i=0;imodule.num_patterns;i++) { - W16(ctx->module.patterns[i].num_rows); - pat_off[pat_off_idx++] = ftell(out); - W32(0); // will fill later (position) - W16((uint16_t)0); // will fill later (size) - } - - for (int i=0;imodule.num_instruments;i++) { - xm_instrument_t *ins = &ctx->module.instruments[i]; -#if XM_STRINGS - WA(ins->name, sizeof(ins->name)); -#else - char name[INSTRUMENT_NAME_LENGTH + 1] = {0}; - WA(name); -#endif - WA(ins->sample_of_notes, sizeof(ins->sample_of_notes)); - - W8(ins->volume_envelope.num_points); - for (int j=0;jvolume_envelope.num_points;j++) { - W16(ins->volume_envelope.points[j].frame); - W16(ins->volume_envelope.points[j].value); - } - W8(ins->volume_envelope.sustain_point); - W8(ins->volume_envelope.loop_start_point); - W8(ins->volume_envelope.loop_end_point); - WB(ins->volume_envelope.enabled); - WB(ins->volume_envelope.sustain_enabled); - WB(ins->volume_envelope.loop_enabled); - - W8(ins->panning_envelope.num_points); - for (int j=0;jpanning_envelope.num_points;j++) { - W16(ins->panning_envelope.points[j].frame); - W16(ins->panning_envelope.points[j].value); - } - W8(ins->panning_envelope.sustain_point); - W8(ins->panning_envelope.loop_start_point); - W8(ins->panning_envelope.loop_end_point); - WB(ins->panning_envelope.enabled); - WB(ins->panning_envelope.sustain_enabled); - WB(ins->panning_envelope.loop_enabled); - - W32(ins->vibrato_type); - W8(ins->vibrato_sweep); - W8(ins->vibrato_depth); - W8(ins->vibrato_rate); - W16(ins->volume_fadeout); - W64(ins->latest_trigger); - - W16(ins->num_samples); - for (int j=0;jnum_samples;j++) { - xm_sample_t *s = &ins->samples[j]; - W8(s->bits); - W32(s->length); - W32(s->loop_start); - W32(s->loop_length); - W32(s->loop_end); - WF(s->volume); - W8(s->finetune); - W32(s->loop_type); - WF(s->panning); - W8(s->relative_note); - sam_off[sam_off_idx++] = ftell(out); - W32(0); // will fill later - } - } - - - pat_off_idx = 0; - sam_off_idx = 0; - - WA("WAVE", 4); - uint32_t wv_overred = XM_WAVEFORM_OVERREAD; - W32(wv_overred); - for (int i=0;imodule.num_instruments;i++) { - xm_instrument_t *ins = &ctx->module.instruments[i]; - for (int j=0;jnum_samples;j++) { - xm_sample_t *s = &ins->samples[j]; - WALIGN(); - - uint32_t pos = ftell(out); - fseek(out, sam_off[sam_off_idx++], SEEK_SET); - W32(pos); - fseek(out, pos, SEEK_SET); - - assert(s->bits == 8 || s->bits == 16); - if (s->bits == 8) - WA(s->data8, s->length+XM_WAVEFORM_OVERREAD); - else { - for (int k=0;klength+XM_WAVEFORM_OVERREAD/2;k++) - W16(s->data16[k]); - } - } - } - - WA("PATT", 4); - for (int i=0;imodule.num_patterns;i++) { - WALIGN(); - - uint32_t pos = ftell(out); - - xm_pattern_t *p = &ctx->module.patterns[i]; - - int pat_size = p->num_rows*ctx->module.num_channels*5; - uint8_t cur_pat[pat_size]; - uint8_t *pp = cur_pat; - - xm_pattern_slot_t *s = &p->slots[0]; - for (int j=0;jnum_rows;j++) { - for (int k=0;kmodule.num_channels;k++) { - *pp++ = s->note; - *pp++ = s->instrument; - *pp++ = s->volume_column; - *pp++ = s->effect_type; - *pp++ = s->effect_param; - s++; - } - } - - // RLE-compress pattern - // - // The compressed stream is a sequence of "blocks". The number of blocks - // is not encoded, so the compressed size must be provided off-band. - // The following describes the format of a block. - // - // Each block begins with one varint-encoded number. The lowest 3 bits - // of this number represents the number of "runs" in this block, while - // the remaining bits are the number of "zeros". If the number of "runs" - // is 7, another varint-encoded number follows, that must be added to 7 - // to obtain the real number of runs. After this, the block contains - // "runs" bytes, which are the literal data. - // The decompressor must first emit the specified number of zeros, - // and then copy the literal data. - // - // Varint encoding: a sequence of bytes where for each byte, the MSB - // is the continuation bit (1=another byte follow, 0=this is the last byte), - // while the other 7 bits are the actual payload that must be concatenated - // across all bytes. Notice that the encoding is big-endian, so the first - // byte contains the highest 7 bits of the encoded number. - // - int x = 0; - while (x < pat_size) { - // Detect sequence of zeros - int zeros = 0; - while (x+zeros < pat_size && cur_pat[x+zeros] == 0) - zeros++; - x += zeros; - - // Detect sequence of runs - // NOTE: we don't stop the sequence for a single zero byte, because - // that would be less efficient. So the runs finish when two zero - // bytes are detected. - int runs = 0; - while (x+runs < pat_size && (cur_pat[x+runs] != 0 || x+runs+1 >= pat_size || cur_pat[x+runs+1] != 0)) - runs++; - - // Prepare the encoded zeros number (with the lowest bits that encode - // part of runs), and the encoded runs (leftover). - int runs_low = (runs > 7) ? 7 : runs; - int enc_zeros = (zeros << 3) | runs_low; - int enc_runs = runs - runs_low; - - #define UINT32_NUM_BITS(n) (32 - ((n) == 0 ? 32 : __builtin_clz(n))) - #define CEIL_DIV(n, d) (((n) + (d) - 1) / (d)) - - // Varint-encode the zeros - int nb = CEIL_DIV(UINT32_NUM_BITS(enc_zeros), 7) - 1; - while (nb>0) { - W8((uint8_t)(0x80 | ((enc_zeros >> (nb*7)) & 0x7F))); - nb--; - } - W8((uint8_t)(enc_zeros & 0x7F)); - - // Varint-encode the lefotver runs (if any) - if (runs_low == 7) { - int nb = CEIL_DIV(UINT32_NUM_BITS(enc_runs), 7) - 1; - while (nb>0) { - W8((uint8_t)(0x80 | ((enc_runs >> (nb*7)) & 0x7F))); - nb--; - } - W8((uint8_t)(enc_runs & 0x7F)); - } - - // Write the actual literal data - WA(cur_pat+x, runs); - x += runs; - } - - uint16_t size = ftell(out) - pos; - - fseek(out, pat_off[pat_off_idx++], SEEK_SET); - W32(pos); - W16(size); - fseek(out, pos+size, SEEK_SET); - } - - WA("END!", 4); - - #undef _CHKSZ - #undef _W8 - #undef _W16 - #undef _W32 - #undef _W64 - #undef _WA - #undef W8 - #undef W16 - #undef W32 - #undef W64 - #undef WA - #undef WB - #undef WF - #undef WALIGN -} - -#endif - -static uint32_t varint_get(uint8_t **pp) { - int8_t *p = (int8_t*)*pp; // trick: get GCC to generate better code in the loop - uint32_t x = 0; - int8_t y; - do { - y = *p++; - x <<= 7; x |= y & 0x7F; - } while (y & 0x80); - *pp = (uint8_t*)p; - return x; -} - -// Decompress a pattern that was compressed with our custom RLE algorithm (see above). -// Notice that in-place decompression is supported, by loading compressed data -// at the end of the decompression buffer, so that no additional memory is required. -int xm_context_decompress_pattern(uint8_t *in, int sz, xm_pattern_slot_t *pat) { - uint8_t *in_end = in+sz; - uint8_t *out = (uint8_t*)pat; - bool direction = (out<=in); - while (in < in_end) { - int zeros = varint_get(&in); - int runs = zeros & 7; if (runs == 7) runs += varint_get(&in); - zeros >>= 3; - memset(out, 0, zeros); out += zeros; - memmove(out, in, runs); out += runs; in += runs; - assert((out<=in) == direction); // check for overruns during in-place decompression - } - return out - (uint8_t*)pat; -} - - int xm_context_load(xm_context_t** ctxp, FILE* in, uint32_t rate) { #define _CHKSZ(x,n) ({ _Static_assert(sizeof(x) == n, "invalid type size"); }) @@ -433,39 +129,17 @@ int xm_context_load(xm_context_t** ctxp, FILE* in, uint32_t rate) { #define RS(x,n) fseek(in, n, SEEK_CUR) // just skip the string #endif - uint8_t version; - char head[4]; - RA(head, 4); - if (head[0] != 'X' || head[1] != 'M' || head[2] != '6' || head[3] != '4') { - DEBUG("invalid header\n"); - return 1; - } - - // Version log: - // 5: first public version - // 6: added overread for non-looping samples. The size of optimal - // stream sample buffer size must change, hance the version bump. - R8(version); - if (version != 5 && version != 6) { - DEBUG("invalid XM64 version %d\n", version); - return 1; - } - - uint32_t ctx_size, ctx_size_all_samples, ctx_size_all_patterns, ctx_size_stream_pattern_buf, ctx_size_stream_sample_buf[32]; + uint32_t ctx_size, ctx_size_all_samples, ctx_size_all_patterns, ctx_size_stream_pattern_buf; + uint32_t buf_rate[32], buf_base[32], buf_min[32], buf_cap[32]; R32(ctx_size); R32(ctx_size_all_patterns); R32(ctx_size_all_samples); R32(ctx_size_stream_pattern_buf); - for (int i=0;i<32;i++) R32(ctx_size_stream_sample_buf[i]); - if (version == 5) { - for (int i=0;i<32;i++) { - // Add the overread size to all (non-empty) channels. This is a small pessimization, - // but it's trivial and we allow loading v5 files (albeit consuming a little bit more RAM). - if (ctx_size_stream_sample_buf[i]) - ctx_size_stream_sample_buf[i] += 64; - } - } + for (int i=0;i<32;i++) R32(buf_rate[i]); + for (int i=0;i<32;i++) R32(buf_base[i]); + for (int i=0;i<32;i++) R32(buf_min[i]); + for (int i=0;i<32;i++) R32(buf_cap[i]); uint32_t alloc_bytes = ctx_size; #if XM_STREAM_PATTERNS @@ -488,10 +162,15 @@ int xm_context_load(xm_context_t** ctxp, FILE* in, uint32_t rate) { ctx->ctx_size_all_samples = ctx_size_all_samples; ctx->ctx_size_all_patterns = ctx_size_all_patterns; ctx->ctx_size_stream_pattern_buf = ctx_size_stream_pattern_buf; - for (int i=0;i<32;i++) ctx->ctx_size_stream_sample_buf[i] = ctx_size_stream_sample_buf[i]; + for (int i=0;i<32;i++) { + ctx->ctx_stream_buf_rate[i] = buf_rate[i]; + ctx->ctx_stream_buf_base[i] = buf_base[i]; + ctx->ctx_stream_buf_min[i] = buf_min[i]; + ctx->ctx_stream_buf_cap[i] = buf_cap[i]; + } #if XM_STREAM_WAVEFORMS || XM_STREAM_PATTERNS - ctx->fh = in; /* Save the file if we need to stream later */ + ctx->fd = fileno(in); /* Save the file if we need to stream later */ #endif R16(ctx->module.tempo); @@ -578,94 +257,10 @@ int xm_context_load(xm_context_t** ctxp, FILE* in, uint32_t rate) { } } - RA(head, 4); - if (head[0] != 'W' && head[1] != 'A' && head[2] != 'V' && head[3] != 'E') { - DEBUG("invalid WAVE header\n"); - free(*ctxp); - *ctxp = NULL; - return 1; - } - - uint32_t wv_overread; - R32(wv_overread); - if (wv_overread < XM_WAVEFORM_OVERREAD) { - DEBUG("waveform overread too little (%d < %d)\n", (int)wv_overread, XM_WAVEFORM_OVERREAD); - free(*ctxp); - *ctxp = NULL; - return 1; - } - - -#if !XM_STREAM_WAVEFORMS - for (int i=0;imodule.num_instruments;i++) { - xm_instrument_t *ins = &ctx->module.instruments[i]; - - for (int j=0;jnum_samples;j++) { - xm_sample_t *s = &ins->samples[j]; - - fseek(in, s->data8_offset, SEEK_SET); - - s->data8 = (int8_t*)mempool; - mempool += s->length * (s->bits / 8) + XM_WAVEFORM_OVERREAD; - if ((size_t)mempool & 7) mempool += 8 - ((size_t)mempool & 7); - - if (s->bits == 8) - RA(s->data8, s->length+XM_WAVEFORM_OVERREAD); - else { - RA(s->data8, s->length*2+XM_WAVEFORM_OVERREAD); - #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ - for (int k=0;klength+XM_WAVEFORM_OVERREAD/2;k++) - s->data16[k] = __builtin_bswap16(s->data16[k]); - #endif - } - } - } - - // This is actually not guaranteed by the file format, but since the - // save function laids out waveforms in order, after reading the last one - // we should have arrived on the pattern magic string. - RA(head, 4); - if (head[0] != 'P' || head[1] != 'A' || head[2] != 'T' || head[3] != 'T') { - DEBUG("invalid PATT header\n"); - free(*ctxp); - *ctxp = NULL; - return 1; - } -#endif + R8(ctx->external_samples); -#if !XM_STREAM_PATTERNS - for (int i=0;imodule.num_patterns;i++) { - xm_pattern_t *p = &ctx->module.patterns[i]; - - int cmp_size = p->slots_size; - int dec_size = sizeof(xm_pattern_slot_t) * p->num_rows * ctx->module.num_channels; - - fseek(in, p->slots_offset, SEEK_SET); - - assert(((size_t)mempool & 7) == 0); - p->slots = (xm_pattern_slot_t*)mempool; - mempool += sizeof(xm_pattern_slot_t) * ctx->module.num_channels * p->num_rows; - if ((size_t)mempool & 7) mempool += 8 - ((size_t)mempool & 7); - - uint8_t *cmp_data = (uint8_t*)p->slots + dec_size - cmp_size; - RA(cmp_data, cmp_size); - - // Decompress the slots - int sz = xm_context_decompress_pattern(cmp_data, cmp_size, p->slots); - assert(sz == dec_size); - } - - // This is actually not guaranteed by the file format, but since the - // save function laids out patterns in order, after reading the last one - // we should have arrived on the end magic string. - RA(head, 4); - if (head[0] != 'E' || head[1] != 'N' || head[2] != 'D' || head[3] != '!') { - DEBUG("invalid END header (%x)\n", (unsigned)ftell(in)); - free(*ctxp); - *ctxp = NULL; - return 1; - } -#else +#if XM_STREAM_PATTERNS + if ((size_t)mempool & 15) mempool += 16 - ((size_t)mempool & 15); ctx->slot_buffer_index = -1; ctx->slot_buffer = (xm_pattern_slot_t*)mempool; mempool += ctx->ctx_size_stream_pattern_buf; diff --git a/src/audio/libxm/load.c b/src/audio/libxm/load.c index 944113fbf..dd78bbd79 100644 --- a/src/audio/libxm/load.c +++ b/src/audio/libxm/load.c @@ -211,6 +211,13 @@ char* xm_load_module(xm_context_t* ctx, const char* moddata, size_t moddata_leng READ_MEMCPY(mod->pattern_table, offset + 20, PATTERN_ORDER_TABLE_LENGTH); offset += header_size; + /* rasky: sanitize pattern table: remove references to non-existing patterns. */ + for(int i = 0; i < mod->length; ++i) { + if(mod->pattern_table[i] >= mod->num_patterns) { + mod->pattern_table[i] = 0; + } + } + /* Read patterns */ for(uint16_t i = 0; i < mod->num_patterns; ++i) { uint16_t packed_patterndata_size = READ_U16(offset + 7); @@ -233,7 +240,9 @@ char* xm_load_module(xm_context_t* ctx, const char* moddata, size_t moddata_leng /* This isn't your typical for loop */ for(uint16_t j = 0, k = 0; j < packed_patterndata_size; ++k) { uint8_t note = READ_U8(offset + j); - xm_pattern_slot_t* slot = pat->slots + k; + int krow = k / mod->num_channels; + int kchan = k % mod->num_channels; + xm_pattern_slot_t* slot = pat->slots + (kchan * pat->num_rows) + krow; if(note & (1 << 7)) { /* MSB is set, this is a compressed packet */ diff --git a/src/audio/libxm/play.c b/src/audio/libxm/play.c index dc6289c4a..a4730727f 100644 --- a/src/audio/libxm/play.c +++ b/src/audio/libxm/play.c @@ -10,6 +10,12 @@ #include "xm_internal.h" #include #include +#include + +#if XM_STREAM_PATTERNS +#include "debug.h" +#include "asset_internal.h" +#endif /* ----- Static functions ----- */ @@ -469,6 +475,11 @@ static void xm_handle_note_and_instrument(xm_context_t* ctx, xm_channel_context_ xm_cut_note(ch); } } + + /* If this key-on doesn't contain 9xx, reset start position memory */ + if(s->effect_type != 9) + ch->sample_starting_position_bytes = 0; + } else if(s->note == 97) { /* Key Off */ xm_key_off(ch); @@ -576,13 +587,14 @@ static void xm_handle_note_and_instrument(xm_context_t* ctx, xm_channel_context_ case 9: /* 9xx: Sample offset */ if(ch->sample != NULL && NOTE_IS_VALID(s->note)) { - uint32_t final_offset = s->effect_param << (ch->sample->bits == 16 ? 7 : 8); - if(final_offset >= ch->sample->length) { - /* Pretend the sample dosen't loop and is done playing */ + int final_offset = s->effect_param << 8; + if (final_offset > 0) { + ch->sample_starting_position_bytes = final_offset; + } + ch->sample_position = ch->sample_starting_position_bytes >> (ch->sample->bits == 16 ? 1 : 0); + if (ch->sample_position >= ch->sample->length) { ch->sample_position = -1; - break; } - ch->sample_position = final_offset; } break; @@ -879,19 +891,11 @@ static void xm_row(xm_context_t* ctx) { #if XM_STREAM_PATTERNS if (ctx->slot_buffer_index != pat_idx) { - // Read the compressed data at the end of the pattern buffer, that is - // at the end of the buffer where data will be uncompressed. The chosen - // RLE compression guarantees that this is safe. int cmp_size = cur->slots_size; - int dec_size = sizeof(xm_pattern_slot_t) * cur->num_rows * ctx->module.num_channels; - uint8_t *cmp_data = (uint8_t*)ctx->slot_buffer + dec_size - cmp_size; - - fseek(ctx->fh, cur->slots_offset, SEEK_SET); - fread(cmp_data, cmp_size, 1, ctx->fh); - - int sz = xm_context_decompress_pattern(cmp_data, cmp_size, ctx->slot_buffer); - assert(sz == dec_size); - + int dec_size = ctx->ctx_size_stream_pattern_buf; + lseek(ctx->fd, cur->slots_offset, SEEK_SET); + bool ret = asset_loadfd_into(ctx->fd, &cmp_size, ctx->slot_buffer, &dec_size); + assertf(ret, "Failed to load pattern %d (dec_size=%d buf=%ld)", pat_idx, dec_size, ctx->ctx_size_stream_pattern_buf); ctx->slot_buffer_index = pat_idx; } #endif @@ -899,9 +903,9 @@ static void xm_row(xm_context_t* ctx) { /* Read notes… */ for(uint8_t i = 0; i < ctx->module.num_channels; ++i) { #if !XM_STREAM_PATTERNS - xm_pattern_slot_t* s = cur->slots + ctx->current_row * ctx->module.num_channels + i; + xm_pattern_slot_t* s = cur->slots + i * cur->num_rows + ctx->current_row; #else - xm_pattern_slot_t* s = ctx->slot_buffer + ctx->current_row * ctx->module.num_channels + i; + xm_pattern_slot_t* s = ctx->slot_buffer + i * cur->num_rows + ctx->current_row; #endif xm_channel_context_t* ch = ctx->channels + i; diff --git a/src/audio/libxm/xm.h b/src/audio/libxm/xm.h index 649b0b1d3..5fe9474f8 100644 --- a/src/audio/libxm/xm.h +++ b/src/audio/libxm/xm.h @@ -79,15 +79,6 @@ void xm_create_context_from_libxmize(xm_context_t**, char* libxmizeddata, uint32 /** Free a XM context created by xm_create_context(). */ void xm_free_context(xm_context_t*); -/** Save a context into a XM64 file. - * - * Saving a context can be done only on PC, and with a non-streaming loader that - * fetched everything. - */ -#if !defined(N64) && !XM_STREAM_PATTERNS && !XM_STREAM_WAVEFORMS -void xm_context_save(xm_context_t* ctx, FILE* out); -#endif - /** Load a context from a XM64 file. * * Returns 0 in case of success, 1 in case of generic error (file corrupted), diff --git a/src/audio/libxm/xm_internal.h b/src/audio/libxm/xm_internal.h index aebfb1195..0d0b0641c 100644 --- a/src/audio/libxm/xm_internal.h +++ b/src/audio/libxm/xm_internal.h @@ -16,9 +16,7 @@ // this amount of bytes. See also rspxm.S for details. #define XM_WAVEFORM_OVERREAD 64 -#if XM_STREAM_WAVEFORMS -typedef struct waveform_s waveform_t; -#endif +typedef struct wav64_s wav64_t; #if XM_DEBUG #include @@ -111,10 +109,9 @@ struct xm_sample_s { int8_t relative_note; uint64_t latest_trigger; -#if XM_STREAM_WAVEFORMS - // libdragon mixer's waveform - waveform_t *wave; -#endif + #ifdef N64 + wav64_t *wave; // libdragon wav64 sample + #endif union { int8_t* data8; @@ -208,7 +205,7 @@ struct xm_channel_context_s { xm_sample_t* sample; /* Could be NULL */ xm_pattern_slot_t* current; - float sample_position; + double sample_position; float period; float frequency; float step; @@ -260,6 +257,8 @@ struct xm_channel_context_s { uint8_t tremor_param; bool tremor_on; + int sample_starting_position_bytes; /* set by 9xx command */ + uint64_t latest_trigger; bool muted; @@ -281,7 +280,14 @@ struct xm_context_s { uint32_t ctx_size_all_patterns; uint32_t ctx_size_all_samples; uint32_t ctx_size_stream_pattern_buf; - uint32_t ctx_size_stream_sample_buf[32]; + /* Samplebuffer sizing of each channel, as computed by audioconv64. How far + * the CPU may run ahead of the RSP is only known at playback time, so the + * part of the ring that covers it is stored as a rate and turned into + * bytes by #xm64player_play. */ + uint32_t ctx_stream_buf_rate[32]; /* bytes of stream per second of playback */ + uint32_t ctx_stream_buf_base[32]; /* bytes that do not depend on the queue */ + uint32_t ctx_stream_buf_min[32]; /* floor: pinned loop, 0 if none */ + uint32_t ctx_stream_buf_cap[32]; /* no sample on the channel needs more */ xm_module_t module; uint32_t rate; @@ -325,7 +331,7 @@ struct xm_context_s { uint64_t channels_offset; }; - FILE* fh; /* open file for streaming content (if requested) */ + int fd; /* open file for streaming content (if requested) */ xm_effect_callback_t effect_callback; void *effect_callback_ctx; @@ -333,6 +339,8 @@ struct xm_context_s { xm_pattern_slot_t *slot_buffer; int slot_buffer_index; #endif + + bool external_samples; /* True if samples are external, false if embedded */ }; /* ----- Internal API ----- */ diff --git a/src/audio/mid64.c b/src/audio/mid64.c new file mode 100644 index 000000000..65c3c73a9 --- /dev/null +++ b/src/audio/mid64.c @@ -0,0 +1,485 @@ +/** + * @file mid64.c + * @author Giovanni Bajo + * @brief MID64 sequence loader, decoder, and MixerEvent player + * @ingroup mixer + */ +#include "mid64.h" +#include "mid64_internal.h" +#include "asset.h" +#include "audio.h" +#include "mixer.h" +#include "mixer_internal.h" +#include "profile.h" +#include "debug.h" +#include +#include +#include +#include + +/// @cond +#define MIDI_STATUS_MASK 0xF0 +#define MIDI_CHANNEL_MASK 0x0F +#define MIDI_STATUS_BIT 0x80 +#define MIDI_NOTE_OFF 0x80 +#define MIDI_NOTE_ON 0x90 +#define MIDI_POLY_PRESSURE 0xA0 +#define MIDI_CONTROL_CHANGE 0xB0 +#define MIDI_PROGRAM_CHANGE 0xC0 +#define MIDI_CHANNEL_PRESSURE 0xD0 +#define MIDI_PITCH_BEND 0xE0 +/// @endcond + +struct mid64player_s { + uint8_t *file_buf; ///< asset_load() buffer (header + events) + uint8_t *events; ///< Event stream within @ref file_buf + uint32_t events_size; + uint32_t cursor; + uint16_t ppqn; + uint8_t running_status; + uint32_t tempo_us; + uint32_t duration_ticks; + uint32_t duration_ms; + uint64_t midi_tick; + + uint32_t sample_rate; ///< From #audio_get_frequency at play + uint64_t timing_remainder; ///< Fractional samples from tick→sample + int64_t event_sample; ///< Absolute sample of last peeked delta + int64_t next_midi_sample; ///< Absolute sample of next MIDI event + int64_t scheduled_sample; ///< Absolute sample of current MixerEvent + int64_t soft_sample; ///< Absolute sample advanced by soft events + int64_t song_start_sample; ///< Absolute sample at start of this loop iter + bool pending; ///< VLQ peeked; payload not yet dispatched + + midi_target_t *target; + bool playing; + bool stop_requested; + bool soft_on; ///< Soft event currently registered + bool looping; +}; + +static inline uint8_t mid64_read_u8(mid64player_t *p) +{ + assertf(p->cursor < p->events_size, "MID64: truncated event stream"); + return p->events[p->cursor++]; +} + +static uint32_t mid64_read_vlq(mid64player_t *p) +{ + uint32_t v = 0; + for (int i = 0; i < 4; i++) { + uint8_t b = mid64_read_u8(p); + v = (v << 7) | (b & 0x7f); + if (!(b & 0x80)) return v; + } + assertf(0, "MID64: VLQ longer than 4 bytes"); +} + +static int channel_data_len(uint8_t status) +{ + switch (status & MIDI_STATUS_MASK) { + case MIDI_PROGRAM_CHANGE: + case MIDI_CHANNEL_PRESSURE: + return 1; + case MIDI_NOTE_OFF: + case MIDI_NOTE_ON: + case MIDI_POLY_PRESSURE: + case MIDI_CONTROL_CHANGE: + case MIDI_PITCH_BEND: + return 2; + default: + return -1; + } +} + +/** Convert @p delta ticks to samples; updates @ref mid64player_s::timing_remainder. */ +static uint64_t mid64_ticks_to_samples(mid64player_t *p, uint32_t delta) +{ + uint64_t den = (uint64_t)p->ppqn * 1000000ull; + // delta * tempo_us * rate can exceed 64 bits for large VLQs; use 128-bit. + __int128 num = (__int128)p->timing_remainder + + (__int128)delta * p->tempo_us * p->sample_rate; + uint64_t samples = (uint64_t)(num / den); + p->timing_remainder = (uint64_t)(num % den); + return samples; +} + +static void mid64_dispatch_channel(mid64player_t *p, midi_target_t *target, + uint8_t status, uint8_t data1, uint8_t data2, int64_t now) +{ + int ch = status & MIDI_CHANNEL_MASK; + const midi_target_ops_t *ops = target->ops; + + switch (status & MIDI_STATUS_MASK) { + case MIDI_NOTE_OFF: + ops->note_off(target, ch, data1, data2, now); + break; + case MIDI_NOTE_ON: + ops->note_on(target, ch, data1, data2, now); + break; + case MIDI_POLY_PRESSURE: + if (ops->poly_pressure) + ops->poly_pressure(target, ch, data1, data2, now); + break; + case MIDI_CONTROL_CHANGE: + ops->control_change(target, ch, data1, data2, now); + break; + case MIDI_PROGRAM_CHANGE: + ops->program_change(target, ch, data1, now); + break; + case MIDI_CHANNEL_PRESSURE: + if (ops->channel_pressure) + ops->channel_pressure(target, ch, data1, now); + break; + case MIDI_PITCH_BEND: + ops->pitch_bend(target, ch, data1 | (data2 << 7), now); + break; + } +} + +/** Peek next delta; leave the event payload for #mid64_dispatch_pending. */ +static void mid64_peek_next(mid64player_t *p) +{ + uint32_t delta = mid64_read_vlq(p); + p->midi_tick += delta; + p->event_sample += (int64_t)mid64_ticks_to_samples(p, delta); + p->next_midi_sample = p->event_sample; + p->pending = true; +} + +/** + * Dispatch the pending payload at sample @p now, then peek the following event. + * @return false if END was reached (no further peek) + */ +static bool mid64_dispatch_pending(mid64player_t *p, int64_t now) +{ + assert(p->pending); + p->pending = false; + + uint8_t b = mid64_read_u8(p); + if (b == MID64_OP_END) + return false; + + if (b == MID64_OP_SET_TEMPO) { + uint32_t tempo = ((uint32_t)mid64_read_u8(p) << 16) + | ((uint32_t)mid64_read_u8(p) << 8) + | mid64_read_u8(p); + assertf(tempo != 0, "MID64: tempo is zero"); + p->tempo_us = tempo; + p->running_status = 0; + mid64_peek_next(p); + return true; + } + + if (b == MID64_OP_GM1_SYSTEM_ON) { + p->running_status = 0; + if (p->target->ops->system_reset) + p->target->ops->system_reset(p->target, MIDI_SYSTEM_GM1, now); + mid64_peek_next(p); + return true; + } + + uint8_t status, data1; + if (b & MIDI_STATUS_BIT) { + status = b; + p->running_status = status; + data1 = mid64_read_u8(p); + } else { + assertf(p->running_status, "MID64: data byte without running status"); + status = p->running_status; + data1 = b; + } + + int dlen = channel_data_len(status); + assertf(dlen >= 0, "MID64: unsupported status 0x%02x", status); + uint8_t data2 = (dlen == 2) ? mid64_read_u8(p) : 0; + mid64_dispatch_channel(p, p->target, status, data1, data2, now); + mid64_peek_next(p); + return true; +} + +/** Restart timeline after END when looping (sample clock continues at @p now). */ +static void mid64_loop_restart(mid64player_t *p, int64_t now) +{ + p->cursor = 0; + p->running_status = 0; + p->tempo_us = MID64_DEFAULT_TEMPO; + p->midi_tick = 0; + p->timing_remainder = 0; + p->event_sample = now; + p->song_start_sample = now; + p->pending = false; + mid64_peek_next(p); +} + +static void mid64_soft(void *arg, int round_ns); + +/** Tear down playback state; caller must already have removed the hard MixerEvent. */ +static void mid64_do_stop(mid64player_t *p, int64_t now) +{ + if (p->soft_on) { + mixer_remove_soft_event(mid64_soft, p); + p->soft_on = false; + } + if (p->target->ops->reset) + p->target->ops->reset(p->target, now); + p->playing = false; + p->stop_requested = false; +} + +/** Per-round soft: advance synth envelopes without splitting mix rounds. */ +static void mid64_soft(void *arg, int round_ns) +{ + mid64player_t *p = arg; + if (!p->playing) + return; + + p->soft_sample += round_ns; + int64_t next = INT64_MAX; + PROFILE_START(PS_MID_PROCESS); + if (p->target->ops->process) + next = p->target->ops->process(p->target, p->soft_sample); + PROFILE_STOP(PS_MID_PROCESS); + + // MIDI finished: keep soft until release tails drain, then deregister. + if (p->next_midi_sample == INT64_MAX && next == INT64_MAX) { + p->playing = false; + p->soft_on = false; + mixer_remove_soft_event(mid64_soft, p); + } +} + +static int mid64_tick(void *arg) +{ + mid64player_t *p = arg; + int64_t now = p->scheduled_sample; + int delay; + + PROFILE_SCOPE(PS_MID_TICK) { + if (p->stop_requested) { + mid64_do_stop(p, now); + delay = 0; + goto done; + } + + // Soft may not have reached this hard boundary yet (e.g. first tick at 0). + if (p->soft_sample < now && p->target->ops->process) { + PROFILE_START(PS_MID_PROCESS); + p->target->ops->process(p->target, now); + PROFILE_STOP(PS_MID_PROCESS); + } + p->soft_sample = now; + + PROFILE_SCOPE(PS_MID_DISPATCH) { + while (p->next_midi_sample <= now) { + if (!mid64_dispatch_pending(p, now)) { + if (p->looping) { + if (p->target->ops->reset) + p->target->ops->reset(p->target, now); + mid64_loop_restart(p, now); + continue; + } + // Natural end: release musically; soft drains envelope tails. + if (p->target->ops->finish) + p->target->ops->finish(p->target, now); + p->next_midi_sample = INT64_MAX; + break; + } + } + } + + if (p->next_midi_sample == INT64_MAX) { + // Hard MIDI done; soft keeps running until the synth is idle. + delay = 0; + goto done; + } + + int64_t d = p->next_midi_sample - now; + assertf(d > 0, "MID64: non-positive mixer delay"); + if (d > INT_MAX) d = INT_MAX; + p->scheduled_sample = p->next_midi_sample; + delay = (int)d; +done: ; + } + return delay; +} + +mid64player_t *mid64player_load(const char *fn) +{ + int sz; + uint8_t *buf = asset_load(fn, &sz); + assertf(sz >= (int)sizeof(mid64_header_t), + "cannot load MID64 file: %s\nTruncated header", fn); + + mid64_header_t *head = (mid64_header_t *)buf; + assertf(memcmp(head->magic, MID64_ID, 4) == 0, + "cannot load MID64 file: %s\nInvalid magic", fn); + assertf(head->version == MID64_VERSION, + "cannot load MID64 file: %s\nVersion %d not supported", fn, head->version); + assertf(head->events_offset >= MID64_HEADER_SIZE + && head->events_offset + head->events_size <= (uint32_t)sz, + "cannot load MID64 file: %s\nInvalid event stream range", fn); + + mid64player_t *player = calloc(1, sizeof(*player)); + assert(player); + player->file_buf = buf; + player->events = buf + head->events_offset; + player->events_size = head->events_size; + player->ppqn = head->ppqn; + player->duration_ticks = head->duration_ticks; + player->duration_ms = head->duration_ms; + mid64player_rewind(player); + return player; +} + +void mid64player_close(mid64player_t *player) +{ + assert(player); + if (player->playing) { + mixer_remove_event(mid64_tick, player); + mid64_do_stop(player, player->scheduled_sample); + } + free(player->file_buf); + free(player); +} + +void mid64player_rewind(mid64player_t *player) +{ + assert(player); + player->cursor = 0; + player->running_status = 0; + player->tempo_us = MID64_DEFAULT_TEMPO; + player->midi_tick = 0; + player->pending = false; +} + +bool mid64player_decode_next(mid64player_t *player, midi_target_t *target) +{ + assert(player); + assert(target && target->ops); + + uint32_t delta = mid64_read_vlq(player); + player->midi_tick += delta; + int64_t now = (int64_t)player->midi_tick; + + uint8_t b = mid64_read_u8(player); + if (b == MID64_OP_END) + return false; + + if (b == MID64_OP_SET_TEMPO) { + uint32_t tempo = ((uint32_t)mid64_read_u8(player) << 16) + | ((uint32_t)mid64_read_u8(player) << 8) + | mid64_read_u8(player); + assertf(tempo != 0, "MID64: tempo is zero"); + player->tempo_us = tempo; + player->running_status = 0; + return true; + } + + if (b == MID64_OP_GM1_SYSTEM_ON) { + player->running_status = 0; + if (target->ops->system_reset) + target->ops->system_reset(target, MIDI_SYSTEM_GM1, now); + return true; + } + + uint8_t status, data1; + if (b & MIDI_STATUS_BIT) { + status = b; + player->running_status = status; + data1 = mid64_read_u8(player); + } else { + assertf(player->running_status, "MID64: data byte without running status"); + status = player->running_status; + data1 = b; + } + + int dlen = channel_data_len(status); + assertf(dlen >= 0, "MID64: unsupported status 0x%02x", status); + uint8_t data2 = (dlen == 2) ? mid64_read_u8(player) : 0; + mid64_dispatch_channel(player, target, status, data1, data2, now); + return true; +} + +void mid64player_play(mid64player_t *player, midi_target_t *target) +{ + assert(player); + assert(target && target->ops); + if (player->playing) + return; + + player->sample_rate = audio_get_frequency(); + assertf(player->sample_rate > 0, "MID64: call audio_init before play"); + + player->target = target; + player->stop_requested = false; + mid64player_rewind(player); + player->timing_remainder = 0; + player->event_sample = 0; + player->scheduled_sample = 0; + player->soft_sample = 0; + player->song_start_sample = 0; + mid64_peek_next(player); + + if (target->ops->reset) + target->ops->reset(target, 0); + + player->playing = true; + player->soft_on = true; + mixer_add_soft_event(mid64_soft, player); + mixer_add_event(0, mid64_tick, player); +} + +void mid64player_stop(mid64player_t *player) +{ + assert(player); + if (!player->playing) + return; + player->stop_requested = true; + // Pull the callback forward so a distant next MIDI event cannot delay stop. + mixer_remove_event(mid64_tick, player); + mixer_add_event(0, mid64_tick, player); +} + +void mid64player_set_loop(mid64player_t *player, bool loop) +{ + assert(player); + player->looping = loop; +} + +uint16_t mid64player_get_ppqn(mid64player_t *player) +{ + assert(player); + return player->ppqn; +} + +uint32_t mid64player_get_duration_ticks(mid64player_t *player) +{ + assert(player); + return player->duration_ticks; +} + +uint32_t mid64player_get_duration_ms(mid64player_t *player) +{ + assert(player); + return player->duration_ms; +} + +uint32_t mid64player_get_tempo(mid64player_t *player) +{ + assert(player); + return player->tempo_us; +} + +uint32_t mid64player_tell_ms(mid64player_t *player) +{ + assert(player); + int64_t samples = player->scheduled_sample - player->song_start_sample; + uint32_t rate = player->sample_rate; + if (rate == 0 || samples <= 0) + return 0; + uint64_t ms = (uint64_t)samples * 1000ull / rate; + if (ms > 0xffffffffu) + return 0xffffffffu; + return (uint32_t)ms; +} diff --git a/src/audio/mid64_internal.h b/src/audio/mid64_internal.h new file mode 100644 index 000000000..fe4ea5496 --- /dev/null +++ b/src/audio/mid64_internal.h @@ -0,0 +1,39 @@ +/** + * @file mid64_internal.h + * @author Giovanni Bajo + * @brief On-disk layout of the MID64 sequence format + * @ingroup mixer + * + * See tools/audioconv64/conv_mid64.cpp for the encoder. + */ +#ifndef LIBDRAGON_MID64_INTERNAL_H +#define LIBDRAGON_MID64_INTERNAL_H + +#include + +#define MID64_ID "MD64" ///< MID64 file identifier +#define MID64_VERSION 1 ///< Current format version +#define MID64_HEADER_SIZE 32 ///< Size of #mid64_header_t +#define MID64_DEFAULT_TEMPO 500000 ///< Default μs/quarter (120 BPM) + +#define MID64_OP_SET_TEMPO 0xF0 ///< Set tempo (3-byte μs/quarter) +#define MID64_OP_GM1_SYSTEM_ON 0xF1 ///< General MIDI Level 1 System On +#define MID64_OP_END 0xFF ///< End of sequence + +/** @brief File header (big-endian on disk; native on N64). */ +typedef struct __attribute__((packed)) { + char magic[4]; ///< Magic (#MID64_ID) + uint8_t version; ///< Format version (#MID64_VERSION) + uint8_t flags; ///< Reserved flags + uint16_t ppqn; ///< Pulses per quarter note + uint32_t events_offset; ///< Byte offset of the event stream + uint32_t events_size; ///< Size of the event stream in bytes + uint32_t num_events; ///< Number of events including END + uint32_t duration_ticks; ///< Sequence length in ticks + uint32_t duration_ms; ///< Wall-clock length (tempo map), milliseconds + uint32_t reserved; ///< Reserved +} mid64_header_t; + +_Static_assert(sizeof(mid64_header_t) == MID64_HEADER_SIZE, "mid64_header_t size"); + +#endif diff --git a/src/audio/mixer.c b/src/audio/mixer.c index 089dd4759..7fc80db9b 100644 --- a/src/audio/mixer.c +++ b/src/audio/mixer.c @@ -1,7 +1,7 @@ /** * @file mixer.c * @author Giovanni Bajo - * @brief RSP Audio mixer + * @brief RSP Audio mixer * @ingroup mixer */ @@ -16,6 +16,10 @@ #include "audio.h" #include "n64sys.h" #include "interrupt.h" +#include "profile.h" +#include "fmath.h" +#include "accounting_internal.h" +#include "../rspq/rspq_internal.h" #include #include #include @@ -33,40 +37,74 @@ #define tracef(fmt, ...) ({ }) #endif -/** - * @name AI Status Register Values - * @{ - */ -/** @brief Bit representing that the AI is busy */ -#define AI_STATUS_BUSY ( 1 << 30 ) -/** @brief Bit representing that the AI is full */ -#define AI_STATUS_FULL ( 1 << 31 ) -/** @} */ - /** @brief Maximum number of mixer events */ #define MAX_EVENTS 32 -/** @brief Number of expected #mixer_poll calls per second +/** @brief Maximum number of soft (per-round) mixer callbacks */ +#define MAX_SOFT_EVENTS 8 +/** @brief Fallback samplebuffer depth when #audio_init has not been called yet. * - * This is used to allocate memory for the sample buffers - * according to the expected number of samples that must - * be calculated and held in memory. + * Sizing normally follows the AI queue (#audio_get_num_buffers × + * #audio_get_buffer_length). This rate is only the fallback and the + * #mixer_throttle extra, so it stays conservative. */ #define MIXER_POLL_PER_SECOND 8 +/** @brief Polls the sample buffers are sized to hold at once. + * + * One is the minimum: a single #mixer_poll_async must never have to wait for + * the RSP, whatever the number of rounds it splits into. The second one is + * what keeps the CPU from waiting in practice, since by the time it starts a + * poll the RSP is normally done with the one before the last (see + * #__mixer_inflight_samples). + */ +#define MIXER_POLL_LOOKAHEAD 2 + /** * RSP mixer ucode (rsp_mixer.S) */ DEFINE_RSP_UCODE(rsp_mixer); -/** @brief Size of the ucode state that is automatically persisted by rspq */ -#define MIXER_STATE_SIZE 128 +/** @brief Size of the ucode state that is automatically persisted by rspq. + * Layout must match RSPQ_BeginSavedState in rsp_mixer.S: just the per-channel + * VADPCM pointer table. + * ACCUM lives in .bss (not saved state) so the VADPCM bssovl1 bank still fits. + */ +#define MIXER_STATE_SIZE 384 + +/** @brief Max output samples per mix round (must match rsp_mixer.S). */ +#define MIXER_MAX_SAMPLES_PER_ROUND 512 + +/** + * @brief Length of the ramp #mixer_ch_set_vol spreads a volume change over. + * + * A volume that jumps from one sample to the next is a step in the waveform, + * and a step is heard as a click. Music players change volumes constantly (an + * XM does it on every tick, on every channel), so the change is walked over a + * few milliseconds instead. That is short enough that nothing sounds delayed, + * and it replaces the one-pole filter the ucode used to run on every channel. + * #mixer_ch_set_vol_ramp is left alone: a caller that names a duration means + * it, zero included. + */ +#define MIXER_DECLICK_SAMPLES 128 // NOTE: keep these in sync with rsp_mixer.S #define CH_FLAGS_BPS_SHIFT (3<<0) ///< BPS shift value #define CH_FLAGS_16BIT (1<<2) ///< Set if the channel is 16 bit #define CH_FLAGS_STEREO (1<<3) ///< Set if the channel is stereo (left) #define CH_FLAGS_STEREO_SUB (1<<4) ///< The channel is the second half of a stereo (right) -#define CH_FLAGS_STEREO_ALLOC (1<<5) ///< The channel has a buffer sized for stereo +#define CH_FLAGS_VADPCM (1<<5) ///< In-mixer VADPCM mono (wire + CPU) +#define CH_FLAGS_VLOOP_STATE (1<<6) ///< VADPCM: decode from the loop-start state (wire only, see mixer_emit_channel) +#define CH_FLAGS_CLEAR_ACCUM (1<<7) ///< Zero ACCUM before mixing (first MIX_CHANNEL of a round) +#define CH_FLAGS_RESIDENT (1<<8) ///< Channel plays from waveform->mem (no samplebuffer) +#define CH_FLAGS_LOOP_CACHED (1<<10) ///< Streamed loop pinned in the samplebuffer; RSP wraps +#define CH_FLAGS_STEREO_ALLOC (1<<9) ///< The channel has a buffer sized for stereo (CPU-side only) +#define CH_FLAGS_FORCE_MONO (1<<11) ///< Fold this channel's output to both buses (mono downmix). CPU-side only; RSP ucode ignores this bit. + +#define MIXER_CMD_CHANNEL 0x0 ///< rspq command ID for channel setup +#define MIXER_CMD_SETCHANNEL 0x1 ///< rspq command ID for setting a channel +#define MIXER_CMD_FLUSH 0x2 ///< rspq command ID for flushing the mixer +#define MIXER_CMD_SETSTATE 0x3 ///< rspq command ID for seeding a VADPCM state +#define MIXER_CMD_COPY 0x4 ///< rspq command ID for an RDRAM to RDRAM copy /// @brief Fixed point value used in waveform position calculations. /// This is a signed 64-bit integer with the fractional part using @@ -87,52 +125,101 @@ typedef int16_t mixer_fx15_t; /// Convert a floating point value to #mixer_fx15_t #define MIXER_FX15(f) (int16_t)((f) * ((1< 0) + float k; ///< Exp curve: log2(end/start); silence asymptote if end≤0 + mixer_ramp_fn_t fn; ///< Curve (ignored when #duration is 0) +} mixer_ramp_t; + +/** Fade-to-silence exponent for #mixer_ramp_exp when the target is 0. */ +#define MIXER_RAMP_EXP_SILENCE 9.226f +/** log2(e): converts the natural silence asymptote into an #fm_exp2f argument. */ +#define MIXER_LOG2E 1.44269504f + +float mixer_ramp_linear(float start, float end, float u) +{ + return start + (end - start) * u; +} + +float mixer_ramp_exp(float start, float end, float u) +{ + if (start <= 0.0f) + return end * u; + if (end <= 0.0f) + return start * fm_exp2f(u * -(MIXER_RAMP_EXP_SILENCE * MIXER_LOG2E)); + return start * fm_exp2f(u * log2f(end / start)); +} + +/** @brief Volumes a channel is mixed with over one round. * - * This struct reflects the settings defined in rsp_mixer.S. + * The ucode is given a slope and the value the slope is heading for: it walks + * the volume one output sample at a time and clamps it between the two ends of + * the ramp, which is what lets a ramp end in the middle of a round. With no + * ramp running the target equals the starting volume and the clamp holds it + * there. */ -typedef struct rsp_mixer_settings_s { - uint32_t lvol[MIXER_MAX_CHANNELS/2] __attribute__((aligned(16))); - uint32_t rvol[MIXER_MAX_CHANNELS/2]; - rsp_mixer_channel_t channels[MIXER_MAX_CHANNELS] __attribute__((aligned(16))); -} rsp_mixer_settings_t; +typedef struct { + mixer_fx15_t l; ///< Left volume at the first sample of the round + mixer_fx15_t r; ///< Right volume at the first sample of the round + mixer_fx15_t dl; ///< Left increment per block of 4 samples + mixer_fx15_t dr; ///< Right increment per block of 4 samples + mixer_fx15_t tl; ///< Left volume the round ends on + mixer_fx15_t tr; ///< Right volume the round ends on +} mixer_round_vol_t; + +/** @brief Overlay saved-state layout (must match rsp_mixer.S) */ +typedef struct { + uint32_t codebook[MIXER_MAX_CHANNELS]; + uint32_t state[MIXER_MAX_CHANNELS]; + uint32_t loop_state[MIXER_MAX_CHANNELS]; +} mixer_overlay_state_t; + +_Static_assert(sizeof(mixer_overlay_state_t) == MIXER_STATE_SIZE, "mixer overlay state size mismatch"); -/** @brief Configured limits of a mixer channel. +/** @brief Shadow of the VADPCM pointers currently held by the ucode table. */ +typedef struct { + void *codebook; + void *state; + void *loop_state; +} mixer_chtbl_t; + +/** @brief Configured limits of a mixer channel. * * This structure describes the playback limits for a mixer channel. The limits * are used to avoid over-allocating memory via sample buffers. @@ -150,81 +237,258 @@ typedef struct { void *ctx; ///< Opaque context pointer to pass to the callback } mixer_event_t; +/** @brief Per-round soft callback (does not split mix rounds) */ +typedef struct { + MixerSoftEvent cb; + void *ctx; +} mixer_soft_event_t; + static struct { uint32_t sample_rate; int num_channels; float vol; float max_samples; bool throttled; + uint32_t uuid_counter; int64_t ticks; int num_events; mixer_event_t events[MAX_EVENTS]; + int num_soft; + mixer_soft_event_t soft[MAX_SOFT_EVENTS]; samplebuffer_t ch_buf[MIXER_MAX_CHANNELS]; channel_limit_t limits[MIXER_MAX_CHANNELS]; mixer_channel_t channels[MIXER_MAX_CHANNELS]; - mixer_fx15_t lvol[MIXER_MAX_CHANNELS]; - mixer_fx15_t rvol[MIXER_MAX_CHANNELS]; - - rsp_mixer_settings_t ucode_settings __attribute__((aligned(16))); + mixer_chtbl_t chtbl[MIXER_MAX_CHANNELS]; + float lvol[MIXER_MAX_CHANNELS]; ///< Target left volume [-1..1] (signed for Dolby) + float rvol[MIXER_MAX_CHANNELS]; ///< Target right volume [-1..1] (signed for Dolby) + float gain[MIXER_MAX_CHANNELS]; ///< Target mono gain [0..1] (default 1) + float freq[MIXER_MAX_CHANNELS]; ///< Target playback frequency (Hz) + mixer_ramp_t lvol_ramp[MIXER_MAX_CHANNELS]; + mixer_ramp_t rvol_ramp[MIXER_MAX_CHANNELS]; + mixer_ramp_t gain_ramp[MIXER_MAX_CHANNELS]; + mixer_ramp_t freq_ramp[MIXER_MAX_CHANNELS]; + uint8_t prio[MIXER_MAX_CHANNELS]; ///< Voice-stealing priority + int64_t start_tick[MIXER_MAX_CHANNELS]; ///< Absolute tick of last #mixer_ch_play + + uint32_t chtbl_dirty; ///< VADPCM channels whose SETCHANNEL is out of date + uint32_t vstate_dirty; ///< VADPCM channels the CPU has re-seeded (see #mixer_vstate) + void *vstates; ///< Decoder states owned by the RSP, 16 bytes per channel + int hi_ch; ///< Exclusive upper bound of channels to scan + + uint32_t round_id; ///< Id of the last round emitted + volatile uint32_t *round_done; ///< Last round the RSP finished, written by MIX_FLUSH + uint32_t poll_round[MIXER_POLL_LOOKAHEAD]; ///< Last round of each recent poll + uint32_t poll_count; ///< Polls done so far (indexes #poll_round) } Mixer; -/** @brief Count of ticks spent in mixer RSP, used for debugging purposes. */ -int64_t __mixer_profile_rsp = 0; - uint32_t __mixer_overlay_id; +void __mixer_profile_init(void) { + profile_register(PS_MIXER, "mixer_try_play", 0); + profile_register(PS_XM_TICK, "xm_tick", 1); + profile_register(PS_XM_GETPOS, "xm_getpos", 2); + profile_register(PS_XM_LIBXM, "xm_libxm", 2); + profile_register(PS_XM_SYNC, "xm_sync", 2); + profile_register(PS_MID_TICK, "mid_tick", 1); + profile_register(PS_MID_PROCESS, "mid_process", 2); + profile_register(PS_MID_DISPATCH, "mid_dispatch", 2); + profile_register(PS_MIXER_EXEC, "mixer_exec", 1); + profile_register(PS_MIXER_PREP, "prep", 2); + profile_register(PS_MIXER_EMIT, "emit", 2); + profile_register(PS_MIXER_FETCH, "fetch", 3); + profile_register(PS_SBUF_GET, "sbuf_get", 4); + profile_register(PS_VADPCM_READ, "vadpcm_read", 5); + profile_register(PS_VADPCM_HUFF, "vadpcm_huff", 6); + profile_register(PS_VADPCM_IO, "vadpcm_io", 6); + profile_register(PS_MIXER_CMD, "cmd", 3); + profile_register(PS_MIXER_VOL, "vol", 4); + profile_register(PS_MIXER_DMAWAIT,"dma_wait", 4); + profile_register(PS_MIXER_RSPQ, "rspq", 4); + profile_register(PS_MIXER_ADVANCE,"advance", 2); + profile_register(PS_MIXER_SEEK, "ch_seek", 3); + profile_register(PS_MIXER_PREFETCH,"prefetch", 2); +} + +static inline uint32_t mixer_bit(int ch) { return 1u << ch; } + +/** Grow the exclusive scan limit so channel @p ch is included. */ +static inline void mixer_touch_ch(int ch) { + if (ch + 1 > Mixer.hi_ch) + Mixer.hi_ch = ch + 1; +} + +/** Recompute #mixer_channel_t.max_round_ns from step and samplebuffer margin. */ +static void mixer_refresh_max_ns(int ch); + static inline int mixer_initialized(void) { return Mixer.num_channels != 0; } void mixer_init(int num_channels) { memset(&Mixer, 0, sizeof(Mixer)); - data_cache_hit_writeback_invalidate(&Mixer.ucode_settings, sizeof(Mixer.ucode_settings)); Mixer.num_channels = num_channels; Mixer.sample_rate = audio_get_frequency(); // actual sample rate obtained via DAC clock assertf(Mixer.sample_rate > 0, "audio_init() must be called before mixer_init()"); + // Size AI buffers as a multiple of the mixer round cap so mixer_poll does + // not end on a remainder. + audio_set_buffer_granularity(MIXER_MAX_SAMPLES_PER_ROUND); Mixer.vol = 1.0f; for (int ch=0;chformat == WAVEFORM_FORMAT_VADPCM && wave->codec); + return VADPCM_FRAME_BYTES(((const waveform_vadpcm_t*)wave->codec)->bits); +} - // Multiply by number of channels - nsamples *= nchannels; +/** + * @brief Output samples the CPU may mix without ever waiting for the RSP. + * + * A poll enqueues its rounds and returns, so the input windows it handed out + * stay live until the RSP gets to them: sample buffers are sized to span that + * much input (#MIXER_POLL_LOOKAHEAD polls of it). The next poll waits for the + * oldest of those before starting (see #mixer_poll_async). + */ +int __mixer_inflight_samples(void) +{ + int blen = audio_get_buffer_length(); + if (blen > 0) + return blen * MIXER_POLL_LOOKAHEAD; + int rate = Mixer.sample_rate ? (int)Mixer.sample_rate : audio_get_frequency(); + return rate / MIXER_POLL_PER_SECOND; +} + +bool __mixer_round_done(uint32_t id) +{ + if (!Mixer.round_done) + return true; + return (int32_t)(*Mixer.round_done - id) >= 0; +} - // Calculate buffer size according to number of expected polls per second. - int64_t size = ROUND_UP((int64_t)ceilf((float)nsamples / (float)MIXER_POLL_PER_SECOND), 8); +void __mixer_round_wait(uint32_t id) +{ + if (__mixer_round_done(id)) + return; + rspq_flush(); + ACCT_SCOPE(ACCT_CAT_RSPQ) RSP_WAIT_LOOP(200) { + if (__mixer_round_done(id)) + break; + } +} + +/** True if starting another poll would have the CPU wait for the RSP. */ +static bool mixer_poll_would_wait(void) +{ + if (Mixer.poll_count < MIXER_POLL_LOOKAHEAD) + return false; + return !__mixer_round_done(Mixer.poll_round[Mixer.poll_count % MIXER_POLL_LOOKAHEAD]); +} + +/** Wait until starting another poll would not overwrite live sample windows. */ +static void mixer_poll_barrier(void) +{ + if (Mixer.poll_count < MIXER_POLL_LOOKAHEAD) + return; + __mixer_round_wait(Mixer.poll_round[Mixer.poll_count % MIXER_POLL_LOOKAHEAD]); +} + +static int mixer_calc_buffer_size(int ch, waveform_t *wave) +{ + bool vadpcm = wave->format == WAVEFORM_FORMAT_VADPCM; + int ub = vadpcm ? mixer_vadpcm_frame_bytes(wave) + : (Mixer.limits[ch].max_bits / 8) * wave->channels; + + int64_t out_samples = __mixer_inflight_samples(); + int64_t nsamples = (int64_t)ceilf((float)out_samples * + (Mixer.limits[ch].max_frequency / (float)Mixer.sample_rate)); + + int64_t nunits = vadpcm ? DIVIDE_CEIL(nsamples, 16) : nsamples; + + // Plus what sits between the oldest window the RSP still has to read and + // the write cursor: the window of the round being emitted now, and the + // units #samplebuffer_prefetch has already pulled in past it. Both are + // capped at a margin. + nunits += SAMPLEBUFFER_MARGIN_UNITS * 2; + + // Block codecs (Opus, ULC) append one whole frame at a time, however few + // samples were asked for, so the ring needs room for one more of those on + // top of everything above. The mirrored tail is also enlarged to cover a + // whole frame (see #samplebuffer_t::margin_units); that extra is allocated + // by the caller (#mixer_ch_play), not here. + int f = wave->append_units; + if (f > SAMPLEBUFFER_MARGIN_UNITS) + nunits += f; + + if (nunits < SAMPLEBUFFER_MARGIN_UNITS * 2) + nunits = SAMPLEBUFFER_MARGIN_UNITS * 2; + + int64_t size = ROUND_UP(nunits * ub, 8); - // If we're over the allowed maximum, clamp to it if (Mixer.limits[ch].max_buf_sz && size > Mixer.limits[ch].max_buf_sz) size = Mixer.limits[ch].max_buf_sz; + // samplebuffer needs ≥MARGIN units of usable space plus the mirrored tail. + // The mirrored tail itself is allocated by the caller (#mixer_ch_play). + int min_bytes = SAMPLEBUFFER_MARGIN_UNITS * 2 * ub; + if (size < min_bytes) + size = min_bytes; + assert((size % 8) == 0); assert((int32_t)size == size); - + return size; } +/** Mirrored-tail size in bytes for a waveform (see #samplebuffer_t::margin_units). */ +static int mixer_margin_bytes(waveform_t *wave, int ub) +{ + return samplebuffer_margin_units(wave->append_units) * ub; +} + void mixer_set_vol(float vol) { Mixer.vol = vol; } @@ -232,39 +496,230 @@ void mixer_set_vol(float vol) { void mixer_close(void) { assert(mixer_initialized()); + rspq_highpri_sync(); + rspq_overlay_unregister(__mixer_overlay_id); __mixer_overlay_id = 0; for (int i=0; iflags & CH_FLAGS_RESIDENT) && c->codec_state) { + free_uncached(c->codec_state); + c->codec_state = NULL; + } if (samplebuffer_is_inited(&Mixer.ch_buf[i])) samplebuffer_close(&Mixer.ch_buf[i]); } + if (Mixer.vstates) { + free_uncached(Mixer.vstates); + Mixer.vstates = NULL; + } + + if (Mixer.round_done) { + free_uncached((void*)Mixer.round_done); + Mixer.round_done = NULL; + } + Mixer.num_channels = 0; } -void mixer_ch_set_freq(int ch, float frequency) { +/** Value of a scalar ramp at absolute tick @p t (target @p end). */ +static float mixer_ramp_at(const mixer_ramp_t *rmp, float end, int64_t t) +{ + int64_t d = t - rmp->start_tick; + if (!rmp->duration || d >= rmp->duration) + return end; + if (d < 0) d = 0; + float u = (float)d * rmp->inv_duration; + if (rmp->fn == mixer_ramp_linear) + return rmp->start + (end - rmp->start) * u; + if (rmp->fn == mixer_ramp_exp) + return rmp->start > 0.0f ? rmp->start * fm_exp2f(u * rmp->k) : end * u; + return rmp->fn(rmp->start, end, u); +} + +/** Arm or replace a scalar ramp toward @p target over @p duration (0 = now). + * @p started_ago is how many samples before #Mixer.ticks the ramp began. */ +static void mixer_ramp_to(mixer_ramp_t *rmp, float *end, float target, + int duration, mixer_ramp_fn_t fn, int started_ago) +{ + assertf(started_ago >= 0, "mixer ramp: negative started_ago %d", started_ago); + int64_t start_tick = Mixer.ticks - started_ago; + if (*end == target && !rmp->duration) + return; + float cur = mixer_ramp_at(rmp, *end, start_tick); + if (duration && cur != target) { + assertf(fn, "mixer ramp: NULL curve with duration %d", duration); + float k = 0.0f; + if (fn == mixer_ramp_exp && cur > 0.0f) { + k = (target <= 0.0f) + ? -(MIXER_RAMP_EXP_SILENCE * MIXER_LOG2E) + : log2f(target / cur); + } + *rmp = (mixer_ramp_t){ + .start = cur, + .start_tick = start_tick, + .duration = duration, + .inv_duration = 1.0f / (float)duration, + .k = k, + .fn = fn, + }; + } else { + rmp->duration = 0; + } + *end = target; +} + +/** True while @p rmp still has samples to run at @p t; retires it when done. */ +static bool mixer_ramp_active(mixer_ramp_t *rmp, int64_t t) +{ + if (!rmp->duration) + return false; + if (t < rmp->start_tick + rmp->duration) + return true; + rmp->duration = 0; + return false; +} + +/** Push @p frequency (Hz) into #mixer_channel_t::step for channel @p ch. */ +static void mixer_ch_apply_freq(int ch, float frequency) +{ mixer_channel_t *c = &Mixer.channels[ch]; - assertf(!(c->flags & CH_FLAGS_STEREO_SUB), "cannot call on secondary stereo channel %d", ch); assertf(frequency >= 0, "cannot set negative frequency on channel %d: %f", ch, frequency); - // Check if the frequency is within the configured limit. Allow for a 1% margin because of rounding errors - // for default maximum frequency being the output sample rate converted from fixed point. - assertf(frequency <= Mixer.limits[ch].max_frequency*1.01, "frequency %.1f exceeds configured limit %.1f on channel %d; use mixer_ch_set_limit to change the limit for this channel", frequency, Mixer.limits[ch].max_frequency, ch); - c->step = MIXER_FX64(frequency / (float)Mixer.sample_rate) << (c->flags & CH_FLAGS_BPS_SHIFT); + // Check if the frequency is within the configured limit. Allow for a 1% + // margin because of rounding errors for the default maximum frequency. + assertf(frequency <= Mixer.limits[ch].max_frequency * 1.01f, + "frequency %.1f exceeds configured limit %.1f on channel %d; use mixer_ch_set_limit to change the limit for this channel", + frequency, Mixer.limits[ch].max_frequency, ch); + mixer_fx64_t step = MIXER_FX64(frequency / (float)Mixer.sample_rate); + if (!(c->flags & CH_FLAGS_VADPCM)) + step <<= (c->flags & CH_FLAGS_BPS_SHIFT); + if (c->step == step) + return; + c->step = step; + mixer_refresh_max_ns(ch); +} + +/** Sample the freq ramp at @p tick into the channel step (owners only). */ +static void mixer_ch_sync_freq(int ch, int64_t tick) +{ + mixer_channel_t *c = &Mixer.channels[ch]; + if (c->flags & CH_FLAGS_STEREO_SUB) + return; + float f = mixer_ramp_at(&Mixer.freq_ramp[ch], Mixer.freq[ch], tick); + mixer_ramp_active(&Mixer.freq_ramp[ch], tick); + mixer_ch_apply_freq(ch, f); +} + +void mixer_ch_set_freq(int ch, float frequency) +{ + mixer_channel_t *c = &Mixer.channels[ch]; + assertf(!(c->flags & CH_FLAGS_STEREO_SUB), "cannot call on secondary stereo channel %d", ch); + mixer_ramp_to(&Mixer.freq_ramp[ch], &Mixer.freq[ch], frequency, 0, + mixer_ramp_linear, 0); + mixer_ch_apply_freq(ch, Mixer.freq[ch]); +} + +void mixer_ch_set_freq_ramp(int ch, float frequency, int duration, + mixer_ramp_fn_t curve, int started_ago) +{ + mixer_channel_t *c = &Mixer.channels[ch]; + assertf(!(c->flags & CH_FLAGS_STEREO_SUB), + "mixer_ch_set_freq_ramp: cannot call on secondary stereo channel %d", ch); + assertf(duration >= 0, + "mixer_ch_set_freq_ramp: negative duration %d on channel %d", duration, ch); + assertf(started_ago >= 0, + "mixer_ch_set_freq_ramp: negative started_ago %d on channel %d", started_ago, ch); + mixer_ramp_to(&Mixer.freq_ramp[ch], &Mixer.freq[ch], frequency, duration, curve, + started_ago); + mixer_ch_apply_freq(ch, + mixer_ramp_at(&Mixer.freq_ramp[ch], Mixer.freq[ch], Mixer.ticks)); +} + +static void mixer_ch_vol_at(int ch, int64_t t, mixer_fx15_t *lvol, mixer_fx15_t *rvol) +{ + *lvol = MIXER_FX15(mixer_ramp_at(&Mixer.lvol_ramp[ch], Mixer.lvol[ch], t)); + *rvol = MIXER_FX15(mixer_ramp_at(&Mixer.rvol_ramp[ch], Mixer.rvol[ch], t)); +} + +static void mixer_ch_vol_to(int ch, float l, float r, int duration) +{ + // Range [-1..1]: negative volumes invert phase. Dolby Pro Logic II matrix + // encoding needs that for the surrounds; the RSP already treats L/R as + // Q15 signed. + if (l < -1.0f) l = -1.0f; + if (l > 1.0f) l = 1.0f; + if (r < -1.0f) r = -1.0f; + if (r > 1.0f) r = 1.0f; + mixer_ramp_to(&Mixer.lvol_ramp[ch], &Mixer.lvol[ch], l, duration, + mixer_ramp_linear, 0); + mixer_ramp_to(&Mixer.rvol_ramp[ch], &Mixer.rvol[ch], r, duration, + mixer_ramp_linear, 0); } void mixer_ch_set_vol(int ch, float lvol, float rvol) { mixer_channel_t *c = &Mixer.channels[ch]; assertf(!(c->flags & CH_FLAGS_STEREO_SUB), "mixer_ch_set_vol: cannot call on secondary stereo channel %d", ch); - Mixer.lvol[ch] = MIXER_FX15(lvol); - Mixer.rvol[ch] = MIXER_FX15(rvol); + mixer_ch_vol_to(ch, lvol, rvol, MIXER_DECLICK_SAMPLES); +} + +void mixer_ch_set_vol_ramp(int ch, float lvol, float rvol, int duration) { + mixer_channel_t *c = &Mixer.channels[ch]; + assertf(!(c->flags & CH_FLAGS_STEREO_SUB), "mixer_ch_set_vol_ramp: cannot call on secondary stereo channel %d", ch); + assertf(duration >= 0, "mixer_ch_set_vol_ramp: negative duration %d on channel %d", duration, ch); + mixer_ch_vol_to(ch, lvol, rvol, duration); +} + +void mixer_ch_set_gain(int ch, float gain) +{ + mixer_channel_t *c = &Mixer.channels[ch]; + assertf(!(c->flags & CH_FLAGS_STEREO_SUB), + "mixer_ch_set_gain: cannot call on secondary stereo channel %d", ch); + if (gain < 0.0f) gain = 0.0f; + if (gain > 1.0f) gain = 1.0f; + mixer_ramp_to(&Mixer.gain_ramp[ch], &Mixer.gain[ch], gain, 0, + mixer_ramp_linear, 0); +} + +void mixer_ch_set_gain_ramp(int ch, float gain, int duration, + mixer_ramp_fn_t curve, int started_ago) +{ + mixer_channel_t *c = &Mixer.channels[ch]; + assertf(!(c->flags & CH_FLAGS_STEREO_SUB), + "mixer_ch_set_gain_ramp: cannot call on secondary stereo channel %d", ch); + assertf(duration >= 0, + "mixer_ch_set_gain_ramp: negative duration %d on channel %d", duration, ch); + assertf(started_ago >= 0, + "mixer_ch_set_gain_ramp: negative started_ago %d on channel %d", started_ago, ch); + if (gain < 0.0f) gain = 0.0f; + if (gain > 1.0f) gain = 1.0f; + mixer_ramp_to(&Mixer.gain_ramp[ch], &Mixer.gain[ch], gain, duration, curve, + started_ago); } void mixer_ch_set_vol_pan(int ch, float vol, float pan) { mixer_ch_set_vol(ch, vol * (1.f - pan), vol * pan); } +void mixer_ch_set_force_mono(int ch, bool enable) { + assert(ch < Mixer.num_channels); + if (enable) Mixer.channels[ch].flags |= CH_FLAGS_FORCE_MONO; + else Mixer.channels[ch].flags &= ~CH_FLAGS_FORCE_MONO; +} + +bool mixer_ch_get_force_mono(int ch) { + assert(ch < Mixer.num_channels); + return (Mixer.channels[ch].flags & CH_FLAGS_FORCE_MONO) != 0; +} + +void mixer_set_force_mono(bool enable) { + for (int i = 0; i < Mixer.num_channels; i++) { + mixer_ch_set_force_mono(i, enable); + } +} + void mixer_ch_set_vol_dolby(int ch, float fl, float fr, float c, float sl, float sr) { @@ -300,179 +755,798 @@ static int waveform_wrap_wpos(int wpos, int len, int loop_len) { return ((wpos - len) % loop_len) + (len - loop_len); } -// A wrapper for a waveform's read function that handles loops. -// Sample buffers are not aware of loops. The way the mixer handles -// loops is by unrolling them in the sample buffer: that is, the sample -// buffer is called with an unlimited growing wpos, and the -// WaveformRead callback is expected to unroll the loop as wpos -// grows. To alleviate all waveforms implementations to handle loop -// unrolling, this simple wrapper performs the wpos wrapping calculations -// and convert it in a sequence of calls to read callbacks using only positions -// in the range [0, len]. +/** Exclusive end of the loop region of a waveform (0 means terminal loop). */ +static int waveform_loop_end(const waveform_t *wave) { + return wave->loop_end ? wave->loop_end : wave->len; +} + +/** + * @brief Set the bounds a channel plays within: the loop region, or all of it. + * + * While looping, the channel ends at the loop end and wraps back by loop_len; + * otherwise it runs to the physical end of the waveform (which includes the + * release tail of a waveform whose loop ends before it). + */ +static void mixer_ch_bounds(mixer_channel_t *c, const waveform_t *wave, bool loop) { + int bps = (c->flags & CH_FLAGS_VADPCM) ? 0 : (c->flags & CH_FLAGS_BPS_SHIFT); + loop = loop && wave->loop_len; + c->len = MIXER_FX64((int64_t)(loop ? waveform_loop_end(wave) : wave->len)) << bps; + c->loop_len = loop ? MIXER_FX64((int64_t)wave->loop_len) << bps : 0; + // The secondary channel of a stereo pair is played with the same bounds. + if (c->flags & CH_FLAGS_STEREO) { + c[1].len = c->len; + c[1].loop_len = c->loop_len; + } +} + +// Clamp a WaveformRead so codecs only ever see positions in [0, len). +// +// Samplebuffers and codecs are not loop-aware. The mixer stops rounds at +// loop boundaries (or pins small loops for the RSP to wrap), but a fetch +// window still includes #MIXER_LOOP_OVERREAD past the last useful sample, +// and a streamed wpos can sit past len while the samplebuffer keeps a +// contiguous unrolled view of the stream. This wrapper maps those cases +// onto reads in [0, len) — silence for one-shots, samples from loop_start +// for looping waveforms — without forcing a seek on a contiguous wrap. static void waveform_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) { - waveform_t *wave = (waveform_t*)ctx; - - if (!wave->loop_len) { - // If we're asked to read past the waveform length (because overread), - // call the underlying function only up to the actual length, - // and then zero the rest of data. - int len1 = wlen; - if (wpos + wlen > wave->len) - len1 = MAX(wave->len - wpos, 0); - int len2 = wlen-len1; - - if (len1 > 0) - wave->read(wave->ctx, sbuf, wpos, len1, seeking); - if (len2 > 0) { - void *dest = samplebuffer_append(sbuf, len2); - memset(dest, 0, len2 << SAMPLES_BPS_SHIFT(sbuf)); + waveform_t *wave = sbuf->wave; + // A seek that restarts the ring is the CPU moving playback where the + // stream does not go on its own, so the state the codec writes for it has + // to reach the RSP (see #mixer_emit_setstate). The refill that follows the + // end of a looping waveform arrives as a seek too, but there the ring + // keeps its live window: the RSP is still decoding the frames before the + // loop, and the state of the loop start only becomes the one it needs once + // the CPU wraps the position. + if (seeking && sbuf->widx == 0 && wave->format == WAVEFORM_FORMAT_VADPCM) { + int ch = sbuf - Mixer.ch_buf; + Mixer.vstate_dirty |= mixer_bit(ch); + if (Mixer.channels[ch].flags & CH_FLAGS_STEREO) + Mixer.vstate_dirty |= mixer_bit(ch+1); + } + + // Bounds come from the channel, not from the waveform: they follow its + // loop state (see #mixer_ch_set_loop), so a voice in release reads through + // to the sample end while another one on the same waveform still loops. + mixer_channel_t *c = &Mixer.channels[sbuf - Mixer.ch_buf]; + int bps_fx64 = ((c->flags & CH_FLAGS_VADPCM) ? 0 : (c->flags & CH_FLAGS_BPS_SHIFT)) + MIXER_FX64_FRAC; + int wave_len = c->len >> bps_fx64; + int wave_loop = c->loop_len >> bps_fx64; + + // Samplebuffer units: PCM samples, or VADPCM frames. Wave metadata is + // always in samples; convert bounds when the buffer stores frames. + if (wave->format == WAVEFORM_FORMAT_VADPCM) { + // The last frame of a waveform is partial whenever its length is not a + // multiple of 16, and the loop point can sit anywhere inside a frame: + // both bounds have to become the frame the sample belongs to, or the + // samples living in those partial frames would never be fetched. + int loop_start = (wave_len - wave_loop) / 16; + wave_len = DIVIDE_CEIL(wave_len, 16); + wave_loop = wave_loop ? wave_len - loop_start : 0; + } + int ub = sbuf->unit_bytes; + // Silence past the end: the codec read fills both VADPCM planes, so the + // padding must too (and at the same wpos), or the right ring falls short + // / misaligns and samplebuffer_get tries to extend it without a reader. + bool stereo_vadpcm = (c->flags & (CH_FLAGS_VADPCM | CH_FLAGS_STEREO)) + == (CH_FLAGS_VADPCM | CH_FLAGS_STEREO); + // On a ring the RSP writes into, every append has to leave the write cursor + // where the producer can write next (see #samplebuffer_align_units), + // including the appends the mixer makes itself: a few extra units of + // silence past the end of a waveform that does not loop cost nothing. + int pad = wave->rsp_written ? samplebuffer_align_units(sbuf) : 1; + + if (wpos >= wave_len) { + if (!wave_loop) { + wlen = ROUND_UP(wlen, pad); + memset(samplebuffer_append(sbuf, wlen), 0, wlen * ub); + if (stereo_vadpcm) { + samplebuffer_t *r = sbuf + 1; + if (r->widx == 0) r->wpos = sbuf->wpos; + memset(samplebuffer_append(r, wlen), 0, wlen * ub); + r->wnext = r->wpos + r->widx; + } + return; } - } else { - // Calculate wrapped position - if (wpos >= wave->len) - wpos = waveform_wrap_wpos(wpos, wave->len, wave->loop_len); - - // If we are requesting a read from 0, we force seeking because it - // means that previous read finished just exactly at the loop point. - if (wpos == 0) - seeking = true; - - // The read might cross the end point of the waveform - // and continue at the loop point. We would need to handle - // this case by performing two reads with a seek inbetween. - - // Split the length into two segments: before loop and loop. - int len1 = wlen; - if (wpos + wlen > wave->len) - len1 = wave->len - wpos; - int len2 = wlen-len1; - - // Logic check: the second segment (loop) shouldn't be longer - // than the loop length plus the loop overread. Otherwise, it means - // that we've been requested a single read that spans more than two - // full loops, but that's impossible! In fact, a single request must fit - // a sample buffer, and if a whole loop fits the sample buffer, - // we wouldn't get here: the mixer handles fully-cachable loops - // without unrolling them (see mixer_poll). - assertf(len2 <= wave->loop_len + (MIXER_LOOP_OVERREAD >> SAMPLES_BPS_SHIFT(sbuf)), - "waveform %s: logic error: double loop in single read\n" - "wpos:%x, wlen:%x, len:%x loop_len:%x", - wave->name, wpos, wlen, wave->len, wave->loop_len); - - // Perform the first read - wave->read(wave->ctx, sbuf, wpos, len1, seeking); - - // See if we need to perform a second read for the loop. Because of - // overread, we need to read the loop as many times as necessary - // (though technically, once would be sufficient without overread). - while (len2 > 0) { - int loop_start = wave->len - wave->loop_len; - int ns = MIN(len2, wave->loop_len); - wave->read(wave->ctx, sbuf, loop_start, ns, true); - len2 -= ns; - } - } -} - -void mixer_ch_play(int ch, waveform_t *wave) { - assert(ch < Mixer.num_channels); + // Keep seeking as-is: a contiguous fetch past len is still sequential + // for the codec (it just continues); only force a seek when the + // wrapped position lands exactly on the loop point. + wpos = waveform_wrap_wpos(wpos, wave_len, wave_loop); + } + if (wave_loop && wpos == wave_len - wave_loop) + seeking = true; + + int len1 = wlen; + if (wpos + wlen > wave_len) + len1 = wave_len - wpos; + int len2 = wlen - len1; + + if (len1 > 0) + wave->read(ctx, sbuf, wpos, len1, seeking); + if (len2 <= 0) + return; + + if (!wave_loop) { + len2 = ROUND_UP(len2, pad); + memset(samplebuffer_append(sbuf, len2), 0, len2 * ub); + if (stereo_vadpcm) { + samplebuffer_t *r = sbuf + 1; + if (r->widx == 0) r->wpos = sbuf->wpos; + memset(samplebuffer_append(r, len2), 0, len2 * ub); + r->wnext = r->wpos + r->widx; + } + return; + } + + // Overread past the end: refill from the loop start. A single fragment + // covers MIXER_LOOP_OVERREAD; tiny loops may need more than one. + int loop_start = wave_len - wave_loop; + while (len2 > 0) { + int ns = MIN(len2, wave_loop); + wave->read(ctx, sbuf, loop_start, ns, true); + len2 -= ns; + } +} + +static bool mixer_wave_fits(int ch); + +/** + * @brief Resume a streamed VADPCM channel over an emptied ring. + * + * A VADPCM stream can only be re-seeded on the points its asset was built + * with, so an empty ring must not turn the next fetch into a seek. It does not + * have to: the frames are addressed directly, and the decoder state the RSP + * carries already belongs to the frame playback sits on. Marking the ring as + * the continuation of that frame is enough for the refill to be a plain + * sequential read. + * + * The state is not the right one at exactly one moment: right after the CPU + * wrapped the position between two rounds (see #mixer_advance), where the RSP + * is still holding the one of the end of the waveform. That frame is the loop + * start, whose state every looping asset carries, so it can just be handed + * over (see #mixer_emit_setstate). + */ +static void mixer_vadpcm_resume(int ch) { + mixer_channel_t *c = &Mixer.channels[ch]; samplebuffer_t *sbuf = &Mixer.ch_buf[ch]; + bool stereo = (c->flags & CH_FLAGS_STEREO) != 0; + + sbuf->wpos = sbuf->wnext = c->vframe; + if (stereo) { + samplebuffer_t *sbuf_r = &Mixer.ch_buf[ch+1]; + sbuf_r->wpos = sbuf_r->wnext = c->vframe; + } + + if (!c->loop_state || !c->wave->loop_len) + return; + if (c->vframe != (waveform_loop_end(c->wave) - c->wave->loop_len) / 16) + return; + memcpy(c->codec_state, c->loop_state, 16 * (stereo ? 2 : 1)); + Mixer.vstate_dirty |= mixer_bit(ch); + if (stereo) + Mixer.vstate_dirty |= mixer_bit(ch+1); +} + +/** + * @brief Go back to streaming a loop that was pinned in the samplebuffer. + * + * The pinned copy is addressed relative to the loop region and is wrapped by + * the RSP, so it only survives while playback stays inside it. Dropping it + * empties the ring, and the next fetch refills it from wherever playback is. + */ +static void mixer_ch_unpin_loop(int ch) { mixer_channel_t *c = &Mixer.channels[ch]; + bool stereo_vadpcm = (c->flags & (CH_FLAGS_VADPCM | CH_FLAGS_STEREO)) + == (CH_FLAGS_VADPCM | CH_FLAGS_STEREO); + c->flags &= ~CH_FLAGS_LOOP_CACHED; + if (stereo_vadpcm) { + Mixer.channels[ch+1].flags &= ~CH_FLAGS_LOOP_CACHED; + samplebuffer_flush(&Mixer.ch_buf[ch+1]); + } + samplebuffer_flush(&Mixer.ch_buf[ch]); + if (c->flags & CH_FLAGS_VADPCM) + mixer_vadpcm_resume(ch); + mixer_refresh_max_ns(ch); +} + +/** @brief Apply a VADPCM seek to the channel's codec state via WaveformRead. */ +static void mixer_vadpcm_seek(mixer_channel_t *c, int sample_pos) { + assert(c->flags & CH_FLAGS_VADPCM); + assert(c->wave && c->wave->read && c->codec_state); + // Two buffers: a stereo read also touches the right-plane one, even when + // seeking with no data to fetch. + samplebuffer_t tmp[2] = {{0}}; + tmp[0].wave = c->wave; + tmp[0].state = c->codec_state; + tmp[0].state_size = c->wave->state_size; + c->wave->read(c->wave->ctx, tmp, sample_pos / 16, 0, true); + c->vframe = sample_pos / 16; + int ch = c - Mixer.channels; + Mixer.vstate_dirty |= mixer_bit(ch); + if (c->flags & CH_FLAGS_STEREO) { + c[1].vframe = c->vframe; + Mixer.vstate_dirty |= mixer_bit(ch+1); + } +} + +/** + * @brief Re-establish the playback state after the CPU moved a position. + * + * The RSP always resumes a channel from where the CPU says it is, assuming + * that what it needs is already in place: the samples of that position in the + * samplebuffer, and (for VADPCM) a decoder state in RDRAM saved exactly for the frame + * containing it. Both assumptions only hold while playback advances + * sequentially, so any jump made by the CPU must be fixed up here. + */ +static void mixer_ch_seek(int ch) { + mixer_channel_t *c = &Mixer.channels[ch]; + samplebuffer_t *sbuf = &Mixer.ch_buf[ch]; + bool vadpcm = (c->flags & CH_FLAGS_VADPCM) != 0; + int bps_fx64 = (vadpcm ? 0 : (c->flags & CH_FLAGS_BPS_SHIFT)) + MIXER_FX64_FRAC; + int frame = (int)(c->pos >> (MIXER_FX64_FRAC + 4)); + + if (!c->wave) + return; + + // Resident waveforms are always fully available: only the decoder state + // has to be moved. + if (c->flags & CH_FLAGS_RESIDENT) { + if (vadpcm && frame != c->vframe) + mixer_vadpcm_seek(c, frame * 16); + return; + } + + if (c->flags & CH_FLAGS_LOOP_CACHED) { + int len = c->len >> bps_fx64; + int loop_len = c->loop_len >> bps_fx64; + int cache_start = mixer_wave_fits(ch) ? 0 : len - loop_len; + bool have_samples = (c->pos >> bps_fx64) >= cache_start; + if (have_samples) { + // The samples are pinned already: just move the decoder. + if (vadpcm && frame != c->vframe) + mixer_vadpcm_seek(c, frame * 16); + return; + } + // Seeking before the pinned region: go back to streaming, and let the + // next round pin the loop again (re-seeding the decoder while at it). + mixer_ch_unpin_loop(ch); + return; + } - // If we're going to play a stereo waveform on a channel that was allocated - // for mono, we need to reallocate the buffer. - if (wave->channels == 2 && !(c->flags & CH_FLAGS_STEREO_ALLOC)) - samplebuffer_close(sbuf); - - if (!samplebuffer_is_inited(sbuf)) { - // If we have not yet allocated the memory for the sample buffers, - // this is a good moment to do so, as we might need the configure - // the samplebuffer in a moment. - int size = mixer_calc_buffer_size(ch, wave->channels); - void *ptr = malloc_uncached(size); - assertf(ptr, "out of memory (size=%d)", size); - samplebuffer_init(sbuf, ptr, size); - if (wave->channels == 2) c->flags |= CH_FLAGS_STEREO_ALLOC; - } - - // Configure the waveform on this channel, if we have not - // already. This optimization is useful in case the caller - // wants to play the same waveform on the same channel multiple - // times, and the waveform has been already decoded and cached - // in the sample buffer. - if (wave != sbuf->wv_ctx) { + // Streamed: flushing the samplebuffer forces the next fetch to seek, which + // is what re-seeds the decoder state. + if (vadpcm && frame != c->vframe) samplebuffer_flush(sbuf); +} + +void mixer_ch_play(int ch, waveform_t *wave) +{ + assert(ch < Mixer.num_channels); + samplebuffer_t *sbuf = &Mixer.ch_buf[ch]; + mixer_channel_t *c = &Mixer.channels[ch]; + assertf(!(c->flags & CH_FLAGS_STEREO_SUB), "mixer_ch_play: cannot call on secondary stereo channel %d", ch); + + // Initialize uuid for this waveform if it wasn't already + if (wave->__uuid == 0) + wave->__uuid = ++Mixer.uuid_counter; + + bool resident = wave->mem != NULL; + bool vadpcm = wave->format == WAVEFORM_FORMAT_VADPCM; + bool stereo_vadpcm = vadpcm && wave->channels == 2; + + // Stereo VADPCM uses two mono rings (ch and ch+1). PCM stereo uses one + // interleaved buffer on the owner (STEREO_ALLOC). The ring on ch+1 belongs + // to this channel for as long as it keeps playing stereo VADPCM: ch+1 + // cannot be played on its own while STEREO_SUB is set. + bool was_stereo_vadpcm = !(c->flags & CH_FLAGS_RESIDENT) && + (c->flags & (CH_FLAGS_VADPCM|CH_FLAGS_STEREO)) == (CH_FLAGS_VADPCM|CH_FLAGS_STEREO); + if (!resident && stereo_vadpcm) { + // Only reuse the pair if it was allocated as a pair and is still intact + // (mixer_ch_set_limits frees one ring of the pair, either of them). + if (!was_stereo_vadpcm || !samplebuffer_is_inited(sbuf) || + !samplebuffer_is_inited(&Mixer.ch_buf[ch+1])) { + rspq_highpri_sync(); + samplebuffer_close(sbuf); + samplebuffer_close(&Mixer.ch_buf[ch+1]); + } + } else if (!resident) { + // Leaving stereo VADPCM: release the secondary ring, or the next stereo + // VADPCM would find it allocated but still holding the previous stream. + if (was_stereo_vadpcm) { + rspq_highpri_sync(); + samplebuffer_close(&Mixer.ch_buf[ch+1]); + } + if (wave->channels == 2 && !(c->flags & CH_FLAGS_STEREO_ALLOC)) { + rspq_highpri_sync(); + samplebuffer_close(sbuf); + } + } + // Check if the sample / state buffer is big enough for this waveform. + // A channel that first played a raw/VADPCM SFX may hold a ring sized + // without Opus/ULC append headroom; keep that from wrapping onto RSP-live + // samples when a block codec is played later. + if (!resident && samplebuffer_is_inited(sbuf)) { + int need = ROUND_UP(mixer_calc_buffer_size(ch, wave), 16); + int ub = vadpcm ? mixer_vadpcm_frame_bytes(wave) + : ((wave->bits / 8) * (stereo_vadpcm ? 1 : wave->channels)); + need += mixer_margin_bytes(wave, ub); + need = ROUND_UP(need, 16); + if (sbuf->capacity_bytes < need || sbuf->state_size < wave->state_size) { + rspq_highpri_sync(); + samplebuffer_close(sbuf); + if (stereo_vadpcm) samplebuffer_close(&Mixer.ch_buf[ch+1]); + } + } + + if (!resident && !samplebuffer_is_inited(sbuf)) { + int size = ROUND_UP(mixer_calc_buffer_size(ch, wave), 16); + int ub = vadpcm ? mixer_vadpcm_frame_bytes(wave) : ((wave->bits / 8) * (stereo_vadpcm ? 1 : wave->channels)); + size += mixer_margin_bytes(wave, ub); + size = ROUND_UP(size, 16); + int state_size = ROUND_UP(wave->state_size, 16); + void *ptr = malloc_uncached(size + state_size); + assertf(ptr, "Out of memory"); + samplebuffer_init(sbuf, ptr, size, state_size); + if (stereo_vadpcm) { + assertf(ch != Mixer.num_channels-1, "cannot play stereo VADPCM on last channel"); + void *ptr_r = malloc_uncached(size); // R samplebuffer: no second state (shared) + assertf(ptr_r, "Out of memory"); + samplebuffer_init(&Mixer.ch_buf[ch+1], ptr_r, size, 0); + c->flags |= CH_FLAGS_STEREO_ALLOC; + } else if (wave->channels == 2) { + c->flags |= CH_FLAGS_STEREO_ALLOC; + } + } + + // Configure the waveform on this channel, if we have not already. The uuid + // alone is not enough: the ring can have been closed and reallocated just + // above, and is then no longer configured for this waveform even though the + // channel still remembers playing it. VADPCM residual width can also change + // on the same waveform object (same uuid): unit_bytes and vbits must track it. + bool configured = resident || (sbuf->wave == wave && sbuf->unit_bytes); + if (configured && !resident && vadpcm) { + int ub = mixer_vadpcm_frame_bytes(wave); + int bits = ((const waveform_vadpcm_t *)wave->codec)->bits; + if (sbuf->unit_bytes != ub || c->vbits != bits || + (stereo_vadpcm && Mixer.ch_buf[ch+1].unit_bytes != ub)) + configured = false; + } + if (!configured || wave->__uuid != c->wave_uuid || + (c->flags & CH_FLAGS_RESIDENT) != (resident ? CH_FLAGS_RESIDENT : 0)) { + if (!resident) { + samplebuffer_flush(sbuf); + // Stereo VADPCM keeps a second ring on ch+1, which is reconfigured + // below as well: it must be emptied too. + if (stereo_vadpcm) + samplebuffer_flush(&Mixer.ch_buf[ch+1]); + } + + // If this channel is playing something else, stop it + if (mixer_ch_playing(ch)) + mixer_ch_stop(ch); + + // Free a previous resident-only codec state if switching away. + if ((c->flags & CH_FLAGS_RESIDENT) && c->codec_state) { + free_uncached(c->codec_state); + c->codec_state = NULL; + } - // Configure the sample buffer for this waveform assert(wave->channels == 1 || wave->channels == 2); assert(wave->bits == 8 || wave->bits == 16); - samplebuffer_set_bps(sbuf, wave->bits*wave->channels); - samplebuffer_set_waveform(sbuf, wave->read ? waveform_read : NULL, wave); - - // Configure the mixer channel structured used by the RSP ucode assertf(wave->len >= 0 && wave->len <= WAVEFORM_MAX_LEN, "waveform %s: invalid length %x", wave->name, wave->len); assertf(wave->len != WAVEFORM_UNKNOWN_LEN || wave->loop_len == 0, "waveform %s with unknown length cannot loop", wave->name); - int bps = SAMPLES_BPS_SHIFT(sbuf); - c->flags = bps | (wave->channels == 2 ? CH_FLAGS_STEREO : 0) | (wave->bits == 16 ? CH_FLAGS_16BIT : 0); - c->len = MIXER_FX64((int64_t)wave->len) << bps; - c->loop_len = MIXER_FX64((int64_t)wave->loop_len) << bps; - mixer_ch_set_freq(ch, wave->frequency); + assertf(wave->loop_len >= 0 && wave->loop_len <= waveform_loop_end(wave) && waveform_loop_end(wave) <= wave->len, + "waveform %s: invalid loop of %d samples ending at %d (len %d)", + wave->name, wave->loop_len, waveform_loop_end(wave), wave->len); + + c->flags &= ~(CH_FLAGS_BPS_SHIFT | CH_FLAGS_16BIT | CH_FLAGS_STEREO | CH_FLAGS_VADPCM | CH_FLAGS_RESIDENT | CH_FLAGS_LOOP_CACHED); + c->codebook = NULL; + c->loop_state = NULL; + c->codec_state = NULL; + + if (resident) { + c->flags |= CH_FLAGS_RESIDENT; + if (wave->state_size) { + c->codec_state = malloc_uncached(ROUND_UP(wave->state_size, 16)); + assertf(c->codec_state, "Out of memory"); + memset(c->codec_state, 0, wave->state_size); + } + } else { + if (wave->format == WAVEFORM_FORMAT_VADPCM) { + samplebuffer_set_unit_bytes(sbuf, mixer_vadpcm_frame_bytes(wave)); + if (stereo_vadpcm) + samplebuffer_set_unit_bytes(&Mixer.ch_buf[ch+1], mixer_vadpcm_frame_bytes(wave)); + } else { + samplebuffer_set_bps(sbuf, wave->bits*wave->channels); + } + samplebuffer_set_waveform(sbuf, wave, wave->read ? waveform_read : NULL); + c->codec_state = sbuf->state; + } - if (wave->channels == 2) { - assertf(ch != Mixer.num_channels-1, "cannot configure last channel (%d) as stereo", ch); - Mixer.channels[ch+1].flags |= CH_FLAGS_STEREO_SUB; - } else if (ch != Mixer.num_channels-1) { - Mixer.channels[ch+1].flags &= ~CH_FLAGS_STEREO_SUB; + if (wave->format == WAVEFORM_FORMAT_VADPCM) { + waveform_vadpcm_t *vc = wave->codec; + assertf(vc && vc->codebook, "waveform %s: VADPCM missing codec/codebook", wave->name); + assertf(vc->bits >= 2 && vc->bits <= 4, "waveform %s: invalid VADPCM residual width %d", wave->name, vc->bits); + c->flags |= CH_FLAGS_VADPCM | CH_FLAGS_16BIT; + if (wave->channels == 2) c->flags |= CH_FLAGS_STEREO; + c->codebook = vc->codebook; + c->loop_state = vc->loop_state; + c->vbits = vc->bits; + if (stereo_vadpcm) { + mixer_channel_t *r = &Mixer.channels[ch+1]; + r->flags = (r->flags & CH_FLAGS_FORCE_MONO) | CH_FLAGS_STEREO_SUB | CH_FLAGS_VADPCM | CH_FLAGS_16BIT; + r->codebook = (uint8_t*)vc->codebook + VADPCM_CODEBOOK_STRIDE; + r->vbits = vc->bits; + r->codec_state = (uint8_t*)c->codec_state + 16; + r->loop_state = vc->loop_state ? (uint8_t*)vc->loop_state + 16 : NULL; + r->wave = wave; + r->wave_uuid = wave->__uuid; + } + } else { + int bps = (wave->bits == 16 ? 1 : 0) + (wave->channels == 2 ? 1 : 0); + c->flags |= bps | (wave->channels == 2 ? CH_FLAGS_STEREO : 0) | (wave->bits == 16 ? CH_FLAGS_16BIT : 0); } + mixer_ch_set_freq(ch, wave->frequency); - tracef("mixer_ch_play: ch=%d len=%llx loop_len=%llx wave=%s\n", ch, c->len >> (MIXER_FX64_FRAC+bps), c->loop_len >> (MIXER_FX64_FRAC+bps), wave->name); + if (!resident && wave->start) + wave->start(wave->ctx, sbuf); + + c->wave_uuid = wave->__uuid; + tracef("mixer_ch_play[new]: ch=%d len=%llx loop_len=%llx wave=%s%s\n", + ch, (uint64_t)wave->len, (uint64_t)wave->loop_len, wave->name, + resident ? " [resident]" : ""); + } else if (!resident) { + tracef("mixer_ch_play[old]: ch=%d wave=%s\n", ch, wave->name); + assertf(sbuf->wave == wave, "%s: uuid match (%ld) but pointer mismatch: %p != %p", + wave->name, wave->__uuid, sbuf->wave, wave); } - // Restart from the beginning of the waveform - c->ptr = SAMPLES_PTR(sbuf); + // Restart from the beginning of the waveform, with the loop armed again: + // a #mixer_ch_set_loop only applies to the note that was playing. + c->wave = wave; + mixer_ch_bounds(c, wave, true); + if (resident && stereo_vadpcm) { + // Planes are laid out back to back, each holding every frame that + // carries a sample (the last one is partial if len is not a multiple + // of 16). + int nframes = DIVIDE_CEIL(wave->len, 16); + c->ptr = (void*)wave->mem; + Mixer.channels[ch+1].ptr = (uint8_t*)wave->mem + nframes * mixer_vadpcm_frame_bytes(wave); + Mixer.channels[ch+1].pos = 0; + } else { + c->ptr = resident ? (void*)wave->mem : SAMPLES_PTR(sbuf); + } c->pos = 0; + PROFILE_START(PS_MIXER_SEEK); + mixer_ch_seek(ch); + PROFILE_STOP(PS_MIXER_SEEK); + + // Mark ch+1 as stereo sub for PCM interleaved or VADPCM dual-mono. + if (c->flags & CH_FLAGS_STEREO) { + assertf(ch != Mixer.num_channels-1, "cannot configure last channel (%d) as stereo", ch); + assertf(!mixer_ch_playing(ch+1) || (Mixer.channels[ch+1].flags & CH_FLAGS_STEREO_SUB), + "cannot play stereo waveform on channel %d because channel %d is active", ch, ch+1); + if (!(c->flags & CH_FLAGS_VADPCM)) { + // PCM interleaved: R has no samplebuffer of its own. Replace flags + // so a previous mono/VADPCM occupant cannot leave VADPCM set and + // make Phase A try to fetch from a null-wave samplebuffer. + Mixer.channels[ch+1].flags = (Mixer.channels[ch+1].flags & CH_FLAGS_FORCE_MONO) | CH_FLAGS_STEREO_SUB; + Mixer.channels[ch+1].ptr = NULL; + } else { + Mixer.channels[ch+1].flags |= CH_FLAGS_STEREO_SUB; + } + } else if (ch != Mixer.num_channels-1) { + Mixer.channels[ch+1].flags &= ~CH_FLAGS_STEREO_SUB; + } + + mixer_touch_ch(ch); + // The state buffer of a channel outlives the waveforms played on it, so a + // new one always has to seed it, even when it starts where the previous + // one happened to be. + if (c->flags & CH_FLAGS_VADPCM) { + Mixer.chtbl_dirty |= mixer_bit(ch); + Mixer.vstate_dirty |= mixer_bit(ch); + } + if (c->flags & CH_FLAGS_STEREO) { + mixer_touch_ch(ch+1); + if (c->flags & CH_FLAGS_VADPCM) { + Mixer.chtbl_dirty |= mixer_bit(ch+1); + Mixer.vstate_dirty |= mixer_bit(ch+1); + } + } + Mixer.start_tick[ch] = Mixer.ticks; + Mixer.prio[ch] = MIXER_PRIORITY_MAX; + mixer_refresh_max_ns(ch); } -void mixer_ch_set_pos(int ch, float pos) { +void mixer_ch_set_pos(int ch, double pos) { mixer_channel_t *c = &Mixer.channels[ch]; assertf(!(c->flags & CH_FLAGS_STEREO_SUB), "mixer_ch_set_pos: cannot call on secondary stereo channel %d", ch); - c->pos = MIXER_FX64(pos) << (c->flags & CH_FLAGS_BPS_SHIFT); + mixer_fx64_t p = MIXER_FX64(pos); + if (!(c->flags & CH_FLAGS_VADPCM)) + p <<= (c->flags & CH_FLAGS_BPS_SHIFT); + c->pos = p; + PROFILE_START(PS_MIXER_SEEK); + mixer_ch_seek(ch); + PROFILE_STOP(PS_MIXER_SEEK); + tracef("mixer_ch_set_pos: ch=%d pos=%.32g(%llx)\n", ch, pos, c->pos); +} + +void mixer_ch_set_loop(int ch, bool enable) { + mixer_channel_t *c = &Mixer.channels[ch]; + assertf(!(c->flags & CH_FLAGS_STEREO_SUB), "mixer_ch_set_loop: cannot call on secondary stereo channel %d", ch); + assertf(c->wave, "mixer_ch_set_loop: channel %d is not playing", ch); + assertf(!enable || c->wave->loop_len, "mixer_ch_set_loop: waveform %s does not loop", c->wave->name); + if (enable == (c->loop_len != 0)) + return; + + mixer_ch_bounds(c, c->wave, enable); + // The ring holds the loop unrolled: whatever it cached past the loop end + // was taken from the loop start, and a pinned loop is not even addressed + // linearly. Drop it and resume streaming from the current position. + if (!(c->flags & CH_FLAGS_RESIDENT)) + mixer_ch_unpin_loop(ch); + tracef("mixer_ch_set_loop: ch=%d enable=%d len=%llx\n", ch, enable, (uint64_t)c->len); } -float mixer_ch_get_pos(int ch) { +double mixer_ch_get_pos(int ch) { mixer_channel_t *c = &Mixer.channels[ch]; assertf(!(c->flags & CH_FLAGS_STEREO_SUB), "mixer_ch_get_pos: cannot call on secondary stereo channel %d", ch); - uint64_t pos = c->pos >> (c->flags & CH_FLAGS_BPS_SHIFT); - return (float)pos / (float)(1<pos; + if (!(c->flags & CH_FLAGS_VADPCM)) + pos >>= (c->flags & CH_FLAGS_BPS_SHIFT); + return (double)pos / (double)(1<ptr = 0; - if (c->flags & CH_FLAGS_STEREO) + + if (!c->ptr) + return; + + tracef("mixer_ch_stop: ch=%d\n", ch); + + bool stereo = (c->flags & CH_FLAGS_STEREO) != 0; + if (stereo) { + // Dropping STEREO_SUB releases ch+1, so it must be disarmed like the + // owner: a stereo VADPCM sub keeps its own ptr/pos, and would + // otherwise look like an independent channel and keep advancing and + // fetching from a samplebuffer that only its owner can fill. c[1].flags &= ~CH_FLAGS_STEREO_SUB; + c[1].ptr = 0; + c[1].pos = 0; + Mixer.chtbl_dirty &= ~mixer_bit(ch+1); + Mixer.vstate_dirty &= ~mixer_bit(ch+1); + } - // Restart caching if played again. We need this guarantee - // because after calling stop(), the caller must be able - // to free waveform, and thus this pointer might become invalid. - Mixer.ch_buf[ch].wv_ctx = NULL; + c->ptr = 0; + c->pos = 0; + Mixer.lvol_ramp[ch].duration = 0; + Mixer.rvol_ramp[ch].duration = 0; + Mixer.gain_ramp[ch].duration = 0; + Mixer.freq_ramp[ch].duration = 0; + Mixer.gain[ch] = 1.0f; + Mixer.prio[ch] = MIXER_PRIORITY_MIN; + Mixer.chtbl_dirty &= ~mixer_bit(ch); + Mixer.vstate_dirty &= ~mixer_bit(ch); + + // Invalidate the wave pointer, as it might become dangling + // anyway, as the user can free the waveform memory at any time after stop. + // Keep the uuid valid instead. This allows + // for an optimization: if mixer_ch_play is called again on the same + // waveform, we will realize that by the uuid, and reuse the same + // samplebuffer contents. + c->wave = NULL; } -void __mixer_wave_stopall(waveform_t *wave) -{ - for (int i=0; iptr && sbuf->wv_ctx == wave) - mixer_ch_stop(i); +waveform_t *mixer_ch_playing_waveform(int ch) { + mixer_channel_t *c = &Mixer.channels[ch]; + if (c->flags & CH_FLAGS_STEREO_SUB) { + assert(ch > 0); + c--; } + return c->ptr != 0 ? c->wave : NULL; } bool mixer_ch_playing(int ch) { - mixer_channel_t *c = &Mixer.channels[ch]; - assertf(!(c->flags & CH_FLAGS_STEREO_SUB), "mixer_ch_playing: cannot call on secondary stereo channel %d", ch); - return c->ptr != 0; + return mixer_ch_playing_waveform(ch) != NULL; +} + +void mixer_ch_set_priority(int ch, int priority) { + assert(ch >= 0 && ch < Mixer.num_channels); + assertf(priority >= MIXER_PRIORITY_MIN && priority <= MIXER_PRIORITY_MAX, + "mixer_ch_set_priority: priority %d out of range on channel %d", priority, ch); + assertf(!(Mixer.channels[ch].flags & CH_FLAGS_STEREO_SUB), + "mixer_ch_set_priority: cannot call on secondary stereo channel %d", ch); + Mixer.prio[ch] = (uint8_t)priority; +} + +/** Instantaneous loudness of @p ch (max of L/R, never negative). */ +static int mixer_ch_loudness(int ch) +{ + mixer_fx15_t l, r; + mixer_ch_vol_at(ch, Mixer.ticks, &l, &r); + int v = l > r ? l : r; + return v > 0 ? v : 0; +} + +/** True if @p ch (and @p ch+1 when stereo) can play @p wave within its limits. */ +static bool mixer_ch_fits_wave(int ch, bool stereo, waveform_t *wave) +{ + if (!wave) + return true; + channel_limit_t *lim = &Mixer.limits[ch]; + if (wave->bits > lim->max_bits) + return false; + if (wave->frequency > lim->max_frequency * 1.01f) + return false; + if (stereo && ch + 1 < Mixer.num_channels) { + lim = &Mixer.limits[ch + 1]; + if (wave->bits > lim->max_bits) + return false; + if (wave->frequency > lim->max_frequency * 1.01f) + return false; + } + return true; +} + +/** Ranking key bit set on an occupied slot: free slots sort before any steal. */ +#define MIXER_ALLOC_BUSY (1ull << 63) +/** Ranking key of a slot that cannot be used at all. */ +#define MIXER_ALLOC_NOKEY UINT64_MAX + +/** + * Ranking key of the slot starting at @p ch: the smallest key wins. + * + * The whole ordering is packed into one integer, so selecting a slot is a + * plain compare and the volume ramps are evaluated once per channel instead + * of once per comparison: + * + * bit 63 busy free slots sort before any steal + * bits 62-55 priority cheapest to steal first + * bits 54-40 loudness quietest first + * bits 39-8 age oldest first (stored inverted) + * bits 7-0 channel lowest index breaks every tie + * + * A free slot leaves every middle field at zero, so its key is just the + * channel index: the stale volume and start tick of a previous occupant + * never affect it. Returns #MIXER_ALLOC_NOKEY if the slot is unusable + * (wrong geometry, limits too small, or an occupant above @p priority); + * that value is out of reach for a real key because the low byte holds a + * channel index below #MIXER_MAX_CHANNELS. + */ +static uint64_t mixer_alloc_key(int ch, bool stereo, int priority, waveform_t *wave) +{ + if (Mixer.channels[ch].flags & CH_FLAGS_STEREO_SUB) + return MIXER_ALLOC_NOKEY; + if (!mixer_ch_fits_wave(ch, stereo, wave)) + return MIXER_ALLOC_NOKEY; + + // Stopping a stereo primary frees its own sub, so that half is not an + // independent occupant. The sub of a pair starting at ch-1 is untouchable. + bool own_sub = stereo && + (Mixer.channels[ch].flags & CH_FLAGS_STEREO) && + (Mixer.channels[ch + 1].flags & CH_FLAGS_STEREO_SUB); + if (stereo && !own_sub && (Mixer.channels[ch + 1].flags & CH_FLAGS_STEREO_SUB)) + return MIXER_ALLOC_NOKEY; + + bool busy = false; + int prio = 0, loud = 0; + int64_t start = INT64_MIN; // only read once "busy", so always assigned + + if (mixer_ch_playing(ch)) { + if (Mixer.prio[ch] > priority) + return MIXER_ALLOC_NOKEY; + busy = true; + prio = Mixer.prio[ch]; + loud = mixer_ch_loudness(ch); + start = Mixer.start_tick[ch]; + } + if (stereo && !own_sub && mixer_ch_playing(ch + 1)) { + if (Mixer.prio[ch + 1] > priority) + return MIXER_ALLOC_NOKEY; + // Taking the pair costs whatever the more expensive half costs. + busy = true; + if (Mixer.prio[ch + 1] > prio) prio = Mixer.prio[ch + 1]; + int l1 = mixer_ch_loudness(ch + 1); + if (l1 > loud) loud = l1; + if (Mixer.start_tick[ch + 1] > start) start = Mixer.start_tick[ch + 1]; + } + + if (!busy) + return (uint64_t)ch; + + uint64_t age = Mixer.ticks - start; + if (age > 0xFFFFFFFFull) age = 0xFFFFFFFFull; + return MIXER_ALLOC_BUSY | ((uint64_t)prio << 55) | ((uint64_t)loud << 40) | + ((0xFFFFFFFFull - age) << 8) | (uint64_t)ch; +} + +/** + * Drop @p ch from the candidate set of #mixer_ch_alloc, together with the + * neighbouring slots that a stereo pair would overlap. + */ +static void mixer_alloc_retire(uint64_t *keys, int ch, int first_ch, int end, + bool stereo) +{ + keys[ch] = MIXER_ALLOC_NOKEY; + if (stereo) { + if (ch > first_ch) keys[ch - 1] = MIXER_ALLOC_NOKEY; + if (ch + 1 < end) keys[ch + 1] = MIXER_ALLOC_NOKEY; + } +} + +int mixer_ch_alloc(int first_ch, int num_ch, int count, bool stereo, + int priority, waveform_t *wave, int *out) +{ + assert(mixer_initialized()); + assert(first_ch >= 0 && num_ch >= 0); + assert(first_ch + num_ch <= Mixer.num_channels); + assert(count >= 0); + assert(out || count == 0); + assertf(priority >= MIXER_PRIORITY_MIN && priority <= MIXER_PRIORITY_MAX, + "mixer_ch_alloc: priority %d out of range", priority); + if (count == 0) + return 0; + + // A stereo slot also needs ch+1, so it cannot start on the last channel + // of the window. + int end = first_ch + num_ch - (stereo ? 1 : 0); + uint64_t keys[MIXER_MAX_CHANNELS]; + int chosen = 0; + + // Free slots outrank every steal and are ranked by index, so while walking + // upwards we can commit to them on the spot. When they are enough we never + // even look at the rest of the window. + for (int ch = first_ch; ch < end; ch++) { + uint64_t key = mixer_alloc_key(ch, stereo, priority, wave); + keys[ch] = key; + if (key & MIXER_ALLOC_BUSY) + continue; + out[chosen++] = ch; + if (chosen == count) + return chosen; + mixer_alloc_retire(keys, ch, first_ch, end, stereo); + if (stereo) + ch++; // ch+1 is now part of this pair, skip it + } + + // Not enough free slots: the rest has to be stolen from the cheapest + // occupants left in the window. + while (chosen < count) { + int best = -1; + for (int ch = first_ch; ch < end; ch++) + if (keys[ch] != MIXER_ALLOC_NOKEY && (best < 0 || keys[ch] < keys[best])) + best = ch; + if (best < 0) + break; + out[chosen++] = best; + mixer_alloc_retire(keys, best, first_ch, end, stereo); + } + return chosen; +} + +int mixer_play(waveform_t *wave, int priority) +{ + assert(mixer_initialized()); + assert(wave); + assertf(priority >= MIXER_PRIORITY_MIN && priority <= MIXER_PRIORITY_MAX, + "mixer_play: priority %d out of range", priority); + + bool stereo = wave->channels == 2; + int ch; + if (!mixer_ch_alloc(0, Mixer.num_channels, 1, stereo, priority, wave, &ch)) + return -1; + + if (mixer_ch_playing(ch)) + mixer_ch_stop(ch); + if (stereo && mixer_ch_playing(ch + 1)) + mixer_ch_stop(ch + 1); + + mixer_ch_set_vol_ramp(ch, 1.0f, 1.0f, 0); + mixer_ch_set_force_mono(ch, false); + mixer_ch_play(ch, wave); + mixer_ch_set_priority(ch, priority); + return ch; } void mixer_ch_set_limits(int ch, int max_bits, float max_frequency, int max_buf_sz) { @@ -483,199 +1557,334 @@ void mixer_ch_set_limits(int ch, int max_bits, float max_frequency, int max_buf_ assert(!mixer_ch_playing(ch)); tracef("mixer_ch_set_limits: ch=%d bits=%d maxfreq:%.2f bufsz:%d\n", ch, max_bits, max_frequency, max_buf_sz); - Mixer.limits[ch] = (channel_limit_t){ + channel_limit_t newlimits = { .max_bits = max_bits ? max_bits : 16, .max_frequency = max_frequency ? max_frequency : Mixer.sample_rate, .max_buf_sz = max_buf_sz, }; + // No-op if the limits are unchanged: the sample buffer is sized from them, + // so it's still valid and we keep it. Freeing it here (to reallocate lazily) + // would churn the uncached buffer on every playback for callers that + // re-assert the same limit, fragmenting the heap. + if (newlimits.max_bits == Mixer.limits[ch].max_bits && + newlimits.max_frequency == Mixer.limits[ch].max_frequency && + newlimits.max_buf_sz == Mixer.limits[ch].max_buf_sz) + return; + + Mixer.limits[ch] = newlimits; + // Free the memory immediately, as it doesn't match the new limits anymore. // We will reallocate it later lazily if needed. if (samplebuffer_is_inited(&Mixer.ch_buf[ch])) { + rspq_highpri_sync(); samplebuffer_close(&Mixer.ch_buf[ch]); + // The waveform that was playing has to be configured again on the new + // buffer, so forget it: mixer_ch_play skips that work when it sees the + // same waveform it already has. + Mixer.channels[ch].wave_uuid = 0; Mixer.channels[ch].flags &= ~CH_FLAGS_STEREO_ALLOC; } } -static void mixer_exec(int32_t *out, int num_samples) { - tracef("mixer_exec: 0x%x samples\n", num_samples); +/** @brief Apply global volume (FX16) to a channel volume (FX15). */ +static inline mixer_fx15_t mixer_apply_gvol(mixer_fx15_t vol, mixer_fx16_t gvol_fx16) { + int32_t v = (int32_t)(((int64_t)vol * gvol_fx16) >> 16); + if (v > 0x7FFF) v = 0x7FFF; + if (v < -0x8000) v = -0x8000; + return (mixer_fx15_t)v; +} - uint32_t fake_loop = 0; +/** @brief Emit a MIX_CHANNEL rspq command for one channel. + * + * @p nsamples is latched by the ucode into DMEM (same value for every channel + * of a round). It must be even and at most #MIXER_MAX_SAMPLES_PER_ROUND so that + * nsamples/2 fits the 9-bit field in a0. + */ +static void mixer_emit_channel(int ch, uint32_t flags, mixer_round_vol_t vol, + uint32_t pos, uint32_t step, uint32_t len, uint32_t loop_len, void *ptr, + int nsamples) +{ + assert(ch >= 0 && ch < MIXER_MAX_CHANNELS); + assert((nsamples & 1) == 0 && nsamples >= 0 && nsamples <= MIXER_MAX_SAMPLES_PER_ROUND); + rspq_write_t w = rspq_write_begin(__mixer_overlay_id, MIXER_CMD_CHANNEL, 9); + // a0: ch<<19 | flags<<11 | (nsamples/2) [5|8|9 bits in the 24-bit payload] + rspq_write_arg(&w, ((uint32_t)ch << 19) | ((flags & 0xFF) << 11) | ((uint32_t)nsamples >> 1)); + rspq_write_arg(&w, ((uint32_t)(uint16_t)vol.l << 16) | (uint16_t)vol.r); + rspq_write_arg(&w, pos); + rspq_write_arg(&w, step); + rspq_write_arg(&w, len); + rspq_write_arg(&w, loop_len); + rspq_write_arg(&w, ptr ? PhysicalAddr(ptr) : 0); + rspq_write_arg(&w, ((uint32_t)(uint16_t)vol.dl << 16) | (uint16_t)vol.dr); + rspq_write_arg(&w, ((uint32_t)(uint16_t)vol.tl << 16) | (uint16_t)vol.tr); + rspq_write_end(&w); +} - for (int i=0; iflags & CH_FLAGS_BPS_SHIFT; - int bps_fx64 = bps + MIXER_FX64_FRAC; - - if (ch->ptr) { - int len = ch->len >> bps_fx64; - int loop_len = ch->loop_len >> bps_fx64; - int wpos = ch->pos >> bps_fx64; - // Calculate how many samples we need to have available for this - // frame. We used to only calculate the last sample, but in the unlikely - // case the playback rate is much higher than the output rate, - // this might cause a seek in the waveform (eg: if we play - // one sample every 10, we don't want to cause a seek forward by 9, - // between the last sample of this frame and the first sample of - // next frame). Seeking creates problem with compressed streams, so - // we want to avoid it. - int wlast = (ch->pos + ch->step*(num_samples-1)) >> bps_fx64; - int wnext = (ch->pos + ch->step*num_samples) >> bps_fx64; - int wlen = MAX(wlast-wpos+1, wnext-wpos); - - assertf(wlen >= 0, "channel %d: wpos overflow", i); - tracef("ch:%d wpos:%x wlen:%x len:%x loop_len:%x sbuf_size:%x\n", i, wpos, wlen, len, loop_len, sbuf->size); - - if (!loop_len) { - // If we reached the end of the waveform, stop the channel - // by NULL-ing the buffer pointer. - if (wpos >= len) { - ch->ptr = 0; - if (ch->flags & CH_FLAGS_STEREO) - ch[1].flags &= ~CH_FLAGS_STEREO_SUB; - continue; - } - // When there's no loop, do not ask for more samples then - // actually present in the waveform. - if (wpos+wlen > len) - wlen = len-wpos; - // FIXME: due to a limit in the RSP ucode, we need to overread - // more data, possibly even past the end of the sample - wlen += MIXER_LOOP_OVERREAD >> bps; - assert(wlen >= 0); - } else if (loop_len < sbuf->size) { - // If the whole loop fits the sample buffer, we just need to - // make sure that it is aligned at the start of the buffer, so - // that it can be fully cached. - // To do so, we discard everything that comes before the loop - // (once we enter the loop). - int loop_pos = len - loop_len; - if (wpos >= loop_pos) { - tracef("ch:%d discard to align loop wpos:%x loop_pos:%x\n", i, wpos, loop_pos); - samplebuffer_discard(sbuf, loop_pos); - } +/** + * @brief Record a channel's VADPCM pointers in the ucode channel table. + * + * The codebook is 8-byte aligned, so the residual width travels in its two low + * bits (as `bits-2`) instead of costing a command word: the ucode needs it to + * build the unpacking constants, and it never changes without the codebook + * changing too. + */ +static void mixer_emit_setchannel(int ch, void *codebook, void *state, void *loop_state, int bits) +{ + assert(!codebook || ((PhysicalAddr(codebook) & 7) == 0 && bits >= 2 && bits <= 4)); + rspq_write(__mixer_overlay_id, MIXER_CMD_SETCHANNEL, (uint32_t)ch << 16, + codebook ? PhysicalAddr(codebook) | (bits - 2) : 0, + state ? PhysicalAddr(state) : 0, + loop_state ? PhysicalAddr(loop_state) : 0); +} - // Do not ask more samples than the end of waveform. When we - // get there, the loop has been already fully cached. The RSP - // will correctly follow the loop. - while (wpos >= len) - wpos -= loop_len; - if (wpos+wlen > len) - wlen = len-wpos; - - // FIXME: due to a limit in the RSP ucode, we need to overread - // more data past the loop end. - wlen += MIXER_LOOP_OVERREAD >> bps; - assertf(wlen >= 0, "ch:%d wlen=%x wpos=%x len=%x\n", i, wlen, wpos, len); - } else { - // The loop is larger than the sample buffer. We cannot fully - // cache it, so we will have to unroll it in the sample buffer. - // This happens by default without doing anything: wpos will - // increase, and the actual unrolling logic will be performed - // by waveform_read() (see above). - - // To avoid having wpos growing indefinitely (and overflowing), - // let's force a manual wrapping of the coordinates. Check if - // this is a good moment to do it. - if (sbuf->wpos > len && wpos > len) { - tracef("mixer_poll: wrapping sample buffer loop: sbuf->wpos:%x len:%x\n", sbuf->wpos, len); - samplebuffer_discard(sbuf, wpos); - sbuf->wpos = waveform_wrap_wpos(sbuf->wpos, len, loop_len); - if (sbuf->wnext >= 0) - sbuf->wnext = sbuf->wpos + sbuf->widx; - int wpos2 = waveform_wrap_wpos(wpos, len, loop_len); - ch->pos -= (int64_t)(wpos-wpos2) << bps_fx64; - wpos = wpos2; - } +/** + * @brief Hand a decoder state the CPU re-seeded over to the RSP. + * + * Sent as a command rather than written to #mixer_vstate directly, so that it + * lands after the rounds already enqueued (whose epilog writes that same + * buffer) and before the one that decodes from it. + */ +static void mixer_emit_setstate(int ch) +{ + const uint32_t *src = Mixer.channels[ch].codec_state; + rspq_write(__mixer_overlay_id, MIXER_CMD_SETSTATE, 0, + PhysicalAddr(mixer_vstate(ch)), src[0], src[1], src[2], src[3]); +} - // We will also lie to the RSP ucode telling it that there is - // no loop in this waveform, since the RSP will always see - // the loop unrolled in the buffer, so it doesn't need to - // do anything. - fake_loop |= 1<ptr = (uint8_t*)ptr - (wpos< 0) { + int n = MIN(nbytes, MIXER_COPY_MAX_BYTES); + rspq_write(__mixer_overlay_id, MIXER_CMD_COPY, n, + PhysicalAddr(dst), PhysicalAddr(src)); + dst = (uint8_t*)dst + n; + src = (uint8_t*)src + n; + nbytes -= n; } + rspq_highpri_end(); + return true; +} - volatile rsp_mixer_settings_t *settings = UncachedAddr(&Mixer.ucode_settings); +// Ring space, in units, that pinning a region of "units" samples actually +// consumes: the region itself plus the overread past its end, both rounded to +// whole appends because a block codec (Opus, ULC) always writes full frames. +// Notably the overread is only a handful of samples but still costs a whole +// frame, so it cannot be folded into the region's own rounding. +static int mixer_pin_span(const samplebuffer_t *sbuf, int units) { + int overread = DIVIDE_CEIL(MIXER_LOOP_OVERREAD, sbuf->unit_bytes) + 1; + int f = sbuf->append_units; + if (f > SAMPLEBUFFER_MARGIN_UNITS) + return ROUND_UP(units, f) + ROUND_UP(overread, f); + return units + overread; +} - volatile rsp_mixer_channel_t *rsp_wv = settings->channels; - mixer_fx15_t lvol[MIXER_MAX_CHANNELS] __attribute__((aligned(8))) = {0}; - mixer_fx15_t rvol[MIXER_MAX_CHANNELS] __attribute__((aligned(8))) = {0}; +// True if the whole waveform fits in the samplebuffer, so that it can be pinned +// as a unit instead of just its loop region. +static bool mixer_wave_fits(int i) { + mixer_channel_t *ch = &Mixer.channels[i]; + samplebuffer_t *sbuf = &Mixer.ch_buf[i]; + if (!samplebuffer_is_inited(sbuf)) + return false; + bool vadpcm = (ch->flags & CH_FLAGS_VADPCM) != 0; + int bps = vadpcm ? 0 : (ch->flags & CH_FLAGS_BPS_SHIFT); + int len = ch->len >> (bps + MIXER_FX64_FRAC); + int slen = vadpcm ? DIVIDE_CEIL(len, 16) : len; + return slen > 0 && mixer_pin_span(sbuf, slen) <= sbuf->size; +} - for (int ch=0;chwave; + bool vadpcm = (c->flags & CH_FLAGS_VADPCM) != 0; + bool stereo_vadpcm = vadpcm && (c->flags & CH_FLAGS_STEREO); + int bps = vadpcm ? 0 : (c->flags & CH_FLAGS_BPS_SHIFT); + int bps_fx64 = bps + MIXER_FX64_FRAC; + int ub = sbuf->unit_bytes; + int len = c->len >> bps_fx64; + int loop_len = c->loop_len >> bps_fx64; + // A waveform that fits entirely is pinned from its start, so that its + // attack stays available and the RSP can wrap it right away. + bool whole = mixer_wave_fits(ch); + int cache_start = whole ? 0 : len - loop_len; + int cache_len = whole ? len : loop_len; + // In frames, the pinned region spans from the frame holding its first + // sample to the one holding its last: a loop point is not necessarily + // aligned to a frame, so this is not just the length rounded up. + int sloop_start = vadpcm ? cache_start / 16 : cache_start; + int sloop_len = vadpcm ? DIVIDE_CEIL(cache_start + cache_len, 16) - sloop_start : cache_len; + int overread = (MIXER_LOOP_OVERREAD + ub - 1) / ub; + // What the producer will really write, which for a block codec is more + // than what is asked for here. + int fill = mixer_pin_span(sbuf, sloop_len); + assertf(fill <= sbuf->size, "ch:%d loop cache %x > samplebuffer %x", ch, fill, sbuf->size); + assert(wave && wave->read); + + // The pinned copy has to be one contiguous run, which only the top of the + // ring can guarantee: this is the one flush that cannot restart forward, + // so the rounds that are still reading the ring have to be drained first. + // It only happens when a channel starts looping. + rspq_highpri_sync(); + samplebuffer_flush(sbuf); + sbuf->wpos = 0; + sbuf->wnext = 0; + sbuf->head = 0; + if (stereo_vadpcm) { + samplebuffer_t *sbuf_r = &Mixer.ch_buf[ch+1]; + int fill_r = mixer_pin_span(sbuf_r, sloop_len); + assertf(fill_r <= sbuf_r->size, "ch:%d R loop cache %x > samplebuffer %x", ch+1, fill_r, sbuf_r->size); + samplebuffer_flush(sbuf_r); + sbuf_r->wpos = 0; + sbuf_r->wnext = 0; + sbuf_r->head = 0; + } + // Go through the wrapper: the overread past the loop end must be filled + // with the samples at the loop start, which is exactly what the RSP reads + // there once it starts wrapping the pinned copy on its own. + waveform_read(wave->ctx, sbuf, sloop_start, sloop_len + overread, true); + sbuf->wpos = sloop_start; + sbuf->wnext = sloop_start + sbuf->widx; + if (stereo_vadpcm) { + samplebuffer_t *sbuf_r = &Mixer.ch_buf[ch+1]; + sbuf_r->wpos = sloop_start; + sbuf_r->wnext = sloop_start + sbuf_r->widx; + Mixer.channels[ch+1].ptr = (uint8_t*)SAMPLES_PTR(sbuf_r) - sloop_start * VADPCM_FRAME_BYTES(c->vbits); + Mixer.channels[ch+1].flags |= CH_FLAGS_LOOP_CACHED; + } - // Stereo sub-channel. Will be ignored by RSP but we need to configure - // volume correctly. - if (c->flags & CH_FLAGS_STEREO_SUB) { - rsp_wv[ch].ptr = 0; - lvol[ch] = 0; - rvol[ch] = Mixer.rvol[ch-1]; - continue; + if (vadpcm) + c->ptr = (uint8_t*)SAMPLES_PTR(sbuf) - sloop_start * VADPCM_FRAME_BYTES(c->vbits); + else + c->ptr = (uint8_t*)SAMPLES_PTR(sbuf) - (cache_start << bps); + c->flags |= CH_FLAGS_LOOP_CACHED; + // The fill reads from the first frame of the pinned region, and the seed + // it leaves behind belongs to that frame. Playback is normally already + // somewhere inside the region, decoding the very same frames the stream + // was serving: handing that seed to the RSP would restart its decoder + // several frames behind. Only the seek the fill did for its own sake is + // dropped, never one the channel still owed. + if (vadpcm) { + int frame = (int)(c->pos >> (MIXER_FX64_FRAC + 4)); + if (frame != sloop_start) { + if (c->vframe == frame) { + Mixer.vstate_dirty &= ~mixer_bit(ch); + if (stereo_vadpcm) Mixer.vstate_dirty &= ~mixer_bit(ch+1); + } else { + mixer_vadpcm_seek(c, frame * 16); + } } + c->vframe = frame; + if (stereo_vadpcm) Mixer.channels[ch+1].vframe = frame; + } + mixer_refresh_max_ns(ch); + tracef("ch:%d loop cached at %x len=%x\n", ch, sloop_start, sloop_len); +} - // Check if the channel is stopped - if (!c->ptr) { - rsp_wv[ch].ptr = 0; - // Configure the volume to 0 when the channel is keyed off. This - // makes sure that we smooth volume correctly even for waveforms - // where the sequencer creates an a attack ramp (which would nullify - // the one-tap volume filter if the volume started from max). - lvol[ch] = 0; - rvol[ch] = 0; +// True if the channel's loop is small enough to be pinned into the samplebuffer, +// so that the RSP can wrap it by itself. Large loops are handled via wrap=seek. +static bool mixer_loop_fits(int i) { + mixer_channel_t *ch = &Mixer.channels[i]; + samplebuffer_t *sbuf = &Mixer.ch_buf[i]; + if (!samplebuffer_is_inited(sbuf)) + return false; + bool vadpcm = (ch->flags & CH_FLAGS_VADPCM) != 0; + int bps = vadpcm ? 0 : (ch->flags & CH_FLAGS_BPS_SHIFT); + int loop_len = ch->loop_len >> (bps + MIXER_FX64_FRAC); + int sloop = vadpcm ? DIVIDE_CEIL(loop_len, 16) : loop_len; + return sloop > 0 && mixer_pin_span(sbuf, sloop) <= sbuf->size; +} + +// Restart a loop that the RSP does not wrap by itself, from its loop start. +// Must be done between rounds. +// +// A round stops a couple of samples past the loop point (see +// #mixer_round_length), and #mixer_ch_set_loop can leave the position much +// further than that: those samples play again. Resuming on them instead would +// keep the loop period exact, but only the loop start is a position that every +// codec can restart from (a VADPCM asset only carries the decoder state of the +// frames it was built with). The loops that reach this function are too large +// to be pinned in the samplebuffer, so a couple of samples per iteration are +// inaudible; the short loops that would detune are wrapped by the RSP. +static void mixer_large_loop_wrap(int i) { + mixer_channel_t *ch = &Mixer.channels[i]; + bool vadpcm = (ch->flags & CH_FLAGS_VADPCM) != 0; + int bps_fx64 = (vadpcm ? 0 : (ch->flags & CH_FLAGS_BPS_SHIFT)) + MIXER_FX64_FRAC; + int wpos = ch->pos >> bps_fx64; + int loop_start = (ch->len - ch->loop_len) >> bps_fx64; + ch->pos -= (int64_t)(wpos - loop_start) << bps_fx64; + samplebuffer_flush(&Mixer.ch_buf[i]); + if (vadpcm) + mixer_vadpcm_seek(ch, loop_start); + tracef("ch:%d large-loop restart %x -> %x\n", i, wpos, loop_start); +} + +// End-of-sample, loop-cache transition and large-loop seek. Runs before each +// round: a short loop can be reached in the middle of a mixer_exec, and from +// that point on the RSP must be the one wrapping it. +static void mixer_update_loops(void) { + for (int i = 0; i < Mixer.hi_ch; i++) { + mixer_channel_t *ch = &Mixer.channels[i]; + bool vadpcm = (ch->flags & CH_FLAGS_VADPCM) != 0; + int bps_fx64 = (vadpcm ? 0 : (ch->flags & CH_FLAGS_BPS_SHIFT)) + MIXER_FX64_FRAC; + + if (!ch->ptr || (ch->flags & CH_FLAGS_STEREO_SUB)) continue; - } - // Convert to RSP mixer channel structure truncating 64-bit values to 32-bit. - // We don't need full absolute position on the RSP, so 32-bit is more - // than enough. In fact, we only expose 31 bits, so that we can use the - // 32nd bit later to correctly update the position without overflow bugs. - rsp_wv[ch].pos = (uint32_t)c->pos & 0x7FFFFFFF; - rsp_wv[ch].step = (uint32_t)c->step & 0x7FFFFFFF; - rsp_wv[ch].ptr = c->ptr + ((c->pos & ~0x7FFFFFFF) >> MIXER_FX64_FRAC); - rsp_wv[ch].flags = c->flags; - - // If the loop is fake (i.e. we are unrolling it), or the current - // position has been truncated but it's far from the end of the waveform, - // just tell the RSP that there is no loop. - if (fake_loop & (1<pos>>31 != c->len>>31) { - rsp_wv[ch].len = 0xFFFFFFFF; - rsp_wv[ch].loop_len = 0; - } else { - rsp_wv[ch].len = (uint32_t)c->len & 0x7FFFFFFF; - // We can't represent a very long loop in RSP. But those loops - // should be unrolled anyway (and thus be a fake_loop), so we - // should not get here. - assert(c->loop_len <= 0x7FFFFFFF); - rsp_wv[ch].loop_len = (uint32_t)c->loop_len & 0x7FFFFFFF; + int len = ch->len >> bps_fx64; + int loop_len = ch->loop_len >> bps_fx64; + int wpos = ch->pos >> bps_fx64; + + if (!loop_len) { + if (wpos >= len) + mixer_ch_stop(i); + continue; } + if (ch->flags & (CH_FLAGS_RESIDENT | CH_FLAGS_LOOP_CACHED)) + continue; - if (c->flags & CH_FLAGS_STEREO) { - lvol[ch] = Mixer.lvol[ch]; - rvol[ch] = 0; - } else { - lvol[ch] = Mixer.lvol[ch]; - rvol[ch] = Mixer.rvol[ch]; + // #mixer_ch_set_loop shrinks len to the loop end, and can do it with + // playback already past it: restart before anything reads from there. + if (wpos >= len) { + mixer_large_loop_wrap(i); + wpos = ch->pos >> bps_fx64; } + if (mixer_loop_fits(i) && (wpos >= len - loop_len || mixer_wave_fits(i))) + mixer_fill_loop_cache(i); } +} - uint32_t *lvol32 = (uint32_t*)lvol; - uint32_t *rvol32 = (uint32_t*)rvol; - for (int ch=0;chlvol[ch] = lvol32[ch]; - settings->rvol[ch] = rvol32[ch]; +// Refresh the ucode per-channel VADPCM table. The pointers only change when a +// new waveform starts playing, so in steady state this emits nothing. +static void mixer_refresh_chtbl(void) { + if (!Mixer.chtbl_dirty) + return; + for (int ch = 0; ch < Mixer.hi_ch; ch++) { + if (!(Mixer.chtbl_dirty & mixer_bit(ch))) + continue; + mixer_channel_t *c = &Mixer.channels[ch]; + mixer_chtbl_t *sh = &Mixer.chtbl[ch]; + mixer_channel_t *src = (c->flags & CH_FLAGS_STEREO_SUB) ? c-1 : c; + Mixer.chtbl_dirty &= ~mixer_bit(ch); + if (!src->ptr || !(c->flags & CH_FLAGS_VADPCM)) + continue; + void *state = mixer_vstate(ch); + *sh = (mixer_chtbl_t){ c->codebook, state, c->loop_state }; + mixer_emit_setchannel(ch, c->codebook, state, c->loop_state, c->vbits); } +} - // Check if we the user pressed RESET. If so, we can apply - // a simple global volume ramp to fade out the volume. - // This is just a user-level feature. audio.c will truncate - // DMA transfers to AI anyway. +// Global volume to apply this call, including the fade-out on reset. +static mixer_fx16_t mixer_global_volume(void) { float gvol = Mixer.vol; uint32_t reset_time = exception_reset_time(); if (reset_time) { @@ -683,27 +1892,520 @@ static void mixer_exec(int32_t *out, int num_samples) { float elapsed = (float)reset_time / TICKS_PER_SECOND; gvol *= (FADE_OUT_TIME - MIN(elapsed, FADE_OUT_TIME)) / FADE_OUT_TIME; } + return MIXER_FX16(gvol); +} - uint32_t t0 = TICKS_READ(); - rspq_highpri_begin(); - rspq_write(__mixer_overlay_id, 0, - (((uint32_t)MIXER_FX16(gvol)) & 0xFFFF), - (num_samples << 16) | Mixer.num_channels, - PhysicalAddr(out), - PhysicalAddr(&Mixer.ucode_settings)); - rspq_highpri_end(); +/** Cap of output samples a streamed channel can take in one round. + * + * A streamed channel can only be given one contiguous window of the + * samplebuffer, that is #SAMPLEBUFFER_MARGIN_UNITS minus the units the RSP may + * overread past its end. Convert that window (16 samples per unit for VADPCM, + * one otherwise) into output samples through the resampling step: past this + * point the RSP would outrun what the CPU can hand it in a single piece. + * Channels reading straight from RDRAM (resident, pinned loop, stereo sub) + * have no such limit. + * + * One unit is reserved on top of the overread because #mixer_channel_window + * counts an inclusive span (last-pos+1), which can be one past the position + * advance that this cap is derived from when the fractional part of pos is + * large relative to step. For VADPCM that same reserve also absorbs the + * ceiling to frames: a span of N samples starting mid-frame covers + * ceil((pos%16+N)/16) frames, one more than N/16 when pos is not aligned. + */ +static void mixer_refresh_max_ns(int ch) { + mixer_channel_t *c = &Mixer.channels[ch]; + if (!c->ptr || !c->step || + (c->flags & (CH_FLAGS_RESIDENT | CH_FLAGS_LOOP_CACHED | CH_FLAGS_STEREO_SUB))) { + c->max_round_ns = MIXER_MAX_SAMPLES_PER_ROUND; + return; + } + bool vadpcm = (c->flags & CH_FLAGS_VADPCM) != 0; + int bps_fx64 = (vadpcm ? 0 : (c->flags & CH_FLAGS_BPS_SHIFT)) + MIXER_FX64_FRAC; + int ub = Mixer.ch_buf[ch].unit_bytes; + int overread = (MIXER_LOOP_OVERREAD + ub - 1) / ub; + int max_wlen = MAX(SAMPLEBUFFER_MARGIN_UNITS - overread - 1, 1); + uint64_t units = vadpcm ? (uint64_t)max_wlen * 16 : (uint64_t)max_wlen; + int ns = (int)((units << bps_fx64) / c->step); + c->max_round_ns = MIN(MAX(ns, 1), MIXER_MAX_SAMPLES_PER_ROUND); +} - rspq_highpri_sync(); +// Number of samples the next round can mix: a full round, unless a streamed +// channel would outrun what the CPU is able to feed it in one go. Volume ramps +// never enter into it: the ucode is told where they end and clamps to it, so +// one can land anywhere inside a round. +// +// The result is always even, so that each round advances the output pointer by +// a multiple of 8 bytes and MIX_FLUSH can DMA the accumulator out as-is +// (see #mixer_poll_async). +static int mixer_round_length(int max_ns) { + int ns = MIN(max_ns, MIXER_MAX_SAMPLES_PER_ROUND); + + for (int ch = 0; ch < Mixer.hi_ch; ch++) { + mixer_channel_t *c = &Mixer.channels[ch]; + if (!c->ptr || !c->step || + (c->flags & (CH_FLAGS_RESIDENT | CH_FLAGS_LOOP_CACHED | CH_FLAGS_STEREO_SUB))) + continue; + // max_round_ns is 0 until the first set_freq/play refresh; treat as + // unlimited so a stale zero cannot collapse the round to empty (which + // would spin forever in mixer_exec). + if (c->max_round_ns > 0) + ns = MIN(ns, c->max_round_ns); + + // Stop where the CPU has to step in: the start of a small loop (pinned + // by mixer_update_loops before the next round), or len for a large one + // (rebased by a seek). + if (c->loop_len) { + mixer_fx64_t limit = mixer_loop_fits(ch) ? c->len - c->loop_len : c->len; + if (c->pos < limit && c->pos + c->step * (uint64_t)ns >= limit) { + int64_t dist = (int64_t)(limit - c->pos); + int ns2 = (int)((dist + (int64_t)c->step - 1) / (int64_t)c->step); + ns = MIN(ns, MAX(ns2, 1)); + } + } + } + + // Truncate to an even number of samples. Stopping one sample before a + // constraint is always safe (the next round resumes there); the only + // exception is a limit exactly one sample away, where we mix one sample + // too many. That sample is still valid data: past a small loop point it + // is the loop body the CPU is about to pin, past the end of a large loop + // it is covered by #MIXER_LOOP_OVERREAD, and in both cases the CPU takes + // over from the next round. + return MAX(ns & ~1, 2); +} - __mixer_profile_rsp += TICKS_READ() - t0; +// Output volumes of a channel whose own volume pair is (@p vl, @p vr): a stereo +// owner only carries the L plane and its sub channel only the R one, and +// FORCE_MONO folds the surviving plane onto both outputs. @p flags are the +// owner's flags. +static void mixer_fold_volumes(uint32_t flags, bool sub, mixer_fx16_t gvol, + mixer_fx15_t vl, mixer_fx15_t vr, mixer_fx15_t *lvol, mixer_fx15_t *rvol) +{ + bool mono = (flags & CH_FLAGS_FORCE_MONO) != 0; + if (sub) { + *lvol = mono ? mixer_apply_gvol(vr >> 1, gvol) : 0; + *rvol = mixer_apply_gvol(mono ? vr >> 1 : vr, gvol); + } else if (flags & CH_FLAGS_STEREO) { + *lvol = mixer_apply_gvol(mono ? vl >> 1 : vl, gvol); + *rvol = mono ? *lvol : 0; + } else if (mono) { + *lvol = *rvol = mixer_apply_gvol((vl + vr) >> 1, gvol); + } else { + *lvol = mixer_apply_gvol(vl, gvol); + *rvol = mixer_apply_gvol(vr, gvol); + } +} - for (int i=0;i= 1.0f) return v; + if (g <= 0.0f) return 0; + return (mixer_fx15_t)((float)v * g + 0.5f); +} + +// Increment the ucode adds every 4 samples to walk @p delta units in @p +// nblocks of them. Rounded away from zero, so that the ramp always reaches +// its target within the blocks it was given: overshooting is free, since the +// ucode clamps, while falling short would leave the volume hanging. +static mixer_fx15_t mixer_ramp_step(int delta, int nblocks) +{ + int d = DIVIDE_CEIL(ABS(delta), nblocks); + return (mixer_fx15_t)CLAMP(delta < 0 ? -d : d, -0x8000, 0x7FFF); +} + +// Volumes a channel is mixed with over the round of @p ns samples starting at +// tick @p tick: the value at its first sample, the value it stops on, and the +// increment that walks from one to the other every 4 samples. All are derived +// from the absolute tick, so a round never inherits the rounding of the ones +// before it. Mono gain is folded in here; the RSP still sees a plain linear +// L/R walk for the round. +static mixer_round_vol_t mixer_channel_volumes(int ch, uint32_t flags, bool sub, + mixer_fx16_t gvol, int64_t tick, int ns) +{ + // A stereo sub channel is a plane of its owner, and follows its ramps. + int och = sub ? ch-1 : ch; + mixer_round_vol_t vol = {0}; + mixer_fx15_t vl0, vr0; + float g0 = mixer_ramp_at(&Mixer.gain_ramp[och], Mixer.gain[och], tick); + + mixer_ch_vol_at(och, tick, &vl0, &vr0); + mixer_fold_volumes(flags, sub, gvol, + mixer_fx15_scale(vl0, g0), mixer_fx15_scale(vr0, g0), + &vol.l, &vol.r); + vol.tl = vol.l; + vol.tr = vol.r; + + bool l_ramping = mixer_ramp_active(&Mixer.lvol_ramp[och], tick); + bool r_ramping = mixer_ramp_active(&Mixer.rvol_ramp[och], tick); + bool gain_ramping = mixer_ramp_active(&Mixer.gain_ramp[och], tick); + if (l_ramping || r_ramping || gain_ramping) { + // The slice of the ramp(s) this round covers, and nothing beyond it: the + // volume it ends on is what the ucode clamps to, and the increment + // walks the round's own blocks. The increment is a whole number of + // fx15 units, so a ramp shallower than one unit per block cannot be + // walked at its slope; taking the end of the whole ramp as the bound + // would let such a ramp run through every round many times too fast + // and be pulled back to the right value at the next one, which is an + // audible flutter on any ramp of more than a couple of seconds. Held + // to the round, it lands early and waits there, and the round after + // it picks up exactly where it stopped. With several ramps, walk until + // the later of their ends (within the round). + int64_t end = tick + ns; + int64_t last = tick; + if (l_ramping) + last = MAX(last, MIN(Mixer.lvol_ramp[och].start_tick + Mixer.lvol_ramp[och].duration, end)); + if (r_ramping) + last = MAX(last, MIN(Mixer.rvol_ramp[och].start_tick + Mixer.rvol_ramp[och].duration, end)); + if (gain_ramping) + last = MAX(last, MIN(Mixer.gain_ramp[och].start_tick + Mixer.gain_ramp[och].duration, end)); + if (last <= tick) last = end; + + // Reuse the unscaled L/R (and/or gain) when that ramp is idle. + mixer_fx15_t el = vl0, er = vr0; + float g1 = g0; + if (gain_ramping) + g1 = mixer_ramp_at(&Mixer.gain_ramp[och], Mixer.gain[och], last); + if (l_ramping || r_ramping) + mixer_ch_vol_at(och, last, &el, &er); + mixer_fold_volumes(flags, sub, gvol, + mixer_fx15_scale(el, g1), mixer_fx15_scale(er, g1), + &vol.tl, &vol.tr); + int nblocks = MAX(DIVIDE_CEIL((int)(last - tick), 4), 1); + vol.dl = mixer_ramp_step(vol.tl - vol.l, nblocks); + vol.dr = mixer_ramp_step(vol.tr - vol.r, nblocks); + } + return vol; +} + +// Waveform bounds to send to the RSP. The top bit of pos and len is a wrap +// flag: when the two differ the position is past the end of the waveform, and +// the RSP must not compare against len. @p wrap is false for the loops that +// the CPU wraps by itself between rounds. +static void mixer_rsp_bounds(mixer_channel_t *c, bool wrap, uint32_t *len, uint32_t *loop_len) { + if (c->pos>>31 != c->len>>31) { + *len = 0xFFFFFFFF; + *loop_len = 0; + } else { + *len = (uint32_t)c->len & 0x7FFFFFFF; + *loop_len = wrap ? (uint32_t)c->loop_len & 0x7FFFFFFF : 0; + } +} + +// Byte offset that the CPU folds into the pointer sent to the RSP. The ucode +// only receives the low 31 bits of the position, so a streamed channel (whose +// position grows with the stream instead of wrapping) must carry everything +// above that bit in the base pointer. VADPCM positions count samples, and the +// ucode addresses one compressed frame every 16 of them. +static int32_t mixer_pos_fold(const mixer_channel_t *c) { + int64_t high = c->pos & ~(int64_t)0x7FFFFFFF; + if (c->flags & CH_FLAGS_VADPCM) + return (high >> (MIXER_FX64_FRAC+4)) * VADPCM_FRAME_BYTES(c->vbits); + return high >> MIXER_FX64_FRAC; +} + +// samplebuffer window that @p ns output samples read from a channel, in units +// (VADPCM frames or PCM samples), including the overread the RSP does at loops. +static void mixer_channel_window(int ch, int ns, int *wpos, int *wlen) { + mixer_channel_t *c = &Mixer.channels[ch]; + samplebuffer_t *sbuf = &Mixer.ch_buf[ch]; + bool vadpcm = (c->flags & CH_FLAGS_VADPCM) != 0; + int bps = vadpcm ? 0 : (c->flags & CH_FLAGS_BPS_SHIFT); + int bps_fx64 = bps + MIXER_FX64_FRAC; + int ub = sbuf->unit_bytes; + + int pos = c->pos >> bps_fx64; + int last = (c->pos + c->step * (uint64_t)(ns > 0 ? ns-1 : 0)) >> bps_fx64; + int next = (c->pos + c->step * (uint64_t)ns) >> bps_fx64; + int len = MAX(last - pos + 1, next - pos); + + if (vadpcm) { + *wpos = pos / 16; + *wlen = (pos + len + 15) / 16 - *wpos + (MIXER_LOOP_OVERREAD + ub - 1) / ub; + } else { + *wpos = pos; + *wlen = len + (MIXER_LOOP_OVERREAD >> bps); + } +} + +// Fetch from the samplebuffer the input window this round is going to read. +// Updates c->ptr to the waveform base; the RSP pointer for PCM may need +// the high-bit pos offset applied by the caller. +static void mixer_fetch_window(int ch, int ns) { + mixer_channel_t *c = &Mixer.channels[ch]; + samplebuffer_t *sbuf = &Mixer.ch_buf[ch]; + int wpos, wlen; + mixer_channel_window(ch, ns, &wpos, &wlen); + +#ifndef NDEBUG + // Every window handed to a round stays live until the RSP runs it. A ring + // that does not span a whole poll would overwrite its own windows: the + // poll barrier (see #mixer_poll_barrier) only frees space from older polls. + // Waveforms that end up entirely in the ring never refill: exempt. + if (!mixer_wave_fits(ch) && !mixer_loop_fits(ch)) { + int poll_ns = __mixer_inflight_samples() / MIXER_POLL_LOOKAHEAD; + bool vadpcm = (c->flags & CH_FLAGS_VADPCM) != 0; + int bps_fx64 = (vadpcm ? 0 : (c->flags & CH_FLAGS_BPS_SHIFT)) + MIXER_FX64_FRAC; + uint64_t span = ((uint64_t)poll_ns * c->step) >> bps_fx64; + int need = (vadpcm ? (int)DIVIDE_CEIL(span, 16) : (int)span) + + SAMPLEBUFFER_PREFETCH_UNITS + + DIVIDE_CEIL(MIXER_LOOP_OVERREAD, sbuf->unit_bytes) + + sbuf->append_units; + assertf(sbuf->size >= need, + "ch%d: sample buffer holds %d units, %d needed to play at %.0f Hz " + "for the %d samples of a poll; raise max_buf_sz in " + "mixer_ch_set_limits (or convert the asset again)", + ch, sbuf->size, need, + (double)c->step * Mixer.sample_rate / (double)(1ull << bps_fx64), + poll_ns); + } +#endif + + if (c->flags & CH_FLAGS_VADPCM) { + if (sbuf->widx == 0 || wpos < sbuf->wpos || wpos > sbuf->wpos + sbuf->widx) + c->vframe = wpos; + void *p = samplebuffer_get(sbuf, wpos, &wlen); + assert(p); + c->ptr = (uint8_t*)p - wpos * VADPCM_FRAME_BYTES(c->vbits); + } else { + void *p = samplebuffer_get(sbuf, wpos, &wlen); + assert(p); + c->ptr = (uint8_t*)p - (wpos << (c->flags & CH_FLAGS_BPS_SHIFT)); + } +} + +// Kick the fetches the next mixer_exec will need, so their PI DMA flies while +// the CPU sequences the song and the RSP mixes the rounds just enqueued. +static void mixer_prefetch_next(int num_samples) { + for (int ch = 0; ch < Mixer.hi_ch; ch++) { + mixer_channel_t *c = &Mixer.channels[ch]; + mixer_channel_t *owner = (c->flags & CH_FLAGS_STEREO_SUB) ? c-1 : c; + if (!owner->ptr || !c->step || + (owner->flags & (CH_FLAGS_RESIDENT | CH_FLAGS_LOOP_CACHED))) + continue; + // A stereo pair streams through the owner's read, which fills both rings. + if (c->flags & CH_FLAGS_STEREO_SUB) + continue; + // Nothing to overlap with if the waveform is produced synchronously. + waveform_t *wave = Mixer.ch_buf[ch].wave; + if (!wave || !wave->async_read) + continue; + int wpos, wlen; + mixer_channel_window(ch, num_samples, &wpos, &wlen); + samplebuffer_prefetch(&Mixer.ch_buf[ch], wpos, wlen); + } +} + +/** Wait for any in-flight PI DMA into the samplebuffer this channel reads. */ +static void mixer_wait_channel_dma(int ch) { + mixer_channel_t *c = &Mixer.channels[ch]; + if (c->flags & CH_FLAGS_RESIDENT) + return; + // PCM stereo R shares the owner's interleaved buffer. + int bufch = ((c->flags & CH_FLAGS_STEREO_SUB) && !(c->flags & CH_FLAGS_VADPCM)) ? ch - 1 : ch; + if (samplebuffer_is_inited(&Mixer.ch_buf[bufch])) + samplebuffer_dma_wait(&Mixer.ch_buf[bufch]); +} + +// Emit one mix round: a MIX_CHANNEL per active channel, plus the flush that +// writes the accumulator out to @p out. +// +// Phase A kicks all streamed fetches so PI DMAs pipeline across channels; +// Phase B waits for each channel's DMA only when about to emit its command. +static void mixer_emit_round(int32_t *out, int ns, mixer_fx16_t gvol, int64_t tick) { + bool clear_accum = true; + + // The windows fetched below belong to this round, and stay live until the + // RSP publishes its id back (see MIX_FLUSH / #__mixer_round_done). + uint32_t round_id = ++Mixer.round_id; + + PROFILE_SCOPE(PS_MIXER_FETCH) { + // Frequency ramps are piecewise-constant per round: sample Hz at @p tick + // into step before sizing fetches and emitting MIX_CHANNEL. + for (int ch = 0; ch < Mixer.hi_ch; ch++) + mixer_ch_sync_freq(ch, tick); + + // Phase A: enqueue all PI DMAs back-to-back. + for (int ch = 0; ch < Mixer.hi_ch; ch++) { + mixer_channel_t *c = &Mixer.channels[ch]; + mixer_channel_t *owner = (c->flags & CH_FLAGS_STEREO_SUB) ? c-1 : c; + if (!owner->ptr || (owner->flags & (CH_FLAGS_RESIDENT | CH_FLAGS_LOOP_CACHED))) + continue; + if (c->flags & CH_FLAGS_STEREO_SUB) { + if (!(owner->flags & CH_FLAGS_VADPCM)) + continue; // PCM R shares the owner's fetch + c->pos = owner->pos; + c->step = owner->step; + c->len = owner->len; + c->loop_len = owner->loop_len; + } + mixer_fetch_window(ch, ns); + } + } + + // Phase B: emit, syncing each channel's DMA at the last moment. + PROFILE_SCOPE(PS_MIXER_CMD) { + for (int ch = 0; ch < Mixer.hi_ch; ch++) { + mixer_channel_t *c = &Mixer.channels[ch]; + mixer_channel_t *owner = (c->flags & CH_FLAGS_STEREO_SUB) ? c-1 : c; + uint32_t flags = c->flags & (CH_FLAGS_BPS_SHIFT | CH_FLAGS_16BIT | + CH_FLAGS_STEREO | CH_FLAGS_STEREO_SUB | CH_FLAGS_VADPCM); + mixer_round_vol_t vol = {0}; + void *ptr = NULL; + uint32_t pos = 0, step = 0, len = 0xFFFFFFFF, loop_len = 0; + + if (!owner->ptr) { + // A channel with no data has nothing to mix and no state left + // behind on the RSP: drop it from the round entirely. + continue; + } else if (c->flags & CH_FLAGS_STEREO_SUB) { + PROFILE_SCOPE(PS_MIXER_VOL) { + vol = mixer_channel_volumes(ch, owner->flags, true, gvol, tick, ns); + } + + if (!(owner->flags & CH_FLAGS_VADPCM)) { + // PCM interleaved: extract R from the owner's sample stream. + flags = (owner->flags & (CH_FLAGS_BPS_SHIFT | CH_FLAGS_16BIT | CH_FLAGS_STEREO)) | CH_FLAGS_STEREO_SUB; + pos = (uint32_t)owner->pos & 0x7FFFFFFF; + step = (uint32_t)owner->step & 0x7FFFFFFF; + ptr = (uint8_t*)owner->ptr + mixer_pos_fold(owner); + mixer_rsp_bounds(owner, true, &len, &loop_len); + } else { + // Stereo VADPCM R: timing already synced in Phase A when streamed. + c->pos = owner->pos; + c->step = owner->step; + c->len = owner->len; + c->loop_len = owner->loop_len; + flags = CH_FLAGS_VADPCM | CH_FLAGS_16BIT; + pos = (uint32_t)c->pos & 0x7FFFFFFF; + step = (uint32_t)c->step & 0x7FFFFFFF; + if (owner->flags & (CH_FLAGS_RESIDENT | CH_FLAGS_LOOP_CACHED)) { + ptr = c->ptr; + mixer_rsp_bounds(c, true, &len, &loop_len); + } else { + ptr = (uint8_t*)c->ptr + mixer_pos_fold(c); + } + } + } else { + PROFILE_SCOPE(PS_MIXER_VOL) { + vol = mixer_channel_volumes(ch, c->flags, false, gvol, tick, ns); + } + pos = (uint32_t)c->pos & 0x7FFFFFFF; + step = (uint32_t)c->step & 0x7FFFFFFF; + + if (c->flags & (CH_FLAGS_RESIDENT | CH_FLAGS_LOOP_CACHED)) { + ptr = c->ptr; + mixer_rsp_bounds(c, true, &len, &loop_len); + } else { + ptr = (uint8_t*)c->ptr + mixer_pos_fold(c); + // Small loops are pinned by mixer_update_loops, large ones are + // wrapped by the CPU between rounds: either way the RSP is + // never allowed to wrap a streamed channel. + if (!c->loop_len) + mixer_rsp_bounds(c, false, &len, &loop_len); + } + } + + PROFILE_SCOPE(PS_MIXER_DMAWAIT) { + if (owner->ptr) + mixer_wait_channel_dma(ch); + } + + PROFILE_SCOPE(PS_MIXER_RSPQ) { + // A state the CPU re-seeded rides with the first round that needs it, + // which is where it belongs in the queue. + if (Mixer.vstate_dirty & mixer_bit(ch)) { + Mixer.vstate_dirty &= ~mixer_bit(ch); + if ((c->flags & CH_FLAGS_VADPCM) && c->codec_state) + mixer_emit_setstate(ch); + } + + // A command that starts on the frame the loop starts on must decode + // from the state saved for that frame: the one the stream left behind + // belongs to the end of the waveform, since it is the CPU that wraps + // the position between rounds. The ucode has no cheap way to tell, + // while here it is a comparison on values already at hand. + if ((flags & CH_FLAGS_VADPCM) && loop_len && Mixer.chtbl[ch].loop_state && + (pos >> (MIXER_FX64_FRAC+4)) == ((len - loop_len) >> (MIXER_FX64_FRAC+4))) + flags |= CH_FLAGS_VLOOP_STATE; + + if (clear_accum) { + flags |= CH_FLAGS_CLEAR_ACCUM; + clear_accum = false; + } + + mixer_emit_channel(ch, flags, vol, pos, step, len, loop_len, ptr, ns); + } + } + + PROFILE_START(PS_MIXER_RSPQ, 1); + // All channels were silent: the accumulator must still be cleared before + // being flushed out. + if (clear_accum) + mixer_emit_channel(0, CH_FLAGS_CLEAR_ACCUM, (mixer_round_vol_t){0}, 0, 0, 0, 0, NULL, ns); + + rspq_write(__mixer_overlay_id, MIXER_CMD_FLUSH, + (uint32_t)ns, PhysicalAddr(out), round_id, + PhysicalAddr((void*)Mixer.round_done)); + PROFILE_STOP(PS_MIXER_RSPQ, 1); + } +} + +// Advance all channels by the samples just mixed, wrapping the loops that the +// RSP does not wrap by itself. +static void mixer_advance(int ns) { + for (int i = 0; i < Mixer.hi_ch; i++) { mixer_channel_t *ch = &Mixer.channels[i]; - if (ch->ptr) - ch->pos += (uint64_t)rsp_wv[i].pos - (uint64_t)(ch->pos & 0x7FFFFFFF); + if (!ch->ptr || (ch->flags & CH_FLAGS_STEREO_SUB)) + continue; + ch->pos += ch->step * (uint64_t)ns; + if (ch->loop_len && ch->pos >= ch->len) { + if (ch->flags & (CH_FLAGS_RESIDENT | CH_FLAGS_LOOP_CACHED)) { + while (ch->pos >= ch->len) + ch->pos -= ch->loop_len; + } else { + // Streamed loop crossed between two rounds of the same call. + mixer_large_loop_wrap(i); + } + } + if (ch->flags & CH_FLAGS_VADPCM) { + ch->vframe = (int)(ch->pos >> (MIXER_FX64_FRAC+4)); + if (ch->flags & CH_FLAGS_STEREO) ch[1].vframe = ch->vframe; + } } +} + +static void mixer_invoke_soft(int round_ns); + +static void mixer_exec(int32_t *out, int num_samples) { + PROFILE_SCOPE(PS_MIXER_EXEC) { + tracef("mixer_exec: 0x%x samples\n", num_samples); + + mixer_fx16_t gvol = mixer_global_volume(); - Mixer.ticks += num_samples; + for (int offset = 0; offset < num_samples; ) { + int ns; + rspq_highpri_begin(); + PROFILE_SCOPE(PS_MIXER_PREP) { + mixer_update_loops(); + mixer_refresh_chtbl(); + ns = mixer_round_length(num_samples - offset); + } + PROFILE_SCOPE(PS_MIXER_EMIT) { + mixer_emit_round(out + offset, ns, gvol, Mixer.ticks); + } + PROFILE_SCOPE(PS_MIXER_ADVANCE) { + mixer_advance(ns); + Mixer.ticks += ns; + } + rspq_highpri_end(); + mixer_invoke_soft(ns); + offset += ns; + } + + PROFILE_SCOPE(PS_MIXER_PREFETCH) { + mixer_prefetch_next(num_samples); + } + } } static mixer_event_t* mixer_next_event(void) { @@ -716,6 +2418,7 @@ static mixer_event_t* mixer_next_event(void) { } void mixer_add_event(int64_t delay, MixerEvent cb, void *ctx) { + assertf(Mixer.num_events < MAX_EVENTS, "too many mixer events"); Mixer.events[Mixer.num_events++] = (mixer_event_t){ .cb = cb, .ctx = ctx, @@ -734,7 +2437,50 @@ void mixer_remove_event(MixerEvent cb, void *ctx) { assertf("mixer_remove_event: specified event does not exist\ncb:%p ctx:%p", (void*)cb, ctx); } -void mixer_poll(int16_t *out16, int num_samples) { +static void mixer_invoke_soft(int round_ns) +{ + for (int i = 0; i < Mixer.num_soft; ) { + int n = Mixer.num_soft; + Mixer.soft[i].cb(Mixer.soft[i].ctx, round_ns); + if (Mixer.num_soft < n) + continue; // callback removed itself (or another); recheck index + i++; + } +} + +void mixer_add_soft_event(MixerSoftEvent cb, void *ctx) +{ + assertf(cb, "mixer_add_soft_event: NULL callback"); + assertf(Mixer.num_soft < MAX_SOFT_EVENTS, "too many soft mixer events"); + Mixer.soft[Mixer.num_soft++] = (mixer_soft_event_t){ .cb = cb, .ctx = ctx }; +} + +void mixer_remove_soft_event(MixerSoftEvent cb, void *ctx) +{ + for (int i = 0; i < Mixer.num_soft; i++) { + if (Mixer.soft[i].cb == cb && Mixer.soft[i].ctx == ctx) { + memmove(&Mixer.soft[i], &Mixer.soft[i+1], + sizeof(mixer_soft_event_t) * (Mixer.num_soft - i - 1)); + Mixer.num_soft--; + return; + } + } + assertf("mixer_remove_soft_event: specified event does not exist\ncb:%p ctx:%p", + (void*)cb, ctx); +} + +void mixer_throttle(float num_samples) { + Mixer.max_samples += num_samples; + Mixer.throttled = true; +} + +void mixer_unthrottle(void) { + Mixer.max_samples = 0; + Mixer.throttled = false; +} + +/** @brief Poll the mixer asynchronously. */ +void mixer_poll_async(int16_t *out16, int num_samples) { int32_t *out = (int32_t*)out16; // Since the AI can only play an even number of samples, @@ -742,16 +2488,52 @@ void mixer_poll(int16_t *out16, int num_samples) { // otherwise buffering might become complicated / impossible. assert(num_samples % 2 == 0); + // MIX_FLUSH DMAs the accumulator straight out of DMEM, so every output + // address the RSP is given must be 8-byte aligned. That holds as long as + // the buffer itself is aligned and we only ever advance it by an even + // number of stereo samples, which is what the rest of this function and + // #mixer_round_length guarantee. + assertf(((uint32_t)out16 & 7) == 0, + "mixer output buffer must be 8-byte aligned: %p", out16); + + // Sample buffers span #MIXER_POLL_LOOKAHEAD polls. Wait for the oldest + // still-live one before starting, so a refill cannot overwrite windows + // the RSP has not read yet. The first LOOKAHEAD polls never wait. + mixer_poll_barrier(); + + assertf(num_samples <= __mixer_inflight_samples(), + "cannot mix %d samples in one go: sample buffers are sized for %d", + num_samples, __mixer_inflight_samples()); + + // Check if the mixer is throttled. If so, do not produce more + // than the allowance (with a small extra equal to a full audio buffer, + // to avoid issues with fixed-size buffers like those provided by audio.c), + // and silence after it. + if (Mixer.throttled) { + int extra = Mixer.sample_rate / MIXER_POLL_PER_SECOND; + int total = num_samples; + num_samples = (int)MIN(num_samples, Mixer.max_samples+extra) & ~1; + Mixer.max_samples -= num_samples; + memset(out + num_samples, 0, (total - num_samples) * sizeof(int32_t)); + } + while (num_samples > 0) { mixer_event_t *e = mixer_next_event(); - int ns = MIN(num_samples, e ? e->ticks - Mixer.ticks : num_samples); + // Stop at the next event, rounding the split up to an even number of + // samples. The event then fires up to one sample late, but without + // drifting: its schedule stays anchored to absolute ticks. + int ns = num_samples; + if (e) { + int64_t delay = e->ticks - Mixer.ticks; + ns = delay > 0 ? MIN(ns, (int)ROUND_UP(delay, 2)) : 0; + } if (ns > 0) { mixer_exec(out, ns); out += ns; num_samples -= ns; } - if (e && Mixer.ticks == e->ticks) { + if (e && Mixer.ticks >= e->ticks) { int64_t repeat = e->cb(e->ctx); if (repeat) e->ticks += repeat; @@ -759,4 +2541,59 @@ void mixer_poll(int16_t *out16, int num_samples) { mixer_remove_event(e->cb, e->ctx); } } + + // Remember where this poll ended, so that #mixer_poll_would_wait / + // #mixer_poll_barrier can tell whether the ring space of the polls + // before it has been freed. + Mixer.poll_round[Mixer.poll_count % MIXER_POLL_LOOKAHEAD] = Mixer.round_id; + Mixer.poll_count++; +} + +void mixer_poll(int16_t *out16, int num_samples) { + // Sample buffers only span what a poll can queue (see + // #__mixer_inflight_samples), so a caller asking for more than that in one + // go would wrap onto windows still live from earlier in the same call. + // Sync along the way instead, which costs nothing for the usual sizes. + int budget = __mixer_inflight_samples() & ~1; + while (num_samples > budget) { + mixer_poll_async(out16, budget); + rspq_highpri_sync(); + out16 += budget * 2; + num_samples -= budget; + } + mixer_poll_async(out16, num_samples); + // Preserve the synchronous contract for direct callers (user-owned + // buffer / legacy audio_set_buffer_callback). One sync per poll instead + // of one per mixer_exec. + rspq_highpri_sync(); +} + +void mixer_try_play(void) +{ + PROFILE_SCOPE(PS_MIXER) { + // To smooth out the pacing for mixer and wav64 decodes, we fill buffers + // to at least audio_get_num_buffers()-1, but fill completely, if there is + // only one free one left. + int free_buffers = audio_get_num_buffers() - audio_get_queued_buffers(); + if (free_buffers <= 0) + goto done; + + int queued = audio_get_queued_buffers(); + int buffers_to_fill = MAX(free_buffers - 1, 1); + while (buffers_to_fill-- > 0) { + // Filling one more buffer now would mean waiting for the RSP to free + // the ring space of an older poll. There is already mixed audio + // queued, so leave the rest for the next frame rather than spending + // it here. With nothing queued the wait is unavoidable: the AI needs + // samples now. + if (queued > 1 && mixer_poll_would_wait()) + break; + short *buf = audio_write_begin(); + mixer_poll_async(buf, audio_get_buffer_length()); + audio_write_end(); + queued++; + } + rspq_flush(); +done: ; + } } diff --git a/src/audio/mixer_internal.h b/src/audio/mixer_internal.h index 80eb201e2..7a72a28bf 100644 --- a/src/audio/mixer_internal.h +++ b/src/audio/mixer_internal.h @@ -6,11 +6,116 @@ #define LIBDRAGON_MIXER_INTERNAL_H #include +#include /** @brief RSPQ overlay ID assigned to the mixer ucode */ extern uint32_t __mixer_overlay_id; -/** Check if a certain waveform is currently playing on some channel */ -void __mixer_wave_stopall(waveform_t *wave); +/** @brief Profile slots for mixer / xm64 / mid64 / vadpcm (see #__mixer_profile_init). + * Base must match #PROFILE_SLOT_AUDIO in profile.h (kept numeric here so this + * header stays includable from host tools that share wav64_internal.h). */ +enum { + PS_MIXER = 0, ///< mixer_try_play + PS_XM_TICK, ///< xm64 tick callback (sequencing + channel sync) + PS_XM_GETPOS, ///< xm64: sync sample_position from mixer + PS_XM_LIBXM, ///< xm64: xm_tick (libxm sequencer) + PS_XM_SYNC, ///< xm64: play/pos/freq/vol sync to mixer + PS_MID_TICK, ///< mid64 tick callback (MIDI + synth process) + PS_MID_PROCESS, ///< mid64: midi_target process (sf64 envelopes) + PS_MID_DISPATCH, ///< mid64: decode + dispatch MIDI events + PS_MIXER_EXEC, ///< mixer_exec + PS_MIXER_PREP, ///< update_loops + refresh_chtbl + round_length + PS_MIXER_EMIT, ///< mixer_emit_round + PS_MIXER_FETCH, ///< emit phase A: freq sync + samplebuffer fetch + PS_SBUF_GET, ///< samplebuffer_get + PS_VADPCM_READ, ///< waveform_vadpcm_read_compressed + PS_VADPCM_HUFF, ///< huffv_decompress + PS_VADPCM_IO, ///< plain VADPCM DMA/read + PS_MIXER_CMD, ///< emit phase B: volumes + rspq MIX_CHANNEL/FLUSH + PS_MIXER_VOL, ///< cmd: mixer_channel_volumes + PS_MIXER_DMAWAIT, ///< cmd: mixer_wait_channel_dma (should be ~0 user time) + PS_MIXER_RSPQ, ///< cmd: rspq MIX_CHANNEL / SETSTATE / FLUSH + PS_MIXER_ADVANCE, ///< mixer_advance + PS_MIXER_SEEK, ///< mixer_ch_seek + PS_MIXER_PREFETCH, ///< mixer_prefetch_next +}; + +/** + * Bytes of VADPCM codebook data per channel: eight predictor vectors + * (128 bytes) followed by the first three samples decoded at the loop start + * (plus one halfword of padding). Must match the WAV64 on-disk layout and the + * mixer ucode DMA size. + */ +#define VADPCM_CODEBOOK_STRIDE 136 + +/** + * @brief Size in bytes of a VADPCM frame with residuals of `bits` bits. + * + * One control byte plus 16 residuals: 9 bytes for the classic 4-bit encoding, + * 7 for 3-bit, 5 for 2-bit. Frames always hold 16 samples. + */ +#define VADPCM_FRAME_BYTES(bits) (2*(bits) + 1) + +/** @brief Codec side-data for #WAVEFORM_FORMAT_VADPCM waveforms. */ +typedef struct { + /** Predictor codebook in RDRAM (must stay valid for the waveform lifetime). */ + void *codebook; + /** + * Decoder state at the loop start (16-byte vector), or NULL if the + * waveform does not loop / loop state is unknown. Used by the RSP on + * cached-loop wraps. + */ + void *loop_state; + /** + * Bits per residual in the bitstream: 2, 3 or 4. Framing, codebook, state + * and predictor order do not change with it, only the payload packing + * (see #VADPCM_GROUP_OFFSET) and thus #VADPCM_FRAME_BYTES. + */ + uint8_t bits; +} waveform_vadpcm_t; + +/** @brief Register mixer profile slots with #profile_init. */ +void __mixer_profile_init(void); + +/** + * @brief Rounds: how the CPU knows what the RSP has already read. + * + * The CPU queues mix rounds without waiting for them, so the input windows it + * hands out stay live until the RSP gets to them. Each round carries an id + * that MIX_FLUSH writes back to RDRAM once the round is over. + * #mixer_poll_async waits for the round that closed the poll + * MIXER_POLL_LOOKAHEAD polls ago before starting a new one, which is what + * keeps samplebuffer refills from overwriting live windows. Ids start at 1, + * so 0 means "no round". + * @{ + */ + +/** @brief True if the RSP has finished the given round. */ +bool __mixer_round_done(uint32_t id); + +/** @brief Wait for the RSP to finish the given round. */ +void __mixer_round_wait(uint32_t id); + +/** @} */ + +/** + * @brief Copy a block of RDRAM onto another one through the mixer ucode. + * + * What the RSP brings here is ordering: the copy runs where it is enqueued, + * which is what mirroring data that an audio codec overlay has been asked to + * write, and may not have written yet, requires (see #waveform_t::rsp_written). + * + * Both addresses and the length must be 8-byte aligned. Returns false if the + * mixer is not running, leaving the copy to the caller. + */ +bool __mixer_rdram_copy(void *dst, void *src, int nbytes); + +/** + * @brief Output samples the CPU may mix without ever waiting for the RSP. + * + * The span sample buffers are sized for. Callers that size their own buffers + * (xm64) have to use the same figure. + */ +int __mixer_inflight_samples(void); #endif diff --git a/src/audio/opus/fixed_generic.h b/src/audio/opus/fixed_generic.h index 8f29d46bb..33c810ae9 100644 --- a/src/audio/opus/fixed_generic.h +++ b/src/audio/opus/fixed_generic.h @@ -72,10 +72,10 @@ #endif /** Compile-time conversion of float constant to 16-bit value */ -#define QCONST16(x,bits) ((opus_val16)(.5+(x)*(((opus_val32)1)<<(bits)))) +#define QCONST16(x,bits) ((opus_val16)(.5f+(x)*(((opus_val32)1)<<(bits)))) /** Compile-time conversion of float constant to 32-bit value */ -#define QCONST32(x,bits) ((opus_val32)(.5+(x)*(((opus_val32)1)<<(bits)))) +#define QCONST32(x,bits) ((opus_val32)(.5f+(x)*(((opus_val32)1)<<(bits)))) /** Negate a 16-bit value */ #define NEG16(x) (-(x)) diff --git a/src/audio/rsp_mixer.S b/src/audio/rsp_mixer.S index fa4bf3eab..d475dda4a 100644 --- a/src/audio/rsp_mixer.S +++ b/src/audio/rsp_mixer.S @@ -4,1216 +4,1641 @@ # # Authors: Giovanni Bajo #################################################################### - - ############################################################## - # - # This ucode implements a mixer, for 32 channels of digital samples, - # with per-channel resampling (mixing channels of different frequencies), - # per-channel 16-bit volume and 16-bit panning control, - # and full 16-bit stereo output with no loss of precision. - # - # The C code that drives this ucode is in mixer.c (mixer_poll). - # The input for the ucode is the channel parameters, that describe - # the playback configuration and where to find the actual samples - # in RDRAM. - # - # The ucode fetches the samples from RDRAM via DMA, resample them - # (that is, operating a frequency change using point sampling), - # apply volume/panning, and mix them together. The final output - # samples are sent to RDRAM via DMA. - # - # Given the limited space in DMEM, this happens in loops where a small - # amount of samples per each channel is processed (MAX_SAMPLES_PER_LOOP). # - # RESAMPLING - # ********** + # Architecture (per-channel commands) + # =================================== # - # Resampling is implemented by the function UpdateAndFetch. As the name - # describes, this functions fetches samples from RDRAM via DMA into a - # temporary buffer in DMEM (DMEM_SAMPLE_CACHE), and then resample them - # into a buffer called CHANNEL_BUFFER, which holds the resampled samples - # for all channels. + # Mixing is driven by three RSPQ commands: # - # Resampling means doing a point sampling on the input sample, - # according to the resampling frequency. The channel configuration - # already contains a fixed-point "step" value for each channel, that - # is used as increment in the loop. For instance, if the step is 2, it - # means that every other sample will be skipped, thus achieving a 0.5x - # resampling (= playing a 88Khz sample on a 44Khz output). + # MIX_SETCHANNEL – record the VADPCM codebook/state pointers of one channel + # into a per-channel DMEM table. + # MIX_CHANNEL – resample one channel for up to MAX_SAMPLES_PER_ROUND + # output ticks and accumulate into a stereo DMEM buffer + # (ACCUM) in .bss. Optionally decode mono VADPCM frames + # into a DMEM window first (CH_FLAGS_VADPCM). Stereo + # VADPCM is two independent mono MIX_CHANNELs (L/R planes). + # MIX_FLUSH – DMA ACCUM out to RDRAM and clear it. # - # Resampling requires thus copying one sample at at time from DMEM_SAMPLE_CACHE - # into CHANNEL_BUFFER, with a fixed-point increment. This is actually - # quite slow and cannot be vectorized; it is by far the slowest part of - # the whole ucode. To achieve reasonable performance, the inner resampling - # loop has been painstakingly optimized by having specific version for 8-bit - # and 16-bit input samples, and with manual loop unrolling to increase - # performance. The final version takes 4,88 cycles/sample for 8-bit channels, - # and 5,88 cycles/samples for 16-bit channels. TODO: handle also stereo - # waveforms spanning 2 channels. This would allow the mixer to support - # interleaved stereo waveforms. + # All MIX_CHANNEL + MIX_FLUSH of a round are enqueued in one highpri burst + # of this overlay so ACCUM (in .bss) cannot be clobbered mid-round. The + # per-channel VADPCM table lives in saved state, so it survives overlay + # swaps; nothing else has to. # - # The DMEM_SAMPLE_CACHE area is a temporary 64-byte buffer that is used to - # hold the original samples fetched via DMA (before resampling). Since the - # ucode doesn't know how many samples will be needed (the exact number - # depends on the resampling step and would require a division to be - # calculated), the DMA always fills this area in full, so we define it - # small-ish to avoid wasting too much time fetching useless samples. On - # the other hand, in the rare cases in which there is a high resampling - # step, multiple DMA transfers might be needed. + # Per-round channel parameters travel inline in the MIX_CHANNEL command; the + # VADPCM pointers, which only change when a new waveform starts, are sent by + # MIX_SETCHANNEL. Sample data comes from + # either resident RDRAM or a CPU-side samplebuffer (no RSP compaction). + # Loop wraps (pos -= loop_len, reload loop_state) are handled here when + # the CPU pins a stable loop region (resident or loop-cached). # + # PCM and VADPCM resampling is 4-tap Catmull-Rom (Hermite) with a + # fixed-point step (WAVEFORM_POS_FRAC_BITS). Stereo PCM is two MIX_CHANNELs + # (owner=L, STEREO_SUB=R) that share the same interleaved cache and loop + # body as mono; only the gather differs. VADPCM frames are DMA'd compressed, + # decoded into DMEM_SAMPLE_CACHE, then Hermite-resampled by that same loop. + # Stereo VADPCM is two independent mono MIX_CHANNELs. # - # MIXER - # ***** + # Volume ramps + # ============ # - # The mixer is actually made of two cores: one that mixes up to 32 channels - # and runs at 16 cycles/sample, and one that mixes up to 8 channels - # and runs at 11 cycles/sample. The 8 channels core is only - # 32% faster because the 32 channels core better exploit vector - # instruction parallelism. - # - # The 8-channel mixer is automatically selected whenever no more than 8 - # channels are configured. TODO: this could be improved by actually looking - # at channel status; a channel might be configured but be currently turned - # off or otherwise silent. - # - # The mixer fetches the samples from CHANNEL_BUFFER, apply volume and - # panning, mix them, and write the output stream in a buffer called - # OUTPUT_AREA. The main loop will then DMA this buffer back to RDRAM. - # - # The mixer cores also implement a one-tap volume filter to - # smooth out sudden volume changes to avoid clicks in the output. - # This is common for instance with XM modules, and in fact most - # XM players implement filters to achieve a similar effect. - # To avoid taxing too much the core, the filter runs at 1/8th - # of the output rate. With the filter turned on, the 32-channel - # core runs at 18 cycles/samples, while the 8-channel core - # runs at 11.5 cycles/samples. The filter can be turned off at compile - # time (VOLUME_FILTER). TODO: make this a runtime option. - # - # Even with the filter overhead, the mixer is still very fast. - # For 32 channel mixing at 44100Hz, it uses only the 1.65% of the - # available frame time, while 8-channel mixing takes 1.10%. In - # general, resampling takes much more time than mixing. Because of this, - # the volume filter is on by default. - # - #################################################################### - # - # Glossary: - # * Sample: a single 8-bit / 16-bit amplitude value - # * Waveform: a sequence of samples that can be played (eg: a WAV file) + # Each MIX_CHANNEL carries the volume the channel starts the round with, + # the one it ends the round on and the increment between the two, per + # block of four output samples. The mixing loop walks the volume one + # output sample at a time and clamps it to the two ends, so a ramp can + # land anywhere inside a round; a channel with no ramp sends an increment + # of zero and the clamp simply holds the volume. None of it costs a cycle: + # it all fits vector slots the loop leaves empty. The CPU recomputes the + # three values from absolute time at every round, so rounding of the + # increment cannot accumulate into drift (see mixer_ch_set_vol_ramp). # #################################################################### #include +#include .set noreorder .set at -# Maximum number of channels supported by this ucode. You can't really increase -# this without modifying the code. -#define MAX_CHANNELS 32 +# Maximum number of channels supported by this ucode. +#define MAX_CHANNELS 32 -# Activate one-tap filter on volume changes, to smooth out sudden changes -# that can cause clicks. -#define VOLUME_FILTER 1 +# Max output samples that fit in ACCUM (stereo int16). Must match mixer.c. +#define MAX_SAMPLES_PER_ROUND 512 -# How many sample to process in a mixing loop. This can be tuned to use more/less -# DMEM. Obviously, more samples per loop is better for performance. -#define MAX_SAMPLES_PER_LOOP 32 +# Extra ACCUM slots the mono resampler is allowed to overshoot into. Sized so +# that ACCUM+padding is a whole number of 64-byte clear blocks. +#define ACCUM_PAD_SAMPLES 16 +#define ACCUM_CLEAR_BLOCKS (((MAX_SAMPLES_PER_ROUND + ACCUM_PAD_SAMPLES) * 4) / 64) # Number of fractional bits used in the fixed point numbers that specify # the waveform position, step, length and loop length. -# NOTE: This must be the same of MIXER_FX32_FRAC in mixer.c. -#define WAVEFORM_POS_FRAC_BITS 12 +# NOTE: This must be the same of MIXER_FX64_FRAC in mixer.c. +#define WAVEFORM_POS_FRAC_BITS 12 -# Waveform flags. Keep these in sync with mixer.c -#define CH_FLAGS_16BIT (1<<2) -#define CH_FLAGS_STEREO (1<<3) +# Assert codes reported by the RSP crash handler (debug builds only). +#define MIXER_ASSERT_VADPCM_AHEAD 0xA001 -#define MAX_CHANNELS_VOFF (MAX_CHANNELS*2) +# Waveform flags. Keep these in sync with mixer.c +#define CH_FLAGS_BPS_SHIFT (3<<0) +#define CH_FLAGS_16BIT (1<<2) +#define CH_FLAGS_STEREO (1<<3) +#define CH_FLAGS_STEREO_SUB (1<<4) +#define CH_FLAGS_VADPCM (1<<5) +# Decode from the state saved for the loop start rather than the one the last +# round left behind (see VadpcmSetup). +#define CH_FLAGS_VLOOP_STATE (1<<6) +# Zero ACCUM before mixing (first MIX_CHANNEL of a round; ACCUM is in .bss). +#define CH_FLAGS_CLEAR_ACCUM (1<<7) + + +# Sample DMA cache. A transfer may never end more than SAMPLE_CACHE_SIZE bytes +# past the end of the RDRAM window (that is #MIXER_LOOP_OVERREAD in mixer.c, +# the only part of the overread that is guaranteed to hold valid samples), but +# it may well be larger than that when the window is far from its end. The +# mono resampler exploits this to fetch SAMPLE_CACHE_SAMPLES samples at a +# time, whatever the PCM format: refilling costs about as much as mixing a +# cacheful, so it is worth spending most of the spare DMEM on the window. +#define SAMPLE_CACHE_SIZE 64 +#define SAMPLE_CACHE_SAMPLES 160 + +# In-mixer VADPCM: one fill decodes as many frames of 16 samples as the +# window holds. It cannot hold more: their compressed form is staged in the +# tail of that same window (see VadpcmDecodeFrames). +#define VADPCM_WIN_FRAMES (SAMPLE_CACHE_SAMPLES / 16) ################################ - # Global register allocations, valid in the whole ucode + # Global register allocations ################################ + # $v00 is zeroed once per command: it is the vs operand of the vor/vmrg + # that build the splats of the mono resampler, and vs cannot be swizzled, + # so no element of another vector can stand in for it. Nothing else in + # this file may write to it, VADPCM decoder included (see v____), with + # one exception: the mono mixing loop borrows it as the lower bound of + # the volume clamp (see v_vlo), and hands it back at every exit. #define v_zero $v00 - - # Current volume left/right for each channel. This is the volume - # that's being ramped up to the final volume. - #define v_xvol_l_0 $v13 - #define v_xvol_r_0 $v14 - #define v_xvol_l_1 $v15 - #define v_xvol_r_1 $v16 - #define v_xvol_l_2 $v17 - #define v_xvol_r_2 $v18 - #define v_xvol_l_3 $v19 - #define v_xvol_r_3 $v20 - - # Final volume left/right (basic volume * panning * global volume), - # pre-multiplied by 1-alpha. - #define v_chvol_l_0 $v21 - #define v_chvol_r_0 $v22 - #define v_chvol_l_1 $v23 - #define v_chvol_r_1 $v24 - #define v_chvol_l_2 $v25 - #define v_chvol_r_2 $v26 - #define v_chvol_l_3 $v27 - #define v_chvol_r_3 $v28 - - # Shift registers - #define v_shift8 $v29 #define v_shift $v30 - - # Misc constants #define v_const1 $v31 #define k_0000 v_zero - #define k_8000 v_shift8.e0 + # k_minusone: integer minus one. vmudh by it negates a vector with the + # same saturation a vsub from zero would have, which is what + # lets the mono loop keep its zero vector free (see v_vlo). + # k_half: 0.5 as a 0.16 unsigned fraction. Multiplying by it with vmudl + # is a logical shift right by one (vsrl cannot be used here: it + # needs the RSPQ shift table in $v31, which holds VCONST_1). + # k_one: integer one. vmudh by it loads a vector into the accumulator + # unshifted, to start a vmadm chain. + # k_eight: integer eight, one group of the mono resampler. + #define k_minusone v_const1.e1 + #define k_half v_const1.e3 + #define k_one v_const1.e4 + #define k_eight v_const1.e5 .data RSPQ_BeginOverlayHeader - RSPQ_DefineCommand command_exec, 16 # 0x0 - RSPQ_DefineCommand VADPCM_Decompress, 16 # 0x1 + RSPQ_DefineCommand MIX_CHANNEL, 36 # 0x0 + RSPQ_DefineCommand MIX_SETCHANNEL, 16 # 0x1 + RSPQ_DefineCommand MIX_FLUSH, 16 # 0x2 + RSPQ_DefineCommand MIX_SETSTATE, 24 # 0x3 + RSPQ_DefineCommand MIX_COPY, 12 # 0x4 RSPQ_EndOverlayHeader ############################################################################ - # Misc constants .align 4 VCONST_1: .half 0x7FFF - .half 0xe076 # (0.9837**8) fixed 0.16 - .half 0x1f8a # 1-(0.9837**8) fixed 0.16 + .half -1 # integer minus one + .half 0 + .half 0x8000 # 0.5 fixed 0.16 + .half 1 # integer one + .half 8 # integer eight + + # Lane -> output sample mapping used by the mono resampler. The eight + # results come out interleaved this way so that the .q0/.q1 swizzles + # feed the LRLRLRLR layout of ACCUM without any further shuffling. + .align 4 +PERM8: + .half 0, 4, 1, 5, 2, 6, 3, 7 - #define k_ffff v_const1.e0 - #define k_alpha v_const1.e1 - #define k_1malpha v_const1.e2 + # Position of each ACCUM slot of a quad within the four samples the + # ramp walks between two of its steps, as a 0.16 fraction. Multiplying + # the step by it is what staggers a volume vector across its lanes. + .align 4 +VSTAGGER: + .half 0, 0, 0x4000, 0x4000, 0x8000, 0x8000, 0xC000, 0xC000 .align 4 BANNER0: .ascii "Dragon RSP Audio" BANNER1: .ascii " Coded by Rasky " + # ================================================================ + # Overlay saved state – survives RSPQ overlay swaps. + # Layout must match mixer_overlay_state_t in mixer.c. + # ================================================================ RSPQ_BeginSavedState - # Current volume state for each channel. This might differ from CHANNEL_VOLUMES - # when the volume filter is turned on: this is the actual current value that - # is being interpolated to CHANNEL_VOLUMES, which is the requested target - # volume to reach. + # Per-channel VADPCM pointers, written by MIX_SETCHANNEL. They only + # change when a new waveform is played, so the CPU sends them once + # instead of inlining them in every MIX_CHANNEL. .align 4 -XVOL_L: .dcb.w MAX_CHANNELS -XVOL_R: .dcb.w MAX_CHANNELS +CHTBL_CODEBOOK: .dcb.l MAX_CHANNELS +CHTBL_STATE: .dcb.l MAX_CHANNELS +CHTBL_LOOP_STATE: .dcb.l MAX_CHANNELS RSPQ_EndSavedState .align 4 IDENTITY: .half 1<<11,1<<11,1<<11,1<<11, 1<<11,1<<11,1<<11,1<<11 - .bss + # Per-lane multipliers that bring each residual of a VADPCM frame to the + # top of its 16-bit lane, one entry of four halfwords per residual width. + # + # A frame is a control byte followed by 16 residuals of `bits` bits each, + # packed MSB first: residual i occupies payload bits [i*bits, (i+1)*bits). + # The decoder reads the payload as four halfwords, one per group of four + # consecutive residuals, and isolates a residual with two multiplies: one + # that shifts it up to bit 15 of its lane (the low 16 bits of the product, + # so everything above it drops out) and one that shifts it back down by + # 16-bits, which sign extends it. + # + # Group j starts at payload byte (j*bits)>>1, at bit d = 4 if j*bits is + # odd and 0 otherwise. Within the group, residual 4j+k is k*bits further + # down. The .q0 swizzle feeds each halfword to a pair of lanes, which take + # the even residual (multiplier K0) and the odd one (K0 << bits), so: + # + # K0 = [2^0, 2^2b, 2^d, 2^(d+2b)] with b = bits + # + # The parity of j*bits has period 2 in j, so lanes 4 to 7 repeat the + # entry: VadpcmDecodeFrames loads it with two ldv. + # + # The rest of an entry is what else depends on the width alone, so that + # VadpcmSetup can load it instead of deriving it: 2^bits, the multiplier + # of the sign extending shift, and the decomposition of the frame stride + # (see CH_VMULSH). The two of those sit as one word, in the order of the + # fields they are copied to. + .align 4 +VADPCM_K0: + .half 1, 16, 1, 16; .half 0, 4; .half 2, 0 # 2 bits (stride 5) + .half 1, 64, 16, 1024; .half 0, 8; .half 3, -1 # 3 bits (stride 7) + .half 1, 256, 1, 256; .half 0, 16; .half 3, 0 # 4 bits (stride 9) -############################################################################ -# UCODE INPUT DATA -############################################################################ + .bss -# Output RDRAM buffer where to store mixed samples (16-bit, stereo) -OUTPUT_RDRAM: .long 0 -# Global volume of playback -GLOBAL_VOLUME: .half 0 -# Number of samples to resample/mix on each channel -NUM_SAMPLES: .half 0 -# Number of configured channels -NUM_CHANNELS: .half 0 - -# Requested volumes for each channel. If VOLUME_FILTER is on, these are the -# values requested by the user, but the current value for each channel might -# be different (as the filter is running). + # Stereo int16 accumulator for one mix round (LRLR...). Lives in .bss + # (not saved state) — kept out of the ACCUM bank + # bank. All MIX_CHANNEL + MIX_FLUSH of a round are enqueued in one + # highpri burst of this overlay, so no overlay switch can clobber it + # mid-round. Cleared by the first MIX_CHANNEL (CH_FLAGS_CLEAR_ACCUM) + # and again by MIX_FLUSH after DMA-out. + # The mono resampler emits whole groups of 8 samples, so it can write up + # to 7 samples past the end of the round. ACCUM_PAD absorbs them; it is + # never DMA'd out, but it is cleared together with ACCUM so that the + # accumulation never saturates on leftovers. + # ACCUM_PRE is the mirror image of that: entering the mono resampler + # costs one pass over the group before the first real one. The pass only + # adds zero, but it does read and write the slots, which on the very + # first group are the ones right before ACCUM. .align 4 -SETTINGS_START: -CHANNEL_VOLUMES_L: .dcb.w MAX_CHANNELS -CHANNEL_VOLUMES_R: .dcb.w MAX_CHANNELS +ACCUM_PRE: .dcb.w (8 * 2) +ACCUM: .dcb.w (MAX_SAMPLES_PER_ROUND * 2) +ACCUM_PAD: .dcb.w (ACCUM_PAD_SAMPLES * 2) -# Array of structures rsp_mixer_channel_s. See mixer.c. 6 words for each -# channel with the following content: -# -# 0: pos: absolute waveform position (as fixed point, WAVEFORM_POS_FRAC_BITS) -# 1: step: resampling increment of position for each output sample (fixed point) -# 2: len: length of the waveform (fixed point) -# 3: loop_len: length of the loop from the end of the waveform (or 0 if no loop) -# 4: ptr: pointer to the beginning of the waveform -# 5: flags: channel flags (see CH_FLAGS_ macros in mixer.c) -# + # Temporary cache of samples fetched by DMA for resampling. + # + # The mono resampler sees it as a window of SAMPLE_CACHE_SAMPLES int16 + # samples: 8-bit waveforms are DMA'd into its upper half and widened in + # place to fill it, so that one inner loop serves both PCM formats. + # VADPCM decodes directly into this window (up to VADPCM_WIN_FRAMES). + # + # The window is followed by four zero samples: the mono resampler parks + # the lanes it must discard there, so they interpolate to silence + # instead of needing a separate mask (MonoLoop). It is 16-byte aligned + # because it is written/read with sqv/lqv. + # + # The tail of the window also stages the compressed frames of a VADPCM + # fill, which are decoded into the window itself (see VadpcmDecodeFrames). + # + # The pipelined VADPCM decoder spills one stale vector 16 bytes below the + # window before its first real store: that lands in the tail of ACCUM_PAD, + # which is never DMA'd out. .align 4 -WAVEFORM_SETTINGS: .dcb.l (6*MAX_CHANNELS) -SETTINGS_END: - - # Temporary cache of samples fetched by DMA. Notice that this must be - # less or equal than MIXER_LOOP_OVERREAD (mixer.c), because the - # RSP will over-read up to this amount of bytes after waveform's end. - .align 3 - #define SAMPLE_CACHE_SIZE 64 -DMEM_SAMPLE_CACHE: .dcb.b SAMPLE_CACHE_SIZE - +DMEM_SAMPLE_CACHE: .dcb.b SAMPLE_CACHE_SAMPLES * 2 +DMEM_SAMPLE_CACHE_ZERO: .dcb.b 16 - # CHANNEL_BUFFER holds the resampled samples for all the channels. - # Samples of different channels are interleaved, so that they can - # be mixed with vector instructions. - .align 4 # for human visual debugging -CHANNEL_BUFFER: .dcb.w (MAX_SAMPLES_PER_LOOP * MAX_CHANNELS) - - # OUTPUT_AREA holds the final mixed stereo samples, that will be copied - # to RDRAM via DMA. - .align 4 # for human visual debugging, 3 would be sufficient (for DMA) -OUTPUT_AREA: .dcb.w MAX_SAMPLES_PER_LOOP*2 - - .text 1 - - # Number of samples that will be processed in the current loop. - #define num_samples k1 - - -command_exec: - #define samples_left t4 - #define outptr s8 - - vxor v_zero, v_zero, v_zero - li t0, %lo(VCONST_1) - lqv v_const1, 0,t0 - - # Extract command parameters - andi a0, 0xFFFF - sh a0, %lo(GLOBAL_VOLUME) - - srl t1, a1, 16 - sh t1, %lo(NUM_SAMPLES) - - andi a1, 0xFFFF - sh a1, %lo(NUM_CHANNELS) - - lw a2, CMD_ADDR(0x8, 0x10) - sw a2, %lo(OUTPUT_RDRAM) - - # Load settings - jal DMASettings - li t2, DMA_IN - - jal SetupMixer - nop - -MainLoop: - lh samples_left, %lo(NUM_SAMPLES) - beqz samples_left, End - - # Fetch output RDRAM pointer and set t1=1 if it's not 8-byte aligned. - # We expect the pointer to be 32-bit aligned (since it's a buffer of 16-bit - # stereo samples), but it might not be 64-bit aligned. - lw t1, %lo(OUTPUT_RDRAM) - andi t1, 7 - slt t1, zero, t1 - - # The maximum number of samples that we want to generate - # in a single loop is MAX_SAMPLES_PER_LOOP. Subtract - # 1 from this number if RDRAM buffer is not aligned so - # that after this loop we're 8-byte aligned again. - li num_samples, MAX_SAMPLES_PER_LOOP - sub num_samples, t1 - - # num_samples = MIN(num_samples, MAX_SAMPLES_PER_LOOP[-1]) - bgt samples_left, num_samples, CheckDMAAlignment - nop - move num_samples, samples_left -CheckDMAAlignment: - # If the output buffer is not aligned, fetch one DMA line (8 bytes) - # so that we preserve the 4 bytes that come before the buffer we were - # given (which would be overwritten by the RSP DMA). - beqz t1, DoLoop - li outptr, %lo(OUTPUT_AREA) - - lw s0, %lo(OUTPUT_RDRAM) - li s4, %lo(OUTPUT_AREA) - jal DMAIn - li t0, DMA_SIZE(8,1) - addi outptr, 4 - -DoLoop: - # Update number of samples left, subtracting the number of samples - # that will be calculated in this loop. - sub samples_left, num_samples - sh samples_left, %lo(NUM_SAMPLES) - - # Fetch the samples and do resampling - jal UpdateAndFetch - lhu k0, %lo(NUM_CHANNELS) - - # Mix the samples - jal Mixer - move s4, outptr - - # Update the output pointer in RDRAM for next loop. - sll t0, num_samples, 2 - lw s0, %lo(OUTPUT_RDRAM) - add s1, s0, t0 - sw s1, %lo(OUTPUT_RDRAM) - - # DMA the output buffer into RDRAM (s0 is the output pointer before update). - # We can do the transfer in background because it will surely be finished - # by the time we start filling the output area again. - addi t0, -1 - jal DMAOutAsync - li s4, %lo(OUTPUT_AREA) - - j MainLoop - nop - -End: - # Store the current channel volume in DMEM (permanent state) - jal EndMixer - nop - - jal DMASettings - li t2, DMA_OUT_ASYNC + # Spill slot of the volume vector of the second half of a group. It lives + # in $v30, which the VADPCM decoder needs as a shift table, so it is + # saved here around a decode (the other one is in a register the decoder + # leaves alone). + .align 4 +DMEM_VMID: .dcb.b 16 - # Wait for the last out transfer to be finished - jal_and_j DMAWaitIdle, RSPQ_Loop + # Lower bound of the volume clamp, splatted the same way. It lives in + # $v00, which the paths outside the mixing loop need back as the zero + # vector, so it is reloaded from here on the way in (see MonoPrime). + .align 4 +DMEM_VLO: .dcb.b 16 - #undef samples_left - #undef outptr + # Per-lane tap addresses produced by the mono resampler, and the + # scratch used to transpose the eight gathered 4-tap groups. + .align 4 +DMEM_ADDRS: .dcb.b 16 + .align 4 +DMEM_TRANSPOSE: .dcb.b 128 + # In-mixer VADPCM staging: 8 predictor vectors (128 bytes) plus the three + # Hermite taps past a loop point (8 bytes with padding). Stereo VADPCM + # uses two MIX_CHANNEL commands, each with its own half. + .align 4 +MIX_VADPCM_CODEBOOK: .space 144 + .align 4 +MIX_VADPCM_STATE: .space 16 + .align 4 +MIX_VADPCM_STATE_BEFORE: .space 16 -############################################################### -# DMASettings - Load/save the settings via DMA. -# -# Arguments: -# t2: DMA_* flag for DMAExec -############################################################### + # Frame index the current fill starts at. + .align 2 +ROUND_SCRATCH: .long 0 - .func DMASettings -DMASettings: - # Save settings - lw s0, CMD_ADDR(0xC, 0x10) - li s4, %lo(SETTINGS_START) - j DMAExec - li t0, DMA_SIZE((SETTINGS_END - SETTINGS_START), 1) - .endfunc + # Staging for the round marker MIX_FLUSH sends back to RDRAM, telling the + # CPU which windows the RSP is done reading (see MIX_FLUSH). A DMA moves + # 8 bytes at a time, so the id is stored twice rather than paired with + # garbage. + .align 3 +ROUND_MARKER: .long 0, 0 + + # Stashed MIX_CHANNEL parameters (command buffer may be overwritten). + .align 2 +CH_POS: .long 0 +CH_STEP: .long 0 +CH_LEN: .long 0 +CH_LOOP_LEN: .long 0 +CH_PTR: .long 0 +CH_CODEBOOK: .long 0 +CH_STATE: .long 0 +CH_LOOP_STATE: .long 0 +CH_FLAGS: .half 0 +CH_IDX: .half 0 +CH_LVOL: .half 0 +CH_RVOL: .half 0 +# Volume this command's round ends on, [left:16][right:16]. Together with +# CH_LVOL/CH_RVOL it gives the two ends of the round, which are the bounds the +# mixing loops clamp to. Equal to CH_LVOL/CH_RVOL when no ramp is running. + .align 2 +CH_TVOL: .long 0 +# Volume the ramp advances by every four output samples, [left:16][right:16]. +# Only the setup needs it: the loop keeps the step as the difference of its two +# volume vectors. + .align 2 +CH_DVOL: .long 0 +# Output samples of the current round. Latched by every MIX_CHANNEL from +# a0's 9-bit nsamples/2 field; shared by every channel of the round. +# Not a scratch: the VADPCM path spills the live tick count elsewhere. +ROUND_NSAMPLES: .half 0 +# Scratch for the live tick count across a VADPCM decode, which clobbers the +# scalar registers that hold it. +CH_TICKS: .half 0 +# First frame currently decoded into DMEM_SAMPLE_CACHE; -1 = none. +CH_VCUR_FRAME: .long 0 +# Frame index that MIX_VADPCM_STATE is ready to decode next. +CH_VNEXT_FRAME: .long 0 +CH_VADPCM_RA: .long 0 +# Frames of the current fill, and frames the next decode call will produce +# (they differ while the decoder is catching up to the start of the fill). +CH_VNFRAMES: .half 0 +CH_VDECN: .half 0 +# k_limit of the current fill, computed before the decode clobbers it, and +# whether that fill stops short of the end of the waveform (in which case the +# limit is the end of the window instead of the end of the waveform). +CH_VLIMIT: .half 0 +CH_VATEND: .half 0 +# Per-sample step of the mono resampler, split the same way as the position: +# fraction as 0.16, plus whole samples. +CH_STEPF: .half 0 +CH_STEPI: .half 0 +# Decomposition of the frame stride (2*bits+1 bytes) that turns a frame index +# into a byte offset without a multiply: +# n*stride = (n << CH_VMULSH) + ((n ^ neg) - neg) +# The two are adjacent and word aligned because they are copied together out +# of VADPCM_K0. CH_VBITS is the residual width itself (2, 3 or 4 bits). + .align 2 +CH_VMULSH: .half 0 +CH_VMULNEG: .half 0 +CH_VBITS: .half 0 +# Frames the waveform covers, ceil(len/16). Every fill measures how many are +# left against it, and it cannot change within a command. + .align 2 +CH_VNFRAMES_TOT: .long 0 + .text 1 ############################################################### -# UpdateAndFetch - Resampling loop. -# -# This function goes through all active channels, fetch -# the samples of current waveforms via DMA, and apply the -# required resampling (frequency change) for a specified -# number of ticks (output samples). -# -# All channels parameters are fetched from WAVEFORM_SETTINGS. +# MIX_CHANNEL – resample one channel and accumulate into ACCUM. # -# Arguments: +# Command words (32 bytes): +# a0: [ovl+cmd:8][ch:5][flags:8][nsamples/2:9] +# a1: [lvol:16][rvol:16] volume at the start of the round +# a2: pos (fixed point, WAVEFORM_POS_FRAC_BITS) +# a3: step +# +16: len +# +20: loop_len +# +24: ptr (0 = nothing to mix) +# +28: [dlvol:16][drvol:16] volume ramp, per block of 4 samples +# +32: [tlvol:16][trvol:16] volume the round ends on (= lvol/rvol if none) # -# k0: number of active channels +# nsamples is the same for every channel of a round and is always even +# (see mixer_round_length), so every MIX_CHANNEL latches nsamples/2 from a0 +# into ROUND_NSAMPLES (9 bits: 0..256 covers MIXER_MAX_SAMPLES_PER_ROUND). +# Accumulator writes always start at ACCUM: the old per-command acc_offset +# was invariably 0. # -# Global state: +# VADPCM codebook/state/loop_state come from the per-channel table filled by +# MIX_SETCHANNEL. # -# num_samples: number of ticks to resample +# Positions for CH_FLAGS_VADPCM are in *samples* (not bytes). ptr points to +# compressed 9-byte frames; frame i starts at ptr + i*9. VADPCM is decoded +# into DMEM_SAMPLE_CACHE and Hermite-resampled like PCM. # +# Stereo PCM owner (CH_FLAGS_STEREO): ptr points to interleaved LR samples; +# L is extracted and mixed with (lvol, rvol). The stereo-sub channel is +# a separate MIX_CHANNEL with CH_FLAGS_STEREO_SUB that extracts R. +# Stereo VADPCM is two mono MIX_CHANNELs (STEREO bit ignored by the ucode). ############################################################### #define ticks t3 - #define waveform_ptr s1 #define wv_pos t4 #define wv_step t5 #define wv_len t6 #define wv_loop_len t7 #define wv_addr t8 - #define wv_dma_addr v0 - #define nchan v1 - #define dma_cache_end s7 - #define out_ptr s5 - #define wv_pos_to_dmem s6 #define wv_step_8x t2 - #define is_stereo a0 - #define is_16bit a1 - - .func UpdateAndFetch -UpdateAndFetch: - move ra2, ra - - li out_ptr, %lo(CHANNEL_BUFFER) - li t0, (MAX_SAMPLES_PER_LOOP * MAX_CHANNELS * 2) / 64 - 1 -ClearLoop: - sqv v_zero, 0x00,out_ptr - sqv v_zero, 0x10,out_ptr - sqv v_zero, 0x20,out_ptr - sqv v_zero, 0x30,out_ptr - addi out_ptr, 64 - bnez t0, ClearLoop - addi t0, -1 - - li waveform_ptr, %lo(WAVEFORM_SETTINGS) - li nchan, 0 - - li dma_cache_end, %lo(DMEM_SAMPLE_CACHE+SAMPLE_CACHE_SIZE) - sll dma_cache_end, WAVEFORM_POS_FRAC_BITS - - # main update loop: will be done once per channel. -UpdateLoop: - # Fetch waveform parameters. Notice that if the RDRAM - # address is zero, no waveform was configured in this channel, - # so we can skip directly to next channel. - lw wv_addr, 16(waveform_ptr) - beqz wv_addr, WaveLoopEpilog2 - lw wv_pos, 0(waveform_ptr) - lw wv_step, 4(waveform_ptr) - lw wv_len, 8(waveform_ptr) - lw wv_loop_len, 12(waveform_ptr) - - # Prepare output pointer - addi out_ptr, nchan, %lo(CHANNEL_BUFFER) - add out_ptr, nchan - move ticks, num_samples - - # Flags: isolate 16-bit flag (bit 2) and stereo flags (bit 3) - lw t0, 20(waveform_ptr) - andi is_stereo, t0, CH_FLAGS_STEREO - andi is_16bit, t0, CH_FLAGS_16BIT -WaveStart: - # Check if we reached end of sample. - bltu wv_pos, wv_len, WaveDmaFetch - nop - - # End of sample. Check if the waveform loops - beqz wv_loop_len, WaveLoopEpilog - nop + #define acc_ptr s8 - # Apply loop to current position and loop again (until wv_pos < wv_len). - # We need to iterate in case during a single UpdateLoop we moved wv_pos - # more than wv_loop_len bytes, so subtracting just once is not sufficient. - j WaveStart - sub wv_pos, wv_loop_len - -WaveDmaFetch: - # Fetch SAMPLE_CACHE_SIZE bytes of the current waveform - # into DMEM_SAMPLE_CACHE, from the current position. - # Notice that DMA is 8-byte aligned, and DMAIn will adjust - # s4 to point to the actual byte in DMEM containing the - # first requested sample. - srl s2, wv_pos, WAVEFORM_POS_FRAC_BITS - add s0, s2, wv_addr - li s4, %lo(DMEM_SAMPLE_CACHE) - jal DMAIn - li t0, DMA_SIZE(SAMPLE_CACHE_SIZE, 1) - - # Calculate an offset that converts wv_pos from a (fixed point) RDRAM - # pointer into a (fixed point) DMEM pointer. This will be used because - # the inner loop will use wv_pos adjusted to be a DMEM pointer - # rather than a RDRAM pointer, for performance. - sub wv_pos_to_dmem, s2, s4 - sll wv_pos_to_dmem, WAVEFORM_POS_FRAC_BITS - - # Compute wv_step_8x - sll wv_step_8x, wv_step, 3 - - ############################################################ - # His Royal Majesty The Resampling Loop. - ############################################################ - # This is where 80% of the RSP time is spent. Any clock - # cycle counts! + # Volumes, as [L,R,L,R,L,R,L,R] to match the ACCUM layout. # - # We need to go through the samples fetched in DMEM and - # resample them into the output buffer (aka: copying them - # going through with specified step). + # The ramp moves once per output sample, so the eight samples a group + # emits are mixed with eight different volumes. Two vectors carry them: + # v_xvol the four samples ACCUM's first quad holds, v_xvol4 the four of + # the second one, four samples further along the ramp. Both are built + # already staggered (lane k of v_xvol is k quarters of a half-group step + # above the volume the round starts at) and both advance by a whole + # group at a time, which is what the loop needs the step for. # - # For performance, this loop assumes that all samples in - # DMEM are valid (that is, they were not fetched more than - # waveform length). To make sure this is always true, - # the C code has prepared all samples with an overread - # buffer at the end (see XM_WAVEFORM_OVERREAD). + # The step itself is not kept anywhere: it is the difference between the + # two vectors, which the loop subtracts back out of them. That is what + # leaves a register for the second vector, and it is also what makes the + # landing come out right, since the difference of two clamped vectors is + # what is left of the step and goes to zero as the ramp lands. # - # The loop is available in four different versions: - # 8-bit and 16-bits, and 1x and 8x (unrolled). The unrolled - # version automatically fallbacks to the 1x version when - # required. - ############################################################ + # The ramp is a slope and nothing else: what stops it on the value the + # CPU asked for is the clamp to [v_vlo, v_vhi], the two ends of the round + # sorted, which is also why a round never has to be cut short at the + # point a ramp lands. With no ramp running the two bounds coincide and + # the clamp simply holds the volume. + # + # $v24/$v25 are outside the range the VADPCM decoder uses, so they + # survive a frame decode untouched. v_xvol4 does not: the decoder needs + # $v30 as a shift table, so it is spilled to DMEM_VMID around a decode. + # v_vlo borrows the zero vector, which every path outside the mixing + # loop wants back, so it is reloaded from DMEM_VLO on the way in. + #define v_xvol $v24 + #define v_xvol4 v_shift + #define v_vhi $v25 + #define v_vlo v_zero + + # Mono resampler state, live across MonoLoop iterations: + # v_frac fraction of the sample position, 0.16 unsigned + # v_idx DMEM index of tap y0, in units of 2 bytes + # v_kmono broadcast scalars of the loop, see k_* below + #define v_frac $v26 + #define v_idx $v27 + #define v_kmono $v29 + # k_limit first tap address that would read past the cache + # k_zaddr address of the window of four zero samples + # k_step8f 8-sample step, fractional part + # k_step8i 8-sample step, whole samples + # k_addrmul bytes per v_idx unit: 2 per sample (mono), 4 per frame (stereo) + # k_subofs byte bias of the plane being mixed: 2 on the right channel of + # an interleaved stereo waveform, whose samples start one + # halfword later, and 0 everywhere else + # (e4 carries the VADPCM sign-extension multiplier, see kext.) + #define k_limit v_kmono.e0 + #define k_zaddr v_kmono.e1 + #define k_step8f v_kmono.e2 + #define k_step8i v_kmono.e3 + #define k_addrmul v_kmono.e5 + #define k_subofs v_kmono.e6 + + # Registers of one MonoLoop iteration. The powers of x are the only + # values that must survive from one iteration to the next (they are + # computed for the group that the next iteration interpolates), so they + # are kept away from $v08-$v23, which the gather and the transpose + # rewrite wholesale. + # + # The taps sit in the transpose destination bank (v16-v23): after the + # gather + transpose, y0..y3 of the plane being mixed are in v16..v19. + # Negated taps live in v20..v23 (the half of the bank the 4-tap gather + # leaves unused). + #define v_x2 $v01 + #define v_x05 $v02 + #define v_x205 $v03 + #define v_x3 $v04 + #define v_x305 $v05 + #define v_res $v06 + #define v_acc0 $v07 + #define v_acc1 $v28 + #define v_addr $v07 /* dead by the time v_acc0 is loaded */ + #define v_tmp $v15 /* vlt result, only VCC is of interest */ + #define v_y0 $v16 + #define v_y1 $v17 + #define v_y2 $v18 + #define v_y3 $v19 + #define v_y0n $v20 + #define v_y1n $v21 + #define v_y2n $v22 + #define v_y3n $v23 + #define v_cnt $v20 + # Half-group step of the ramp, the difference of the two volume vectors. + # The loop rebuilds it every iteration in the register y0 is read out of, + # which the transpose only rewrites at the top of the next one; the paths + # around the loop rebuild it in one of the free gather registers. + #define v_diff $v16 + #define v_diffc $v20 + + #define addr_ptr v0 + #define tr_ptr a0 + #define bps a1 + #define cache_half a2 + #define frac_shift a3 + #define mask v1 /* mask of the group being mixed */ + #define k_ff s5 + #define partial t9 /* < 0 once a group has been rejected */ + # stereo_shift lives in k1 (free on the PCM path; VADPCM uses it as the + # volume-filter countdown). is16 / stereo_sub are reloaded from CH_FLAGS + # at the few sites that need them — they must not alias wv_len / wv_addr. + #define stereo_shift k1 /* 0 = mono, 1 = stereo: v_idx unit is a + sample (2 B) or a frame (4 B) */ + + .func MIX_CHANNEL +MIX_CHANNEL: + # Stash command parameters into DMEM. + # a0: bits[23:19]=ch_idx, bits[18:11]=flags, bits[8:0]=nsamples/2 + srl t0, a0, 19 + andi t0, 0x1F # ch_idx + sh t0, %lo(CH_IDX) + srl t0, a0, 11 + andi t0, 0xFF # flags + sh t0, %lo(CH_FLAGS) + srl t0, a1, 16 # lvol + sh t0, %lo(CH_LVOL) + andi t0, a1, 0xFFFF # rvol + sh t0, %lo(CH_RVOL) + sw a2, %lo(CH_POS) + sw a3, %lo(CH_STEP) + lw t0, CMD_ADDR(0x10, 0x24) + sw t0, %lo(CH_LEN) + lw t0, CMD_ADDR(0x14, 0x24) + sw t0, %lo(CH_LOOP_LEN) + lw t0, CMD_ADDR(0x18, 0x24) + sw t0, %lo(CH_PTR) + lw t0, CMD_ADDR(0x20, 0x24) + sw t0, %lo(CH_TVOL) + lw t0, CMD_ADDR(0x1C, 0x24) + sw t0, %lo(CH_DVOL) + # VADPCM pointers: fetch this channel's entry from the table. + lh t1, %lo(CH_IDX) + sll t1, 2 + lw t0, %lo(CHTBL_CODEBOOK)(t1) + sw t0, %lo(CH_CODEBOOK) + lw t0, %lo(CHTBL_STATE)(t1) + sw t0, %lo(CH_STATE) + lw t0, %lo(CHTBL_LOOP_STATE)(t1) + sw t0, %lo(CH_LOOP_STATE) - # Adjust wv_pos to become a DMEM pointer, and then - # jump to the 8-bit or 16-bit resampling loop. - bnez is_stereo, WaveLoopStereo - sub wv_pos, wv_pos_to_dmem + li t0, %lo(VCONST_1) + lqv v_const1, 0,t0 + vxor v_zero, v_zero, v_zero - ############################################################ - # Mono - ############################################################ + # Latch nsamples from a0. Same for every channel of a round; nsamples/2 + # needs 9 bits because MIXER_MAX_SAMPLES_PER_ROUND is 512. + andi t1, a0, 0x1FF + sll t1, 1 + sh t1, %lo(ROUND_NSAMPLES) + + # Optionally clear ACCUM (first channel of a round). + lh t0, %lo(CH_FLAGS) + andi t0, CH_FLAGS_CLEAR_ACCUM + beqz t0, MIX_CHANNEL_NoClear + li s4, %lo(ACCUM) + li t0, ACCUM_CLEAR_BLOCKS - 1 +1: sqv v_zero, 0x00,s4 + sqv v_zero, 0x10,s4 + sqv v_zero, 0x20,s4 + sqv v_zero, 0x30,s4 + addi s4, 64 + bnez t0, 1b + addi t0, -1 - bnez is_16bit, WaveLoop16_8x +MIX_CHANNEL_NoClear: + # Splat the volume this round starts at, the one it ends on and the + # step between two of its points into [L,R,L,R,L,R,L,R] vectors. + # All three are stored as adjacent halfword pairs in DMEM, so one word + # replicated four times does each of them. + li s0, %lo(RSPQ_SCRATCH_MEM) + lw t1, %lo(CH_LVOL) # CH_RVOL is the next halfword + lw t2, %lo(CH_TVOL) + sw t1, 0x00(s0) + sw t1, 0x04(s0) + sw t1, 0x08(s0) + sw t1, 0x0C(s0) + lqv v_xvol, 0x00,s0 + sw t2, 0x00(s0) + sw t2, 0x04(s0) + sw t2, 0x08(s0) + sw t2, 0x0C(s0) + lqv v_vhi, 0x00,s0 + lw t1, %lo(CH_DVOL) + sw t1, 0x00(s0) + sw t1, 0x04(s0) + sw t1, 0x08(s0) + sw t1, 0x0C(s0) + lqv v_diffc, 0x00,s0 + + lh ticks, %lo(ROUND_NSAMPLES) + beqz ticks, MIX_CHANNEL_Done + + # Sort the two ends of the round into the bounds the mixing loop clamps + # to. v_vlo lands in the zero vector, so it is also stashed: every path + # that leaves the loop wants the zero back and reloads it from there. + # They are the ends of the round as a whole, so they are the same for + # every lane: the stagger below adds a fraction of one step to them, and + # any lane it pushes past the far end is pinned there by the clamp. + vlt v_vlo, v_xvol, v_vhi + vge v_vhi, v_xvol, v_vhi + li s0, %lo(DMEM_VLO) + sqv v_vlo, 0x00,s0 + + # Stagger the two volume vectors: the four samples of a quad are a + # quarter of a step apart, and the second quad is a whole step past the + # first one. + li s0, %lo(VSTAGGER) + lqv $v08, 0x00,s0 + vmudm $v08, v_diffc, $v08 + vadd v_xvol, v_xvol, $v08 + vadd v_xvol4, v_xvol, v_diffc + + li acc_ptr, %lo(ACCUM) + + lw wv_addr, %lo(CH_PTR) + beqz wv_addr, MIX_CHANNEL_Done + + lw wv_pos, %lo(CH_POS) + lw wv_step, %lo(CH_STEP) + lw wv_len, %lo(CH_LEN) + lw wv_loop_len, %lo(CH_LOOP_LEN) + + lh t0, %lo(CH_FLAGS) + andi t1, t0, CH_FLAGS_VADPCM + bnez t1, VadpcmSetup + nop + # PCM mono and stereo share the Hermite path below. ############################################################ - # Mono - 8 bit + # PCM 8/16-bit mono and stereo: 4-tap Catmull-Rom (Hermite) + # + # Eight output samples are produced per iteration, one per vector lane. + # Lane k carries output sample PERM8[k] = [0,4,1,5,2,6,3,7], so that the + # .q0/.q1 swizzles of the interpolated vector line up with the LRLRLRLR + # layout of ACCUM without any further shuffling. + # + # The sample position is split across two vectors: v_frac holds the + # fraction as 0.16 and v_idx the DMEM index of the first tap, in units + # of 2 bytes. Advancing eight samples is then vaddc+vadd, because the + # carry out of the fraction is exactly the carry into the sample index. + # + # With taps y0..y3 and x the fraction, the spline is evaluated in the + # central interval, between y1 and y2: + # + # out = y1 + x*(y2-y0)/2 + # + x^2*(y0 - 5*y1/2 + 2*y2 - y3/2) + # + x^3*(-y0/2 + 3*y1/2 - 3*y2/2 + y3/2) + # + # so the output lags the nominal position by one sample. That keeps the + # tap window at [pos, pos+3] and no sample before pos is ever needed — + # which matters because the sample cache is DMA'd starting exactly at + # pos, and for a streamed waveform the samples before it may no longer + # be in RDRAM. + # + # Stereo PCM is interleaved in the cache as [L,R,L,R,...]. The gather + # loads the four taps of the plane this MIX_CHANNEL owns with lsv + # (owner = L at +0/+4/+8/+12, STEREO_SUB = R at +2/+6/+10/+14), so the + # transpose lands on the same y0..y3 layout the mono path uses. The + # owner/sub volume split is unchanged. + # + # Lanes whose tap window runs past the end of the cache are parked on a + # window of four zero samples: they interpolate to silence and, since + # ACCUM is accumulated in place, leave it untouched. Being always the + # last ones in output order, they are simply counted and replayed after + # the next DMA. + # + # The loop is software pipelined over two stages: each iteration gathers + # the taps of one group while interpolating and mixing the group gathered + # by the previous one. That is what allows the 32 memory accesses of the + # gather to run in the shadow of the 36 vector operations of the spline. + # Consequently the powers of x and the mask of accepted lanes are + # computed one iteration ahead of the group they belong to, and the loop + # has to be entered through MonoPrime, which fills the first stage. ############################################################ -WaveLoop8_8x: - add t0, wv_pos, wv_step_8x # check if pos+step*8 is still in the DMEM cache - bgt t0, dma_cache_end, WaveLoop8Checks - li t0, 8 # check if we need to process at least 8 samples - blt ticks, t0, WaveLoop8Checks - - # Process 8 samples - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS # Get raw DMEM pointer (integer part of wv_pos) - lbu t0, 0(t0) # Fetch the sample - add wv_pos, wv_step # Update wv_pos (load cycle delay) - sb t0, (0*MAX_CHANNELS*2+0)(out_ptr) # Store the sample +MonoSetup: + # A sample (mono) or stereo frame is always stored as int16 in DMEM: + # 8-bit waveforms are widened after each DMA so that a single inner + # loop serves every PCM format. + # bps log2(bytes per sample/frame) in the source waveform + # stereo_shift 0 mono / 1 stereo (halfwords per source unit) + # cache_half DMEM address of the int16 cache, in units of 2 bytes + # frac_shift left shift turning wv_pos into a 0.16 unit fraction + lh t0, %lo(CH_FLAGS) + andi bps, t0, CH_FLAGS_BPS_SHIFT + # A VADPCM command decodes one plane into an int16 cache, so its units are + # mono samples however the STEREO / STEREO_SUB bits (which the owner of a + # stereo pair still carries) read. + andi t1, t0, CH_FLAGS_VADPCM + srl t1, 2 # VADPCM bit -> STEREO bit + sll t2, t1, 1 # -> STEREO_SUB bit + or t1, t2 + nor t1, t1, zero + and t0, t1 + andi stereo_shift, t0, CH_FLAGS_STEREO + srl stereo_shift, 3 # bit 3 → 0 or 1 + li cache_half, %lo(DMEM_SAMPLE_CACHE) + li t1, %lo(DMEM_SAMPLE_CACHE_ZERO) + li frac_shift, 4 + sub frac_shift, bps + + # The window of four zero samples (mono) / four zero frames (stereo) + # the rejected lanes are parked on. It sits right after the cache, so + # it also fails the "tap window inside the cache" test and a single + # comparison covers both cases. 16 bytes is exactly one lqv. + sw zero, 0(t1) + sw zero, 4(t1) + sw zero, 8(t1) + sw zero, 12(t1) + + # Assemble the loop scalars in DMEM and splat them in one load. k_limit + # depends on the size of each transfer, so it is filled in by + # MonoDmaFetch before the loop ever runs. + li s0, %lo(RSPQ_SCRATCH_MEM) + sh t1, 0x02(s0) # k_zaddr + # The resampler turns each lane of v_idx into a byte address with a single + # multiply, and biases it to the plane it is mixing with a single or, so + # that neither depends on a test inside the loop. + li t1, 2 + sllv t1, t1, stereo_shift + sh t1, 0x0A(s0) # k_addrmul + andi t0, CH_FLAGS_STEREO_SUB + srl t0, 3 # bit 4 → 2 bytes + sh t0, 0x0C(s0) # k_subofs + srl cache_half, 1 # byte address -> DMEM index + sll t2, wv_step, 3 + sllv t0, t2, frac_shift + sh t0, 0x04(s0) # k_step8f + srl t0, t2, WAVEFORM_POS_FRAC_BITS + srlv t0, t0, bps # samples / frames (v_idx units) + sh t0, 0x06(s0) # k_step8i + # Same split for one sample/frame, needed to seed the eight lanes. + sllv t0, wv_step, frac_shift + sh t0, %lo(CH_STEPF) + srl t0, wv_step, WAVEFORM_POS_FRAC_BITS + srlv t0, t0, bps + sh t0, %lo(CH_STEPI) + lqv v_kmono, 0x00,s0 + + li addr_ptr, %lo(DMEM_ADDRS) + li tr_ptr, %lo(DMEM_TRANSPOSE) + li k_ff, 0xFF + +MonoWaveStart: + # VADPCM refills by decoding into the sample cache, not by DMA of PCM. + lh t0, %lo(CH_FLAGS) + andi t0, CH_FLAGS_VADPCM + bnez t0, VadpcmWaveStart + nop + bltu wv_pos, wv_len, MonoDmaFetch + nop + beqz wv_loop_len, MIX_CHANNEL_SilenceLeft + nop + j MonoWaveStart + sub wv_pos, wv_loop_len - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS - lbu t0, 0(t0) - add wv_pos, wv_step - sb t0, (1*MAX_CHANNELS*2+0)(out_ptr) +MonoDmaFetch: + # Fetch as many source units as fill the int16 window (SAMPLE_CACHE_SAMPLES + # halfwords: that is SAMPLE_CACHE_SAMPLES samples mono, or half that many + # stereo frames), unless the transfer would end further than + # SAMPLE_CACHE_SIZE bytes past the RDRAM window: beyond that point the + # waveform is not guaranteed to continue. + srl s2, wv_pos, WAVEFORM_POS_FRAC_BITS # byte offset of the position + srl s1, wv_len, WAVEFORM_POS_FRAC_BITS + sub s1, s2 # bytes left in the window, + addi s1, SAMPLE_CACHE_SIZE # plus the guaranteed overread + li t0, SAMPLE_CACHE_SAMPLES + srlv t0, t0, stereo_shift # samples / frames that fit + sllv t0, t0, bps # ...as a transfer size + sltu s3, s1, t0 + beqz s3, 1f + nop + move t0, s1 +1: + # The tap window of the last accepted lane must stay within the units + # that were really fetched. v_idx is in samples (mono) or frames + # (stereo); the byte address is that times 2<> stereo_shift (halfwords vs halfwords/2). + srlv s1, s2, bps # DMA misalignment -> v_idx units + srlv s3, cache_half, stereo_shift + mtc2 t0, $v21.e0 + add s1, s3 + sllv t0, wv_pos, frac_shift # fraction of wv_pos, 0.16 + mtc2 s0, $v22.e0 + mtc2 s1, $v23.e0 + # vmudn reads vs unsigned, so the step fraction must be the one splatted + # into a full vector: it is a 0.16 value and can have bit 15 set. + vor $v18, v_zero, $v21.e0 + mtc2 t0, $v20.e0 + vmudn v_frac, $v18, $v16 # PERM8*step: fraction... + vsar $v17, COP2_ACC_MD # ...and whole samples + vmudh v_idx, $v16, $v22.e0 + # The priming pass below interpolates a group of zero taps, so zero them + # here, in the shadow of the seeding. + vxor $v19, $v19, $v19 + vxor $v18, $v18, $v18 + vaddc v_frac, v_frac, $v20.e0 + vadd v_idx, v_idx, $v17 # + carry out of the fraction + vxor $v17, $v17, $v17 + vxor $v16, $v16, $v16 + vadd v_idx, v_idx, $v23.e0 + +MonoPrime: + # Fill the first stage of the pipeline: gather the first group of the + # new cache without mixing anything. This is the first half of the loop + # body, minus the transpose read-back, and it is lent the group of ACCUM + # slots that the loop advances past at its end. The zero taps make the + # pass it makes over them add nothing. + # + # It also runs the tail of the body, which is where the ramp advances, + # so the group it advances by has to be given back first: a pair one + # group behind is what the tail turns back into the pair it was handed. + # Only the first vector is rewound, the second one is rebuilt from it, + # because the two ends of the ramp are a step apart and that step is all + # the loop has to go on. This is the one place the ramp leaves the range + # it spans, so the rewind can saturate: a round that starts at a volume + # less than a group below the top of the fx15 range (a fade-out from + # full volume) begins a fraction of a group in instead. Rebuilding the + # second vector is what keeps the step exact when that happens; the + # starting volume is off by whatever was clipped, which is a hair of the + # ramp unless it is steep enough to cover the range in a few groups. Only + # the round a ramp starts in can be in that position — every later one + # starts from a volume the ramp has already passed through. + # + # It is also the one way into the loop, so this is where the lower bound + # of the volume clamp gets its register back from the zero vector. + li t0, %lo(DMEM_VLO) + vsub v_diffc, v_xvol4, v_xvol + lqv v_vlo, 0x00,t0 + vsub v_xvol, v_xvol, v_diffc + vsub v_xvol, v_xvol, v_diffc + vadd v_xvol4, v_xvol, v_diffc + vmudn v_addr, v_idx, k_addrmul # v_idx units → byte address + li mask, 0xFF # nothing to give back yet + addi acc_ptr, -8*4 + vor v_addr, v_addr, k_subofs # → the plane being mixed + vlt v_tmp, v_addr, k_limit + vmrg v_addr, v_addr, k_zaddr + j MonoLoopGather + sqv v_addr, 0x00,addr_ptr + + .balign 8 +MonoLoop: + # Mono: ~50 cycles per group of eight output samples, that is 6.25 cycles + # per sample. Stereo: a few more for the lsv gather; a stereo waveform is + # two MIX_CHANNEL commands (owner=L, STEREO_SUB=R). + # + # The byte addresses of the group being gathered were multiplied out of + # v_idx at the bottom of the previous iteration and biased there to the + # plane this command owns (an or, because a stereo frame is four bytes, + # so the halfword bit is always clear). + # + # Read back the taps gathered by the previous iteration, one tap of all + # eight output samples per register, while the DMEM addresses of the + # group gathered by this one are computed. Lanes whose 4-tap window is + # not fully inside the cache are parked on the zero window; VCC records + # the survivors and is left untouched until the bottom of the body, + # where it becomes the mask of the group the next iteration will mix. + # + # The eight ltv leave the SU no room for anything else, so this is where + # the vector operations that depend on neither the taps nor the powers + # of x are parked: the whole volume clamp. The two volume vectors were + # advanced by one group at the bottom of the previous iteration, and + # pinning them to the ends of the ramp is what is left before the vmacf + # reads them. It has to happen here and nowhere else, because a compare + # writes VCC and the vlt below owns it all the way down to the cfc2. + # The clamps also clear VCO, which nothing between here and the vaddc at + # the bottom of the body relies on. + vge v_xvol, v_xvol, v_vlo; ltv $v16.e0, 0x00,tr_ptr + vlt v_xvol, v_xvol, v_vhi; ltv $v16.e7, 0x10,tr_ptr + vge v_xvol4, v_xvol4, v_vlo; ltv $v16.e6, 0x20,tr_ptr + vlt v_xvol4, v_xvol4, v_vhi; ltv $v16.e5, 0x30,tr_ptr + vlt v_tmp, v_addr, k_limit; ltv $v16.e4, 0x40,tr_ptr + vmrg v_addr, v_addr, k_zaddr; ltv $v16.e3, 0x50,tr_ptr + ltv $v16.e2, 0x60,tr_ptr + ltv $v16.e1, 0x70,tr_ptr + add wv_pos, wv_step_8x + addi ticks, -8 + sqv v_addr, 0x00,addr_ptr + +MonoLoopGather: + # Negated taps: the coefficients are fed to the accumulator one unit # Gather the taps of each of + # at a time (vmadm saturates its output but not the accumulator), so # the eight output samples of + # the subtractions of the polynomial are done on the taps instead. # the next group. + # They are multiplies by minus one, and not subtractions from zero, + # because the zero vector is on loan to the volume clamp for the whole + # of the loop (see v_vlo). Both saturate the same way. + # + # Mono uses ldv (8 bytes = 4 contiguous taps). Stereo taps of one plane + # are every other halfword, so four lsv per lane build the same e0..e3 + # layout; vector loads wrap inside a 16-byte DMEM line, and a lone lsv + # of an aligned halfword never wraps. The test between the two rides + # one slot earlier than it reads, so that the last address load fills + # its delay slot: both gathers need that address anyway. + vmudh v_y0n, v_y0, k_minusone; lhu s0, 0x00(addr_ptr) + vmudh v_y1n, v_y1, k_minusone; lhu s1, 0x02(addr_ptr) + vmudh v_y2n, v_y2, k_minusone; lhu s2, 0x04(addr_ptr) + vmudh v_y3n, v_y3, k_minusone; lhu s3, 0x06(addr_ptr) + vmudh v_res, v_y1, k_one; lhu s4, 0x08(addr_ptr) # y1 + vmadm v_res, v_y2, v_x05; lhu s6, 0x0A(addr_ptr) # + x * (y2 - y0)/2 + vmadm v_res, v_y0n, v_x05; lhu s7, 0x0C(addr_ptr) + vmadm v_res, v_y0, v_x2; bnez stereo_shift, MonoGatherStereo + lhu t0, 0x0E(addr_ptr) # + x^2 * (y0 - 5*y1/2 + vmadm v_res, v_y1n, v_x2; ldv $v08, 0x00,s0 # + 2*y2 - y3/2) + vmadm v_res, v_y1n, v_x2; ldv $v09, 0x00,s1 + vmadm v_res, v_y1n, v_x205; ldv $v10, 0x00,s2 + vmadm v_res, v_y2, v_x2; ldv $v11, 0x00,s3 + vmadm v_res, v_y2, v_x2; ldv $v12, 0x00,s4 + vmadm v_res, v_y3n, v_x205; ldv $v13, 0x00,s6 + vmadm v_res, v_y0n, v_x305; ldv $v14, 0x00,s7 # + x^3 * (-y0/2 + 3*y1/2 + vmadm v_res, v_y1, v_x3; ldv $v15, 0x00,t0 # - 3*y2/2 + y3/2) + + # ACCUM is accumulated in place. acc_ptr is only 4-byte aligned once a + # partial group has been replayed, so the two quads are read and + # written with the unaligned lqv/lrv and sqv/srv pairs. The stv can only + # start four cycles after the last gather load anyway. + vmadm v_res, v_y1, v_x305; lqv v_acc0, 0x00,acc_ptr + vmadm v_res, v_y2n, v_x3; lrv v_acc0, 0x10,acc_ptr + vmadm v_res, v_y2n, v_x305; lqv v_acc1, 0x10,acc_ptr + vmadm v_res, v_y3, v_x305; lrv v_acc1, 0x20,acc_ptr + +MonoGatherMix: + + # One test for both exits: the ticks left, forced negative if any lane + # of the group just mixed was rejected. It is computed here, and not at + # the bottom, because a store stalls when it lands exactly two cycles + # after a load: these two are what keeps the stv block clear of the + # vector loads above. + vmudl v_x05, v_frac, k_half; sub partial, mask, k_ff # powers of x of the group being + vmudl v_x2, v_frac, v_frac; or s0, partial, ticks # gathered, for the next iteration + + # .q0/.q1 duplicate each result into the L and R slot of one ACCUM # ...and spill them, so that they + # entry, so the interleaved volume vector applies both gains at once. # can be read back transposed. + # Each quad has its own vector, four samples of ramp apart. + vmudh v_acc0, v_acc0, k_one; stv $v08.e0, 0x00,tr_ptr + vmacf v_acc0, v_xvol, v_res.q0; stv $v08.e1, 0x10,tr_ptr + vmudh v_acc1, v_acc1, k_one; stv $v08.e2, 0x20,tr_ptr + vmacf v_acc1, v_xvol4, v_res.q1; stv $v08.e3, 0x30,tr_ptr + vmudl v_x305, v_x2, v_x05; stv $v08.e4, 0x40,tr_ptr # x^3/2 = x^2 * (x/2) + vmudl v_x3, v_x2, v_frac; stv $v08.e5, 0x50,tr_ptr # x^3 + vaddc v_frac, v_frac, k_step8f; stv $v08.e6, 0x60,tr_ptr # advance the eight positions: + vadd v_idx, v_idx, k_step8i; stv $v08.e7, 0x70,tr_ptr # the carry crosses over + vmudl v_x205, v_x2, k_half; sqv v_acc0, 0x00,acc_ptr # x^2/2 + # The volume of the group the next iteration mixes: one group further + # along the ramp, which is two of the half-group steps the two vectors + # are apart by. Walking them over each other that way is what keeps the + # step out of a register of its own, and it costs nothing here, where + # the SU is busy draining ACCUM. The clamp cannot follow them, because + # the vlt/vmrg pair at the top owns VCC from there down to the cfc2 + # below: it runs at the top of the next iteration instead. + # + # y0 has been read out of v_diff for the last time by now, and the + # transpose only writes it again at the top. + vsub v_diff, v_xvol4, v_xvol; srv v_acc0, 0x10,acc_ptr + vadd v_xvol, v_xvol4, v_diff; sqv v_acc1, 0x10,acc_ptr + # Byte addresses of the group the next iteration gathers. v_addr shares + # its register with v_acc0, which the stores above have just drained. + vmudn v_addr, v_idx, k_addrmul; srv v_acc1, 0x20,acc_ptr + + # VCC still holds the verdict of the vlt at the top: read it only now + # that "partial" has consumed the mask of the group just mixed. It is + # also the one spot where the cfc2, which counts as both a load and a + # store, has neither two cycles before nor after it. + vadd v_xvol4, v_xvol, v_diff; cfc2 mask, COP2_CTRL_VCC + vor v_addr, v_addr, k_subofs; bgtz s0, MonoLoop + addi acc_ptr, 8*4 + bltz partial, MonoPartial + nop + j MonoExit + nop - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS - lbu t0, 0(t0) - add wv_pos, wv_step - sb t0, (2*MAX_CHANNELS*2+0)(out_ptr) + .balign 8 +MonoGatherStereo: + # Build y0..y3 of this plane into e0..e3 of each gather register, matching + # the mono ldv layout. The addresses already point at the plane this + # command owns, since k_subofs biased the whole vector of them. + vmadm v_res, v_y1n, v_x2; lsv $v08.e0, 0x00,s0 + vmadm v_res, v_y1n, v_x2; lsv $v08.e1, 0x04,s0 + vmadm v_res, v_y1n, v_x205; lsv $v08.e2, 0x08,s0 + vmadm v_res, v_y2, v_x2; lsv $v08.e3, 0x0C,s0 + vmadm v_res, v_y2, v_x2; lsv $v09.e0, 0x00,s1 + vmadm v_res, v_y3n, v_x205; lsv $v09.e1, 0x04,s1 + vmadm v_res, v_y0n, v_x305; lsv $v09.e2, 0x08,s1 + vmadm v_res, v_y1, v_x3; lsv $v09.e3, 0x0C,s1 + vmadm v_res, v_y1, v_x305; lsv $v10.e0, 0x00,s2 + vmadm v_res, v_y2n, v_x3; lsv $v10.e1, 0x04,s2 + vmadm v_res, v_y2n, v_x305; lsv $v10.e2, 0x08,s2 + vmadm v_res, v_y3, v_x305; lsv $v10.e3, 0x0C,s2 + lsv $v11.e0, 0x00,s3 + lsv $v11.e1, 0x04,s3 + lsv $v11.e2, 0x08,s3 + lsv $v11.e3, 0x0C,s3 + lsv $v12.e0, 0x00,s4 + lsv $v12.e1, 0x04,s4 + lsv $v12.e2, 0x08,s4 + lsv $v12.e3, 0x0C,s4 + lsv $v13.e0, 0x00,s6 + lsv $v13.e1, 0x04,s6 + lsv $v13.e2, 0x08,s6 + lsv $v13.e3, 0x0C,s6 + lsv $v14.e0, 0x00,s7 + lsv $v14.e1, 0x04,s7 + lsv $v14.e2, 0x08,s7 + lsv $v14.e3, 0x0C,s7 + lsv $v15.e0, 0x00,t0 + lsv $v15.e1, 0x04,t0 + lsv $v15.e2, 0x08,t0 + lsv $v15.e3, 0x0C,t0 + lqv v_acc0, 0x00,acc_ptr + lrv v_acc0, 0x10,acc_ptr + lqv v_acc1, 0x10,acc_ptr + j MonoGatherMix + lrv v_acc1, 0x20,acc_ptr + +MonoPartial: + # Some lanes were parked: they contributed nothing to ACCUM, so give + # their ticks, ACCUM slots and positions back and replay them once the + # cache has been refilled. Their mask is what the loop turned into + # "partial", and VCC has since moved on to the group being gathered. + # + # Part of the ramp is given back with them. The loop moves the volume by + # a whole group per iteration, so a group that only emitted some of its + # samples has moved it further than those samples paid for, and the + # replay is about to move it again for the rest of them. Handing back + # the fraction that belongs to the replayed lanes is what keeps the + # slope the one the CPU computed, wherever the edge of the cache happens + # to cut a group. MonoPrime gives back a whole group on top of that, for + # its own pass over the tail of the body. + # + # The step is the difference between the two volume vectors, so a lane + # the clamp has already landed owes nothing without anything having to + # ask: what it would hand back is zero. It is kept aside so that the + # second vector can be rebuilt from the first one below rather than + # rewound with it, which is what keeps the step exact if the rewind + # saturates (see MonoPrime). The gather registers are free here: the + # group they hold has been spilled and is about to be dropped. + vsub $v09, v_xvol4, v_xvol + # + # The vmrg below wants the zero vector, which the loop has been using as + # the lower bound of the volume clamp: MonoPrime reloads it. + vxor v_zero, v_zero, v_zero + addi t0, partial, 0xFF + ctc2 t0, COP2_CTRL_VCC + vmrg v_cnt, v_zero, k_one # 1 for each rejected lane + vadd v_cnt, v_cnt, v_cnt.q1 + vadd v_cnt, v_cnt, v_cnt.h2 + vadd v_cnt, v_cnt, v_cnt.e4 + mfc2 t0, v_cnt.e0 + # A step covers four samples, so the share of it is count/4: taken as + # count/16 and doubled twice, because a whole group can be rejected and + # count/4 would not fit a 0.16 multiplier. + sll t1, t0, 12 + mtc2 t1, $v08.e0 + add ticks, t0 + sll t1, t0, 2 + sub acc_ptr, t1 + vmudm $v10, $v09, $v08.e0 + vadd $v10, $v10, $v10 + vadd $v10, $v10, $v10 + vsub v_xvol, v_xvol, $v10 + vadd v_xvol4, v_xvol, $v09 + beqz t0, 2f + nop +1: addi t0, -1 + bnez t0, 1b + sub wv_pos, wv_step +2: bgtz ticks, MonoWaveStart + nop + # fall through - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS - lbu t0, 0(t0) - add wv_pos, wv_step - sb t0, (3*MAX_CHANNELS*2+0)(out_ptr) +MonoExit: + # VADPCM must write decoder state back before finishing. + lh t0, %lo(CH_FLAGS) + andi t0, CH_FLAGS_VADPCM + bnez t0, VadpcmEpilog + nop + j MIX_CHANNEL_Done + nop - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS - lbu t0, 0(t0) - add wv_pos, wv_step - sb t0, (4*MAX_CHANNELS*2+0)(out_ptr) +############################################################ +# VADPCM path: decode into DMEM_SAMPLE_CACHE, then Hermite (MonoLoop). +# Positions are in samples (not bytes). STEREO on the wire is ignored — +# stereo VADPCM is two independent mono MIX_CHANNELs. +############################################################ + +VadpcmSetup: + # DMA codebook once per command (predictors + Hermite loop taps). The + # codebook is 8-byte aligned, so MIX_SETCHANNEL packs the residual width + # of the stream into the two low bits of its pointer, as bits-2. + lw s0, %lo(CH_CODEBOOK) + andi t9, s0, 3 + sub s0, t9 + addi t9, 2 # residual width: 2, 3 or 4 + li s4, %lo(MIX_VADPCM_CODEBOOK) + sh t9, %lo(CH_VBITS) + jal DMAInAsync + li t0, DMA_SIZE(136, 1) - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS - lbu t0, 0(t0) - add wv_pos, wv_step - sb t0, (5*MAX_CHANNELS*2+0)(out_ptr) + # DMA decoder state. next_frame = floor(pos/16); the cache is empty. + # + # A round can end exactly on the loop point, and then it is the CPU that + # wraps the position, leaving behind the state of the end of the waveform + # instead of the one of the loop. The CPU marks the command that starts on + # the frame the loop starts on, which decodes from the state saved for it — + # also the state the waveform reaches that frame with, so it holds for a + # channel that is still playing its attack. + lw s0, %lo(CH_STATE) + lh t0, %lo(CH_FLAGS) + andi t0, CH_FLAGS_VLOOP_STATE + beqz t0, 1f + li s4, %lo(MIX_VADPCM_STATE) + lw s0, %lo(CH_LOOP_STATE) +1: jal DMAIn + li t0, DMA_SIZE(16, 1) + + # Residual unpacking constants of this stream. Everything that depends on + # the width alone is in VADPCM_K0: the decoder indexes it for the per-lane + # multipliers, and the rest is copied out of the same entry here. + # + # 2^bits is the multiplier of the sign extending shift. It reaches the + # decoder in v_kmono.e4, through the scratch MonoSetup splats the loop + # scalars from (which only writes lanes 0 to 3). The stride decomposition + # lands in the field pair it is stored as (see CH_VMULSH). + sll t0, t9, 4 + addi t0, %lo(VADPCM_K0 - 32) # entry of this width: (bits-2)*16 + li s1, %lo(RSPQ_SCRATCH_MEM) + lh t1, 0x0A(t0) + lw t2, 0x0C(t0) + sh t1, 0x08(s1) + sw t2, %lo(CH_VMULSH) + + # Frames the waveform covers: hoisted here because every fill needs it and + # it only depends on the length, which the command fixes. + srl t0, wv_len, WAVEFORM_POS_FRAC_BITS + addi t0, 15 + srl t0, 4 + sw t0, %lo(CH_VNFRAMES_TOT) srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS - lbu t0, 0(t0) - add wv_pos, wv_step - sb t0, (6*MAX_CHANNELS*2+0)(out_ptr) - + srl t0, 4 + sw t0, %lo(CH_VNEXT_FRAME) + li t0, -1 + j MonoSetup # bps = 0, stereo_shift = 0 + sw t0, %lo(CH_VCUR_FRAME) + +VadpcmWaveStart: + bltu wv_pos, wv_len, VadpcmFillCache + nop + beqz wv_loop_len, VadpcmEpilog + sub wv_pos, wv_loop_len + # Cached-loop wrap: reload loop-start state and invalidate the window. + lw s0, %lo(CH_LOOP_STATE) + beqz s0, 1f + li s4, %lo(MIX_VADPCM_STATE) + jal DMAIn + li t0, DMA_SIZE(16, 1) +1: srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS + srl t0, 4 + sw t0, %lo(CH_VNEXT_FRAME) + li t0, -1 + j VadpcmWaveStart + sw t0, %lo(CH_VCUR_FRAME) + +VadpcmFillCache: + # Decode min(VADPCM_WIN_FRAMES, frames left) frames of 16 samples into the + # sample cache, then let MonoLoop resample them like 16-bit mono PCM. srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS - lbu t0, 0(t0) - add wv_pos, wv_step - sb t0, (7*MAX_CHANNELS*2+0)(out_ptr) - - # Update output pointer and ticks counter, and loop. - add out_ptr, 8*MAX_CHANNELS*2 - j WaveLoop8_8x - addi ticks, -8 - -WaveLoop8: - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS # Get the raw DMEM pointer - lbu t0, 0(t0) # Fetch the 8-bit sample - add wv_pos, wv_step # Update the pointer using the resampling step - sb t0, 0(out_ptr) # Store to output as 16-bit - addi out_ptr, MAX_CHANNELS*2 # Update output pointer - addi ticks, -1 -WaveLoop8Checks: - blez ticks, WaveBeforeEpilog # Check if we're finished - slt t0, wv_pos, dma_cache_end # Loop if we've not reached the end of DMEM buffer - bnez t0, WaveLoop8 + srl t1, t0, 4 # start_frame + sw t1, %lo(ROUND_SCRATCH) + lw t0, %lo(CH_VNFRAMES_TOT) # frames covered by the waveform + sub t0, t1 + blez t0, VadpcmEpilog + li t2, VADPCM_WIN_FRAMES + # Only lanes with all four taps inside the decoded window are accepted; + # the others are replayed after the next fill. The window that reaches the + # end of the waveform stops one sample past the last one instead: taps + # beyond it are filled in after the decode, and the lanes that would start + # there are given back to VadpcmWaveStart, which wraps the loop (or ends + # the waveform) before they are replayed. + slt t9, t2, t0 # more frames than the window holds + bnez t9, 1f nop - j WaveStart # End of buffer: fetch some more samples - add wv_pos, wv_pos_to_dmem - - ############################################################ - # Mono - 16 bit - ############################################################ + # The taps written past the last sample have to fit in the cache: on the + # rare window that is full and whose last frame is barely padded they do + # not, so keep the window-end limit and let the next fill, which starts + # further on, be the one that reaches the end. + lw s0, %lo(ROUND_SCRATCH) # start_frame + srl s1, wv_len, WAVEFORM_POS_FRAC_BITS + sll s0, 4 + sub s1, s0 # samples up to the end + addi s0, s1, 3 + slti s0, s0, SAMPLE_CACHE_SAMPLES + 1 + beqz s0, 1f + move t2, t0 # nframes = frames left + sll s1, 1 + addi t1, s1, %lo(DMEM_SAMPLE_CACHE) # limit: the end of the waveform + j 2f + sh zero, %lo(CH_VATEND) +1: li t9, 1 + sll t1, t2, 5 # nframes * 16 samples * 2 bytes + addi t1, %lo(DMEM_SAMPLE_CACHE) - 3*2 # limit: three taps before its end + sh t9, %lo(CH_VATEND) +2: sh t2, %lo(CH_VNFRAMES) + sh t1, %lo(CH_VLIMIT) + sh ticks, %lo(CH_TICKS) + sw wv_pos, %lo(CH_POS) + + # Bring MIX_VADPCM_STATE, which decodes CH_VNEXT_FRAME, to start_frame. + # Every fill but the first starts a few frames back, because the taps of + # the last samples of the previous window were rejected and replayed; + # rewinding is free, since the state of a frame is just the eight samples + # before it and those are still in the cache. + # + # It can only ever be a rewind: the resampler stops at the end of the + # decoded window (k_limit), so the frame the next fill starts on is never + # past the one the decoder stopped at. +VadpcmAlignState: + lw t2, %lo(CH_VNEXT_FRAME) + lw t1, %lo(ROUND_SCRATCH) + beq t2, t1, VadpcmAligned + lw t0, %lo(CH_VCUR_FRAME) + assert_lt t1, t2, MIXER_ASSERT_VADPCM_AHEAD + sub t9, t1, t0 # frames of the window to keep + li s0, %lo(MIX_VADPCM_STATE_BEFORE) + beqz t9, 1f + sll t9, 5 + addi t9, %lo(DMEM_SAMPLE_CACHE) + addi s0, t9, -16 +1: li s1, %lo(MIX_VADPCM_STATE) + lqv $v01, 0,s0 + sqv $v01, 0,s1 + j VadpcmAligned + sw t1, %lo(CH_VNEXT_FRAME) + +VadpcmAligned: + # Snapshot the state that decodes start_frame: the epilog needs it if the + # command ends before a single frame has been consumed. + li s0, %lo(MIX_VADPCM_STATE) + li s1, %lo(MIX_VADPCM_STATE_BEFORE) + lqv $v01, 0,s0 + sqv $v01, 0,s1 + lw t1, %lo(ROUND_SCRATCH) + lh t0, %lo(CH_VNFRAMES) + sw t1, %lo(CH_VCUR_FRAME) + sh t0, %lo(CH_VDECN) + # The decoder wants the register the second volume vector lives in. + li t0, %lo(DMEM_VMID) + jal VadpcmDecodeFrames + sqv v_xvol4, 0x00,t0 + + # Hermite reads three taps past the sample it interpolates, and past the + # last sample of the waveform those are the first three of the loop. They + # ride in with the codebook DMA (MIX_VADPCM_CODEBOOK+128); without them a + # loop point would be interpolated towards the padding of the last frame. + # + # A loop state is only recorded for a waveform that loops, so it says the + # taps are meaningful even when the loop is one the CPU wraps by itself + # (which reaches the ucode as loop_len = 0). + lh t9, %lo(CH_VATEND) + bnez t9, VadpcmTapsDone + lh s0, %lo(CH_VLIMIT) # one sample past the last one + lw t3, %lo(CH_LOOP_STATE) + beqz t3, VadpcmTapsHold + li s1, %lo(MIX_VADPCM_CODEBOOK+128) + lh t0, 0x00(s1) + lh t1, 0x02(s1) + lh t2, 0x04(s1) + sh t0, 0x00(s0) + sh t1, 0x02(s0) + j VadpcmTapsDone + sh t2, 0x04(s0) + +VadpcmTapsHold: + # Nothing to interpolate towards: a waveform that does not loop. Hold the + # last sample, which at least does not decay towards silence. + lh t0, -2(s0) + sh t0, 0x00(s0) + sh t0, 0x02(s0) + sh t0, 0x04(s0) +VadpcmTapsDone: + + # The decoder clobbers ticks/pos/len/loop_len/addr, k_ff, s2, $v01-$v23 + # and $v30/$v31 (which it needs as the shift tables of vsra8). + lh ticks, %lo(CH_TICKS) + lw wv_pos, %lo(CH_POS) + lw wv_len, %lo(CH_LEN) + lw wv_loop_len, %lo(CH_LOOP_LEN) + lw wv_addr, %lo(CH_PTR) + li t0, %lo(VCONST_1) + lqv v_const1, 0,t0 + li t0, %lo(DMEM_VMID) + lqv v_xvol4, 0,t0 + lh t1, %lo(CH_VLIMIT) + mtc2 t1, k_limit -WaveLoop16_8x: - add t0, wv_pos, wv_step_8x - bgt t0, dma_cache_end, WaveLoop16Checks - li t0, 8 - blt ticks, t0, WaveLoop16Checks - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lhu t0, 0(t0) - add wv_pos, wv_step - sh t0, (0*MAX_CHANNELS*2)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lhu t0, 0(t0) - add wv_pos, wv_step - sh t0, (1*MAX_CHANNELS*2)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lhu t0, 0(t0) - add wv_pos, wv_step - sh t0, (2*MAX_CHANNELS*2)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lhu t0, 0(t0) - add wv_pos, wv_step - sh t0, (3*MAX_CHANNELS*2)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lhu t0, 0(t0) - add wv_pos, wv_step - sh t0, (4*MAX_CHANNELS*2)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lhu t0, 0(t0) - add wv_pos, wv_step - sh t0, (5*MAX_CHANNELS*2)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lhu t0, 0(t0) - add wv_pos, wv_step - sh t0, (6*MAX_CHANNELS*2)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lhu t0, 0(t0) - add wv_pos, wv_step - sh t0, (7*MAX_CHANNELS*2)(out_ptr) - - add out_ptr, 8*MAX_CHANNELS*2 - j WaveLoop16_8x - addi ticks, -8 - -WaveLoop16: - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lhu t0, 0(t0) - add wv_pos, wv_step - sh t0, 0(out_ptr) - addi out_ptr, MAX_CHANNELS*2 - addi ticks, -1 -WaveLoop16Checks: - blez ticks, WaveBeforeEpilog - slt t0, wv_pos, dma_cache_end - bnez t0, WaveLoop16 + srl s2, wv_pos, WAVEFORM_POS_FRAC_BITS + andi s2, 15 # offset of wv_pos in the cache + sll wv_step_8x, wv_step, 3 + j MonoRefill + li k_ff, 0xFF + +VadpcmEpilog: + # The resampler mixes in whole groups of eight and can run up to seven + # samples past the end of the round (ACCUM_PAD absorbs them). The next + # command resumes from the position the CPU keeps, which is the one the + # round really ended on: rewind to it, or the state saved below would be + # the one of a later frame and the decode would restart from the wrong + # samples. + bgez ticks, 3f nop - j WaveStart - add wv_pos, wv_pos_to_dmem - - - ############################################################ - # Stereo - ############################################################ - -WaveLoopStereo: - bnez is_16bit, WaveLoop16_Stereo_8x +1: addi ticks, 1 + bltz ticks, 1b + sub wv_pos, wv_step + +3: # The state that decodes frame F is just the eight samples before it, so + # once the cache holds F it can be written back straight out of it. Only + # the two ends need the snapshots: nothing decoded yet (STATE, as loaded) + # or nothing consumed yet (STATE_BEFORE, taken at CH_VCUR_FRAME). + srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS + srl t0, 4 # end_frame + lw t1, %lo(CH_VCUR_FRAME) + li s4, %lo(MIX_VADPCM_STATE) + bltz t1, 1f + sub t0, t1 # frames consumed + blez t0, 2f + lw t2, %lo(CH_VNEXT_FRAME) + sub t2, t1 # frames decoded + slt t9, t2, t0 + bnez t9, 1f + sll t0, 5 + addi t0, %lo(DMEM_SAMPLE_CACHE) + j 1f + addi s4, t0, -16 # eight samples before end_frame +2: li s4, %lo(MIX_VADPCM_STATE_BEFORE) +1: lw s0, %lo(CH_STATE) + jal DMAOut + li t0, DMA_SIZE(16, 1) - ############################################################ - # Stereo - 8 bit - ############################################################ +MIX_CHANNEL_SilenceLeft: + # A channel that runs out of data mixes nothing for the rest of the + # round, and there is no state left to carry across it: the volume of + # every round is computed from scratch by the CPU. -WaveLoop8_Stereo_8x: - add t0, wv_pos, wv_step_8x - bgt t0, dma_cache_end, WaveLoop8StereoChecks - li t0, 8 - blt ticks, t0, WaveLoop8StereoChecks - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lbu t1, 0(t0) - lbu t0, 1(t0) - add wv_pos, wv_step - sb t1, (0*MAX_CHANNELS*2+0)(out_ptr) - sb t0, (0*MAX_CHANNELS*2+2)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lbu t1, 0(t0) - lbu t0, 1(t0) - add wv_pos, wv_step - sb t1, (1*MAX_CHANNELS*2+0)(out_ptr) - sb t0, (1*MAX_CHANNELS*2+2)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lbu t1, 0(t0) - lbu t0, 1(t0) - add wv_pos, wv_step - sb t1, (2*MAX_CHANNELS*2+0)(out_ptr) - sb t0, (2*MAX_CHANNELS*2+2)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lbu t1, 0(t0) - lbu t0, 1(t0) - add wv_pos, wv_step - sb t1, (3*MAX_CHANNELS*2+0)(out_ptr) - sb t0, (3*MAX_CHANNELS*2+2)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lbu t1, 0(t0) - lbu t0, 1(t0) - add wv_pos, wv_step - sb t1, (4*MAX_CHANNELS*2+0)(out_ptr) - sb t0, (4*MAX_CHANNELS*2+2)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lbu t1, 0(t0) - lbu t0, 1(t0) - add wv_pos, wv_step - sb t1, (5*MAX_CHANNELS*2+0)(out_ptr) - sb t0, (5*MAX_CHANNELS*2+2)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lbu t1, 0(t0) - lbu t0, 1(t0) - add wv_pos, wv_step - sb t1, (6*MAX_CHANNELS*2+0)(out_ptr) - sb t0, (6*MAX_CHANNELS*2+2)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lbu t1, 0(t0) - lbu t0, 1(t0) - add wv_pos, wv_step - sb t1, (7*MAX_CHANNELS*2+0)(out_ptr) - sb t0, (7*MAX_CHANNELS*2+2)(out_ptr) - - add out_ptr, 8*MAX_CHANNELS*2 - j WaveLoop8_Stereo_8x - addi ticks, -8 - -WaveLoop8Stereo: - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+1 - sll t0, 1 - lbu t1, 0(t0) - lbu t0, 1(t0) - add wv_pos, wv_step - sb t1, 0(out_ptr) - sb t0, 2(out_ptr) - - addi out_ptr, MAX_CHANNELS*2 - addi ticks, -1 -WaveLoop8StereoChecks: - blez ticks, WaveBeforeEpilog - slt t0, wv_pos, dma_cache_end - bnez t0, WaveLoop8Stereo +MIX_CHANNEL_Done: + # Hand $v00 back to the next command as the zero vector: the mixing + # loop borrows it for the lower bound of the volume clamp, and not + # every path out of it goes through a reload. + vxor v_zero, v_zero, v_zero + j RSPQ_Loop nop - j WaveStart - add wv_pos, wv_pos_to_dmem - - - ############################################################ - # Stereo - 16 bit - ############################################################ - -WaveLoop16_Stereo_8x: - add t0, wv_pos, wv_step_8x - bgt t0, dma_cache_end, WaveLoop16StereoChecks - li t0, 8 - blt ticks, t0, WaveLoop16StereoChecks - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+2 - sll t0, 2 - lw t0, 0(t0) - add wv_pos, wv_step - sw t0, (0*MAX_CHANNELS*2+0)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+2 - sll t0, 2 - lw t0, 0(t0) - add wv_pos, wv_step - sw t0, (1*MAX_CHANNELS*2+0)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+2 - sll t0, 2 - lw t0, 0(t0) - add wv_pos, wv_step - sw t0, (2*MAX_CHANNELS*2+0)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+2 - sll t0, 2 - lw t0, 0(t0) - add wv_pos, wv_step - sw t0, (3*MAX_CHANNELS*2+0)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+2 - sll t0, 2 - lw t0, 0(t0) - add wv_pos, wv_step - sw t0, (4*MAX_CHANNELS*2+0)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+2 - sll t0, 2 - lw t0, 0(t0) - add wv_pos, wv_step - sw t0, (5*MAX_CHANNELS*2+0)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+2 - sll t0, 2 - lw t0, 0(t0) - add wv_pos, wv_step - sw t0, (6*MAX_CHANNELS*2+0)(out_ptr) - - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+2 - sll t0, 2 - lw t0, 0(t0) - add wv_pos, wv_step - sw t0, (7*MAX_CHANNELS*2+0)(out_ptr) + .endfunc - add out_ptr, 8*MAX_CHANNELS*2 - j WaveLoop16_Stereo_8x - addi ticks, -8 -WaveLoop16Stereo: - srl t0, wv_pos, WAVEFORM_POS_FRAC_BITS+2 - sll t0, 2 - lw t0, 0(t0) - add wv_pos, wv_step - sw t0, 0(out_ptr) - - addi out_ptr, MAX_CHANNELS*2 - addi ticks, -1 -WaveLoop16StereoChecks: - blez ticks, WaveBeforeEpilog - slt t0, wv_pos, dma_cache_end - bnez t0, WaveLoop16Stereo - nop - j WaveStart - add wv_pos, wv_pos_to_dmem +############################################################### +# MIX_FLUSH – DMA ACCUM to RDRAM, clear ACCUM, mark the round as done. +# +# a0: [nsamples:16][pad:16] +# a1: output RDRAM pointer (physical, 8-byte aligned) +# a2: round id +# a3: RDRAM pointer where the id of the last round done is published +# +# The DMA engine ignores the low 3 bits of both addresses, so ACCUM can only +# be sent as-is to an 8-byte aligned pointer. The CPU guarantees that: the +# output buffer is aligned and every round mixes an even number of stereo +# samples (see mixer_round_length). +# +# The marker is what lets the CPU know how far behind the RSP is. Every +# MIX_CHANNEL of the round has read its input window by the time we get here, +# and transfers run in order, so a CPU that sees the id knows those windows +# are free and can refill them (see samplebuffer_append). +############################################################### -WaveBeforeEpilog: - add wv_pos, wv_pos_to_dmem + .func MIX_FLUSH +MIX_FLUSH: + andi t4, a0, 0xFFFF # nsamples + beqz t4, MIX_FLUSH_Mark + move s0, a1 # output rdram -WaveLoopEpilog: - beqz is_stereo, WaveLoopEpilog2 - sw wv_pos, 0(waveform_ptr) # store updated wv_pos in DMEM + sll t0, t4, 2 + addi t0, -1 + jal DMAOut + li s4, %lo(ACCUM) - # For stereo, we need to skip 1 channel - addi nchan, 1 - addi waveform_ptr, 6*4 + li s4, %lo(ACCUM) + li t0, ACCUM_CLEAR_BLOCKS - 1 + vxor v_zero, v_zero, v_zero +1: sqv v_zero, 0x00,s4 + sqv v_zero, 0x10,s4 + sqv v_zero, 0x20,s4 + sqv v_zero, 0x30,s4 + addi s4, 64 + bnez t0, 1b + addi t0, -1 -WaveLoopEpilog2: - addi nchan, 1 - bne nchan, k0, UpdateLoop - addi waveform_ptr, 6*4 +MIX_FLUSH_Mark: + sw a2, %lo(ROUND_MARKER) + 0 + sw a2, %lo(ROUND_MARKER) + 4 + move s0, a3 + li s4, %lo(ROUND_MARKER) + jal DMAOut + li t0, DMA_SIZE(8, 1) - jr ra2 + j RSPQ_Loop nop .endfunc +############################################################### +# MIX_SETCHANNEL – record the VADPCM pointers of one channel. +# +# Command words (16 bytes): +# a0: [ovl+cmd:8][ch_idx:8][pad:16] +# a1: codebook +# a2: state (VADPCM decoder state in RDRAM) +# a3: loop_state (VADPCM state at loop start; 0 if none) +############################################################### -############################################################## -# SetupMixer: load left/right volumes for each channel -# into the global registers (v_chvol_l/r), so that they're -# available for mixing. -############################################################## - - #define v_glvol $v01 - - .func SetupMixer -SetupMixer: - # Load global volume (into all lanes) - lh t0, %lo(GLOBAL_VOLUME) - mtc2 t0, v_glvol.e0 - vor v_glvol, v_zero, v_glvol.e0 - - li s0, %lo(CHANNEL_VOLUMES_L) - li s1, %lo(XVOL_L) - - # Load channel volumes (left / right) - lqv v_chvol_l_0, 0*MAX_CHANNELS_VOFF+0x00,s0 - lqv v_chvol_l_1, 0*MAX_CHANNELS_VOFF+0x10,s0 - lqv v_chvol_l_2, 0*MAX_CHANNELS_VOFF+0x20,s0 - lqv v_chvol_l_3, 0*MAX_CHANNELS_VOFF+0x30,s0 - - lqv v_chvol_r_0, 1*MAX_CHANNELS_VOFF+0x00,s0 - lqv v_chvol_r_1, 1*MAX_CHANNELS_VOFF+0x10,s0 - lqv v_chvol_r_2, 1*MAX_CHANNELS_VOFF+0x20,s0 - lqv v_chvol_r_3, 1*MAX_CHANNELS_VOFF+0x30,s0 - - # Apply global volume to obtain the final volume for each channel - vmudl v_chvol_l_0, v_chvol_l_0, v_glvol - vmudl v_chvol_r_0, v_chvol_r_0, v_glvol - vmudl v_chvol_l_1, v_chvol_l_1, v_glvol - vmudl v_chvol_r_1, v_chvol_r_1, v_glvol - vmudl v_chvol_l_2, v_chvol_l_2, v_glvol - vmudl v_chvol_r_2, v_chvol_r_2, v_glvol - vmudl v_chvol_l_3, v_chvol_l_3, v_glvol - vmudl v_chvol_r_3, v_chvol_r_3, v_glvol - -#if VOLUME_FILTER - # Load actual volumes levels - lqv v_xvol_l_0, 0*MAX_CHANNELS_VOFF+0x00,s1 - lqv v_xvol_l_1, 0*MAX_CHANNELS_VOFF+0x10,s1 - lqv v_xvol_l_2, 0*MAX_CHANNELS_VOFF+0x20,s1 - lqv v_xvol_l_3, 0*MAX_CHANNELS_VOFF+0x30,s1 - - lqv v_xvol_r_0, 1*MAX_CHANNELS_VOFF+0x00,s1 - lqv v_xvol_r_1, 1*MAX_CHANNELS_VOFF+0x10,s1 - lqv v_xvol_r_2, 1*MAX_CHANNELS_VOFF+0x20,s1 - lqv v_xvol_r_3, 1*MAX_CHANNELS_VOFF+0x30,s1 -#else - vor v_xvol_l_0, v_chvol_l_0, v_zero - vor v_xvol_l_1, v_chvol_l_1, v_zero - vor v_xvol_l_2, v_chvol_l_2, v_zero - vor v_xvol_l_3, v_chvol_l_3, v_zero - - vor v_xvol_r_0, v_chvol_r_0, v_zero - vor v_xvol_r_1, v_chvol_r_1, v_zero - vor v_xvol_r_2, v_chvol_r_2, v_zero - vor v_xvol_r_3, v_chvol_r_3, v_zero -#endif - - jr ra - nop + .func MIX_SETCHANNEL +MIX_SETCHANNEL: + srl t0, a0, 16 + andi t0, 0xFF + sll t0, 2 + sw a1, %lo(CHTBL_CODEBOOK)(t0) + sw a2, %lo(CHTBL_STATE)(t0) + j RSPQ_Loop + sw a3, %lo(CHTBL_LOOP_STATE)(t0) .endfunc - #undef v_glvol - - - .func EndMixer -EndMixer: - li s1, %lo(XVOL_L) - - sqv v_xvol_l_0, 0*MAX_CHANNELS_VOFF+0x00,s1 - sqv v_xvol_l_1, 0*MAX_CHANNELS_VOFF+0x10,s1 - sqv v_xvol_l_2, 0*MAX_CHANNELS_VOFF+0x20,s1 - sqv v_xvol_l_3, 0*MAX_CHANNELS_VOFF+0x30,s1 - sqv v_xvol_r_0, 1*MAX_CHANNELS_VOFF+0x00,s1 - sqv v_xvol_r_1, 1*MAX_CHANNELS_VOFF+0x10,s1 - sqv v_xvol_r_2, 1*MAX_CHANNELS_VOFF+0x20,s1 - sqv v_xvol_r_3, 1*MAX_CHANNELS_VOFF+0x30,s1 - - jr ra - nop +############################################################### +# MIX_SETSTATE – seed the VADPCM decoder state of one channel. +# +# Command words (24 bytes): +# a0: [ovl+cmd:8][pad:24] +# a1: state (the buffer MIX_SETCHANNEL registered for the channel) +# a2: state word 0 +# a3: state word 1 +# +16: state word 2 +# +20: state word 3 +# +# The CPU re-seeds the decoder whenever it moves a channel somewhere the +# stream does not reach on its own: a seek, or the wrap of a loop too large to +# be pinned in the samplebuffer. Writing that state to RDRAM directly would +# race with the epilog of every MIX_CHANNEL, which saves its own state to the +# same buffer whenever the RSP gets to it — several rounds behind the CPU, and +# at a different moment for each plane of a stereo pair. Carrying it through +# the queue instead puts it after every round already enqueued and before the +# one that has to decode from it. +############################################################### + .func MIX_SETSTATE +MIX_SETSTATE: + addi t0, rspq_dmem_buf_ptr, %lo(RSPQ_DMEM_BUFFER) - 0x10 + li s4, %lo(RSPQ_SCRATCH_MEM) + lqv $v01, 0x00,t0 + lrv $v01, 0x10,t0 + move s0, a1 + li t0, DMA_SIZE(16, 1) + j DMAOut + sqv $v01, 0x00,s4 .endfunc -############################################################## -# Mixer +############################################################### +# MIX_COPY – copy a block of RDRAM onto another one. # -# Arguments: -# s4: buffer into which the mixed samples will be stored +# Command words (12 bytes): +# a0: [ovl+cmd:8][pad:8][nbytes:16] +# a1: destination (physical, 8-byte aligned) +# a2: source (physical, 8-byte aligned) # -# Global state: -# num_samples: number of samples to mix +# The samplebuffers use it to refresh the copy of their first samples that +# sits past the end of the ring (see samplebuffer_commit), when those samples +# are written by the RSP itself (any codec that decodes with an overlay): the +# copy has to travel through the queue to land after them. # -############################################################## - - #define v_out_l $v01 - #define v_out_r $v02 - #define v_sample_0 $v03 - #define v_sample_1 $v04 - #define v_sample_2 $v05 - #define v_sample_3 $v06 - #define v_mix_l $v07 - #define v_mix_r $v08 - - .func Mixer -Mixer: - # Load samples - li s0, %lo(CHANNEL_BUFFER) - move t1, num_samples - - # Load initial samples - lqv v_sample_0, 0x00,s0 - lqv v_sample_1, 0x10,s0 - lqv v_sample_2, 0x20,s0 - lqv v_sample_3, 0x30,s0 - - # For optimal pipelining, output is stored at the beginning of the loop. To avoid - # corrupting memory, load the output register with whatever is there now. - lsv v_out_l.e0, -4,s4 - ble k0, 8, Mix8Start # Optimized mixing loop for <= 8 channels - lsv v_out_r.e0, -2,s4 - -Mix32Start: - blt t1, 8, Mix32Loop - move t0, t1 - li t0, 8 - - ############################################################################ - # VU SU # - ############################################################################ - .align 3 -Mix32Loop: - # Apply volume/panning to each channel sample. - # left channel: - vmulf v_mix_l, v_sample_0, v_xvol_l_0; - vmacf v_mix_l, v_sample_1, v_xvol_l_1; # Store previous loop's output - vmacf v_mix_l, v_sample_2, v_xvol_l_2; ssv v_out_l.e0, -4,s4 - vmacf v_mix_l, v_sample_3, v_xvol_l_3; ssv v_out_r.e0, -2,s4 - # right channel: # Updated counters - vmulf v_mix_r, v_sample_0, v_xvol_r_0; add s0, 32*2 - vmacf v_mix_r, v_sample_1, v_xvol_r_1; addi t0, -1 - vmacf v_mix_r, v_sample_2, v_xvol_r_2; addi s4, 4 - vmacf v_mix_r, v_sample_3, v_xvol_r_3; - - # Mix all lanes together into the first lane # Load next loop's samples - vaddc v_out_l, v_mix_l, v_mix_l.q1; lqv v_sample_0.e0, 0x00,s0 - vaddc v_out_r, v_mix_r, v_mix_r.q1; lqv v_sample_1.e0, 0x10,s0 - # 1 cycle stall here - vaddc v_out_l, v_out_l, v_out_l.h2; lqv v_sample_2.e0, 0x20,s0 - vaddc v_out_r, v_out_r, v_out_r.h2; lqv v_sample_3.e0, 0x30,s0 - # 1 cycle stall here - vaddc v_out_l, v_out_l, v_out_l.e4; bnez t0, Mix32Loop - vaddc v_out_r, v_out_r, v_out_r.e4; - -#if VOLUME_FILTER - # Apply volume ramp - vmudm v_xvol_l_0, v_xvol_l_0, k_alpha - vmadm v_xvol_l_0, v_chvol_l_0, k_1malpha - - vmudm v_xvol_l_1, v_xvol_l_1, k_alpha - vmadm v_xvol_l_1, v_chvol_l_1, k_1malpha - - vmudm v_xvol_l_2, v_xvol_l_2, k_alpha - vmadm v_xvol_l_2, v_chvol_l_2, k_1malpha - - vmudm v_xvol_l_3, v_xvol_l_3, k_alpha - vmadm v_xvol_l_3, v_chvol_l_3, k_1malpha - - vmudm v_xvol_r_0, v_xvol_r_0, k_alpha - vmadm v_xvol_r_0, v_chvol_r_0, k_1malpha - - vmudm v_xvol_r_1, v_xvol_r_1, k_alpha - vmadm v_xvol_r_1, v_chvol_r_1, k_1malpha - - vmudm v_xvol_r_2, v_xvol_r_2, k_alpha # Next iteration - vmadm v_xvol_r_2, v_chvol_r_2, k_1malpha; addi t1, -8 - - vmudm v_xvol_r_3, v_xvol_r_3, k_alpha; bgtz t1, Mix32Start - vmadm v_xvol_r_3, v_chvol_r_3, k_1malpha -#else - addi t1, -8 - bgtz t1, Mix32Start - nop -#endif - - # Store last loop's output and exit - ssv v_out_l.e0, -4,s4 - jr ra - ssv v_out_r.e0, -2,s4 +# nbytes may not exceed the sample cache this stages the copy through, which +# is scratch to every other command. +############################################################### + .func MIX_COPY +MIX_COPY: + andi t0, a0, 0xFFFF + move s0, a2 + addi t0, -1 + jal DMAIn + li s4, %lo(DMEM_SAMPLE_CACHE) -Mix8Start: - blt t1, 8, Mix8Loop - move t0, t1 - li t0, 8 + andi t0, a0, 0xFFFF + move s0, a1 + addi t0, -1 + jal DMAOut + li s4, %lo(DMEM_SAMPLE_CACHE) - ############################################################################ - # VU SU # - ############################################################################ - .align 3 -Mix8Loop: - vmulf v_mix_l, v_sample_0, v_xvol_l_0; ssv v_out_l.e0, -4,s4 - vmulf v_mix_r, v_sample_0, v_xvol_r_0; ssv v_out_r.e0, -2,s4 - # pipeline stall - vaddc v_out_l, v_mix_l, v_mix_l.q1; addi t0, -1 - vaddc v_out_r, v_mix_r, v_mix_r.q1; add s0, 32*2 - # pipeline stall - vaddc v_out_l, v_out_l, v_out_l.h2; addi s4, 4 - vaddc v_out_r, v_out_r, v_out_r.h2; lqv v_sample_0, 0,s0 - # pipeline stall - vaddc v_out_l, v_out_l, v_out_l.e4; bnez t0, Mix8Loop - vaddc v_out_r, v_out_r, v_out_r.e4; - -#if VOLUME_FILTER - # Apply volume ramp - vmudm v_xvol_l_0, v_xvol_l_0, k_alpha - vmadm v_xvol_l_0, v_chvol_l_0, k_1malpha; addi t1, -8 - - vmudm v_xvol_r_0, v_xvol_r_0, k_alpha; bgtz t1, Mix8Start - vmadm v_xvol_r_0, v_chvol_r_0, k_1malpha -#else - addi t1, -8 - bgtz t1, Mix8Start - nop -#endif - - ssv v_out_l.e0, -4,s4 - jr ra - ssv v_out_r.e0, -2,s4 + j RSPQ_Loop + nop .endfunc + #undef ticks + #undef wv_pos + #undef wv_step + #undef wv_len + #undef wv_loop_len + #undef wv_addr + #undef wv_step_8x + #undef acc_ptr + #undef stereo_shift - #undef v_zero - #undef v_xvol_l_0 - #undef v_xvol_r_0 - #undef v_xvol_l_1 - #undef v_xvol_r_1 - #undef v_xvol_l_2 - #undef v_xvol_r_2 - #undef v_xvol_l_3 - #undef v_xvol_r_3 - #undef v_chvol_l_0 - #undef v_chvol_r_0 - #undef v_chvol_l_1 - #undef v_chvol_r_1 - #undef v_chvol_l_2 - #undef v_chvol_r_2 - #undef v_chvol_l_3 - #undef v_chvol_r_3 - #undef v_shift8 - #undef v_shift - #undef v_const1 - #undef k_0000 - #undef k_8000 ############################################################################ -# VADPCM decompressor +# Mono VADPCM decode (MIX_CHANNEL / VadpcmDecodeFrame) ############################################################################ - ######################################## - # VADPCM_Decompress - # - # Args: - # a0: pointer to input buffer - # a1: pointer to output buffer (+ numframes-1 in MSB) - # a2: pointer to state buffer (+ bit 31 set if stereo) - # a3: pointer to codebook - # - ######################################## - -#define VADPCM_PROFILING 0 - #define VADPCM_ORDER_SHIFT 1 #define VADPCM_ORDER (1<>1 of the payload, that is at 0, + # bits>>1, bits and bits + (bits>>1). li s0, %lo(IDENTITY) - ltv pred1a.e0, 0,s0 - - srl nframes, a1, 24 - bgez a2, 1f - addiu nframes, 1 - sll nframes, 1 - -1: # Calculate offset of input buffer inside the output buffer - li dmem_output, %lo(VADPCM_BUFFER) - and rdram_input, 0x00FFFFFF - and a1, 0x00FFFFFF - sub t0, rdram_input, rdram_output - add dmem_input, t0, dmem_output - - # Shift buffer by output misalignment - andi t1, rdram_output, 7 - add dmem_output, t1 - add dmem_input, t1 - - beqz t1, 1f - sll buffer_out_size, nframes, 5 # buffer_out_size = nframes*32 - addiu buffer_out_size, 8 -1: - - sll buffer_in_size, nframes, 3 # buffer_in_size = nframes*8 - andi t1, rdram_input, 7 - beqz t1, 1f - add buffer_in_size, nframes # + nframes - addiu buffer_in_size, 8 -1: + vxor pred1a, pred1a; li dmem_codebook, %lo(MIX_VADPCM_CODEBOOK) + vxor pred1b, pred1b; li dmem_state, %lo(MIX_VADPCM_STATE) + vxor pred1c, pred1c; li dmem_output, %lo(DMEM_SAMPLE_CACHE) + vxor pred1d, pred1d; lh t0, %lo(CH_VBITS) + vxor pred1e, pred1e; add in0, s4, t9 + vxor pred1f, pred1f; addiu in0, 1 # past the control byte + vxor pred1g, pred1g; srl t1, t0, 1 + vxor pred1h, pred1h; add in1, in0, t1 + ltv pred1a.e0, 0,s0 + add in2, in0, t0 # in2 is s0: only free once ltv is done + add in3, in2, t1 + add in_end, in3 # in3 after the last frame of the fill + sll vstride, t0, 1 + addiu vstride, 1 # frame stride: 2*bits + 1 - #ifndef NDEBUG - # Check that dmem_input and RDRAM input have the same misalignment - andi t3, dmem_input, 7 - andi t2, rdram_input, 7 - assert_eq t3, t2, 0x1234 - #endif - - # Fetch the input buffer. Calculate size as nframes*9+misalignemnt - move s0, rdram_input - addiu t0, buffer_in_size, -1 - jal DMAIn - move s4, dmem_input - - move stereo_toggle, a2 lqv vstate1, 0x00,dmem_state - lqv vstate3, 0x10,dmem_state - addiu dmem_input, 1 - - ################################################################## - -VADPCM_DecompressLoop: -VADPCM_DecompressMono: - # Read from input: control byte, plus residuals - lbu t0, -1(dmem_input) - lpv vres0, 0,dmem_input - lpv vres1, 0,dmem_input - - # Process control byte: scaling li t1, 1 - srl scaling, t0, 4 - sllv scaling, t1, scaling - mtc2 scaling, vscale.e0 - # Process control byte: compute predictors address + # Residual unpacking, shared by the priming below and the decode loop. + # + # The 16 residuals of a frame are read as four halfwords, one per group of + # four consecutive residuals, and .q0 hands each halfword to the lane pair + # that holds the group: lanes 2j and 2j+1 of vres0 take residuals 4j and + # 4j+2, the same lanes of vres1 take 4j+1 and 4j+3. + # + # A lane isolates its residual in two steps. The vmudn keeps the low 16 + # bits of halfword * K, with K the power of two that brings the top of the + # residual to bit 15: everything before the residual falls off the top. + # The vmudm then multiplies by 2^bits and keeps bits 31..16 of the result, + # which is an arithmetic shift right by 16-bits: it drops everything after + # the residual and replicates its sign. Both leave a value in the 15.0 + # range that the frame scale then shifts up. + # + # In: in0..in3, one per group; vk0 (even residuals), vk1 (odd ones) + # Out: next_vres0 (even residuals), next_vres1 (odd ones), sign extended + +VADPCM_Priming: + lbu t0, -1(in0) + lsv vsrc.e0, 0x00,in0 + lsv vsrc.e2, 0x00,in1 + lsv vsrc.e4, 0x00,in2 + lsv vsrc.e6, 0x00,in3 + srl scaling, t0, 4 andi predictor, t0, 0xF - sll predictor, VADPCM_ORDER_SHIFT + 4 # multiply by order + sizeof(vector) - add predictor, dmem_codebook - - vsra8 vres0, vres0, 12 # 4bit unpacking - vsll vres1, vres1, 4 - vsra8 vres1, vres1, 12 - vmudh vres0, vscale.e0 - vmudh vres1, vscale.e0 - - # Load state and predictors (ORDER=2) - lqv pred0.e0, 0x00,predictor + sllv scaling, t1, scaling + sll predictor, VADPCM_ORDER_SHIFT + 4 + add predictor, dmem_codebook + mtc2 scaling, vscale.e0; vmudn next_vres0, vk0, vsrc.q0 + lqv pred0.e0, 0x00,predictor; vmudn next_vres1, vk1, vsrc.q0 lqv pred1.e0, 0x10,predictor lqv pred1a.e1, 0x10,predictor - lqv pred1b.e2, 0x10,predictor - lqv pred1c.e3, 0x10,predictor + lqv pred1b.e2, 0x10,predictor; vmudm next_vres0, next_vres0, kext + lqv pred1c.e3, 0x10,predictor; vmudm next_vres1, next_vres1, kext lqv pred1d.e4, 0x10,predictor lqv pred1e.e5, 0x10,predictor lqv pred1f.e6, 0x10,predictor lqv pred1g.e7, 0x10,predictor - - vmudh v____, pred0, vstate1.e6 + # vres1 of this first frame is computed by the top of the loop. + vmudh vres0, next_vres0, vscale.e0 + lqv vresult, 0x00,dmem_state + + # The loop advances in0..in2 at its bottom and in3 at its top, so that + # each of them has the whole body between the advance and the load it + # feeds. Leave the first three on the frame the loop reads first. + addu in0, vstride + addu in1, vstride + addu in2, vstride + + li slot_ptr, %lo(RSPQ_SCRATCH_MEM) + vnop + + .balign 8 +VADPCM_DecodeLoop: + # One iteration decodes one frame, that is 16 samples, in ~37 cycles: + # 2.3 cycles per sample, against the 8.3 that the resampler above spends + # on each output sample it then interpolates from them. + vmudh vres1, next_vres1, vscale.e0; addu in3, vstride + vmudh v____, pred1a, vres0.e0; lsv vsrc.e0, 0x00,in0 + vmadh v____, pred1c, vres0.e1; sqv vresult, 0,slot_ptr + vmadh v____, pred1e, vres0.e2; lsv vsrc.e2, 0x00,in1 + vmadh v____, pred1g, vres0.e3; sqv vresult, -16,dmem_output + vmadh v____, pred1b, vres1.e0; lqv vstate1, 0,slot_ptr + vmadh v____, pred1d, vres1.e1; srv vresult, 0x00,dmem_output + vmadh v____, pred1f, vres1.e2; lsv vsrc.e4, 0x00,in2 + vmadh v____, pred1h, vres1.e3; lbu t0, -1(in0) + vmadh v____, pred0, vstate1.e6; lsv vsrc.e6, 0x00,in3 vmadh v____, pred1, vstate1.e7 - vmadh v____, pred1a, vres0.e0 - vmadh v____, pred1b, vres1.e0 - vmadh v____, pred1c, vres0.e1 - vmadh v____, pred1d, vres1.e1 - vmadh v____, pred1e, vres0.e2 - vmadh v____, pred1f, vres1.e2 - vmadh v____, pred1g, vres0.e3 - vmadh v____, pred1h, vres1.e3 - vsar vstatef, COP2_ACC_MD - vsar vstate0, COP2_ACC_HI - vmudn v____, vstatef, vshift.e2 - vmadh vstate0, vstate0, vshift.e2 - - vmudh v____, pred0, vstate0.e6 - vmadh v____, pred1, vstate0.e7 - vmadh v____, pred1a, vres0.e4 - vmadh v____, pred1b, vres1.e4 - vmadh v____, pred1c, vres0.e5 - vmadh v____, pred1d, vres1.e5 - vmadh v____, pred1e, vres0.e6 - vmadh v____, pred1f, vres1.e6 - vmadh v____, pred1g, vres0.e7 - vmadh v____, pred1h, vres1.e7 - vsar vstatef, COP2_ACC_MD - vsar vstate1, COP2_ACC_HI - vmudn v____, vstatef, vshift.e2 - vmadh vstate1, vstate1, vshift.e2 - - sqv vstate0, 0x00,dmem_output - srv vstate0, 0x10,dmem_output - sqv vstate1, 0x10,dmem_output - srv vstate1, 0x20,dmem_output - - addiu nframes, -1 - addiu dmem_input, 9 - bgez a2, VADPCM_Output - addiu dmem_output, 32 - - vcopy vstate1, vstate3 - xori dmem_codebook, VADPCM_CODEBOOK_SIZE*16 - bltz stereo_toggle, VADPCM_DecompressMono - xor stereo_toggle, a2 - - # Stereo deinterleave - addiu dmem_output, -64 - - lqv vstate0, 0x00,dmem_output - lrv vstate0, 0x10,dmem_output - lqv vstate1, 0x10,dmem_output - lrv vstate1, 0x20,dmem_output - lqv vstate2, 0x20,dmem_output - lrv vstate2, 0x30,dmem_output - lqv vstate3, 0x30,dmem_output - lrv vstate3, 0x40,dmem_output -VADPCM_InterleaveStereo: - ssv vstate0.e0, 0x00,dmem_output - ssv vstate2.e0, 0x02,dmem_output - ssv vstate0.e1, 0x04,dmem_output - ssv vstate2.e1, 0x06,dmem_output - ssv vstate0.e2, 0x08,dmem_output - ssv vstate2.e2, 0x0A,dmem_output - ssv vstate0.e3, 0x0C,dmem_output - ssv vstate2.e3, 0x0E,dmem_output - - ssv vstate0.e4, 0x10,dmem_output - ssv vstate2.e4, 0x12,dmem_output - ssv vstate0.e5, 0x14,dmem_output - ssv vstate2.e5, 0x16,dmem_output - ssv vstate0.e6, 0x18,dmem_output - ssv vstate2.e6, 0x1A,dmem_output - ssv vstate0.e7, 0x1C,dmem_output - ssv vstate2.e7, 0x1E,dmem_output - - vcopy vstate0, vstate1 - vcopy vstate2, vstate3 - addiu dmem_output, 0x20 - bltz stereo_toggle, VADPCM_InterleaveStereo - xor stereo_toggle, a2 - - # Write output into RDRAM (async) -VADPCM_Output: - bgtz nframes, VADPCM_DecompressLoop - nop - ################################################################## - - # Save back state + vsar vstatef, COP2_ACC_MD; srl scaling, t0, 4 + vsar vstate0, COP2_ACC_HI; andi predictor, t0, 0xF + vmudn next_vres0, vk0, vsrc.q0; sllv scaling, t1, scaling + vmudn next_vres1, vk1, vsrc.q0; sll predictor, VADPCM_ORDER_SHIFT + 4 + vmudn v____, vstatef, vshift.e2; add predictor, dmem_codebook + vmadh vstate0, vstate0, vshift.e2; mtc2 scaling, vscale.e0 + + vmudh v____, pred1a, vres0.e4; addu in0, vstride + vmadh v____, pred1b, vres1.e4; lqv pred1a.e1, 0x10,predictor + vmadh v____, pred1c, vres0.e5; lqv pred1b.e2, 0x10,predictor + vmadh v____, pred1d, vres1.e5; lqv pred1c.e3, 0x10,predictor + vmadh v____, pred1e, vres0.e6; lqv pred1d.e4, 0x10,predictor + vmadh v____, pred1f, vres1.e6; lqv pred1e.e5, 0x10,predictor + vmadh v____, pred1g, vres0.e7; lqv pred1f.e6, 0x10,predictor + vmadh v____, pred1h, vres1.e7; lqv pred1g.e7, 0x10,predictor + vmadh v____, pred0, vstate0.e6; addu in1, vstride + vmadh v____, pred1, vstate0.e7; addu in2, vstride + + vsar vstatef, COP2_ACC_MD; sqv vstate0, 0x00,dmem_output + vsar vresult, COP2_ACC_HI; srv vstate0, 0x10,dmem_output + vmudm next_vres0, next_vres0, kext; lqv pred0.e0, 0x00,predictor + vmudm next_vres1, next_vres1, kext; lqv pred1.e0, 0x10,predictor + vmudn v____, vstatef, vshift.e2; addiu dmem_output, 32 + vmadh vresult, vresult, vshift.e2; bne in3, in_end, VADPCM_DecodeLoop + vmudh vres0, next_vres0, vscale.e0 + + sqv vresult, -16,dmem_output + srv vresult, 0x00,dmem_output + sqv vresult, 0,slot_ptr + lqv vstate1, 0,slot_ptr + + lw t0, %lo(CH_VNEXT_FRAME) + lh t1, %lo(CH_VDECN) + lw ra, %lo(CH_VADPCM_RA) + add t0, t1 sqv vstate1, 0x00,dmem_state - sqv vstate3, 0x10,dmem_state - move s4, dmem_state - move s0, a2 - jal DMAOutAsync - li t0, DMA_SIZE(32, 1) - - # Check if the output buffer is misaligned - and t0, rdram_output, 7 - beqz t0, VADPM_OutputDma - nop - - # Handle unaligned output buffer: we need to fetch and merge - # existing samples on misaligned addresses. Use dmem_state - # as scratch buffer - move s0, rdram_output - jal DMAIn - move s4, dmem_state - - li s0, %lo(VADPCM_BUFFER) - move s4, dmem_state -VADPCM_OutputMergeLoop: - lhu t1, 0(s4) - addiu s4, 2 - addiu t0, -2 - sh t1, 0(s0) - bnez t0, VADPCM_OutputMergeLoop - addiu s0, 2 - -VADPM_OutputDma: - # Save back output buffer - li s4, %lo(VADPCM_BUFFER) - move s0, rdram_output - jal DMAOut - addiu t0, buffer_out_size, -1 - - #if VADPCM_PROFILING - mfc0 k1, COP0_DP_CLOCK - subu k1, k1, k0 - and k1, k1, 0x00FFFFFF - emux_log_string "VADPCM_Decompress:\n" - emux_dump_gpr k1 - li k0, 4096-4 - sw k1, 0(k0) - #endif - - j RSPQ_Loop - nop - + jr ra + sw t0, %lo(CH_VNEXT_FRAME) .endfunc - diff --git a/src/audio/rsp_opus_comb.inc b/src/audio/rsp_opus_comb.inc index 4364fa21e..01406907d 100644 --- a/src/audio/rsp_opus_comb.inc +++ b/src/audio/rsp_opus_comb.inc @@ -124,7 +124,7 @@ assert_eq t0, 0, 0x8101 #endif - #D(emux_breakpoint) + #D(xbreak) li t1, 0x4000 mtc2 t1, vgains.e3 mtc2 gains_01, vgains.e0 @@ -166,8 +166,8 @@ # SINGLE: 39 cycles / 8 samples # DUAL: 71 cycles / 8 samples 1: - #emux_breakpoint - #D(emux_log_string "loop\n") + #xbreak + #D(xlog_string "loop\n") # Store previous loop result. sqv vouti, -0x10,dmem_out_i; @@ -191,8 +191,8 @@ lrv vsig1f, 0x10,dmem_in_f1; D(vmacf vgain1_inv, vwin, vgains.e5) lrv vsig2i, 0x10,dmem_in_i2; D(vmudn vzero, vzero) lrv vsig2f, 0x10,dmem_in_f2; D(vmacf vgain2_inv, vwin, vgains.e6) - #D(emux_dump_gpr dmem_in_i0, dmem_in_f0) - #D(emux_dump_vpr vsig0f, vsig0i) + #D(xlogregs_gpr dmem_in_i0, dmem_in_f0) + #D(xlogregs_vpr vsig0f, vsig0i) # Main MAC sequence for FIR filter lrv vsig3i, 0x10,dmem_in_i3; vmudn v____, vsig0f, vgain2 lrv vsig3f, 0x10,dmem_in_f3; vmadh v____, vsig0i, vgain2 @@ -226,8 +226,8 @@ D(lrv vsig4f, 0x10,dmem_in2_f4) D(lqv vsig4i, 0x00,dmem_in2_i4) D(lrv vsig4i, 0x10,dmem_in2_i4) - #D(emux_dump_gpr dmem_in2_i0, dmem_in2_f0) - #D(emux_dump_vpr vsig0f, vsig0i, vsig1f, vsig1i, vsig2f, vsig2i, vsig3f, vsig3i, vsig4f, vsig4i) + #D(xlogregs_gpr dmem_in2_i0, dmem_in2_f0) + #D(xlogregs_vpr vsig0f, vsig0i, vsig1f, vsig1i, vsig2f, vsig2i, vsig3f, vsig3i, vsig4f, vsig4i) D(addiu dmem_in2_i0, 0x10;) D(vmadn v____, vsig0f, vgain2_inv) D(addiu dmem_in2_f0, 0x10;) D(vmadh v____, vsig0i, vgain2_inv) D(addiu dmem_in2_i1, 0x10;) D(vmadn v____, vsig1f, vgain1_inv) @@ -266,7 +266,7 @@ lqv vsig3i, 0x00,dmem_in_i3; vsll32f_sat voutf, voutf, 1 lqv vsig3f, 0x00,dmem_in_f3; vsll32i_sat vouti, vouti, 1 - #D(emux_dump_vpr voutf, vouti) + #D(xlogregs_vpr voutf, vouti) D(addiu window, 0x10); D(vsub vwin_inv, vone, vwin) diff --git a/src/audio/rsp_opus_dsp.S b/src/audio/rsp_opus_dsp.S index e2e63bd55..56f49be29 100644 --- a/src/audio/rsp_opus_dsp.S +++ b/src/audio/rsp_opus_dsp.S @@ -2,7 +2,6 @@ # rsp_opus_dsp.S # # Authors: Giovanni Bajo -# ############################################################ #include @@ -151,7 +150,7 @@ OPUS_32to16: andi t0, dmem_in0, 0xF assert_eq t0, 0, 0x1234 - #emux_dump_gpr dmem_out0, dmem_out1 + #xlogregs_gpr dmem_out0, dmem_out1 addiu dmem_in1, dmem_in0, 1 addiu dmem_in2, dmem_in1, 1 @@ -198,7 +197,7 @@ OPUS_32to16_loop: srv vsigf.e0, 0x10,dmem_out1 addiu dmem_out0, 0x10 addiu dmem_out1, 0x10 - #emux_dump_vpr vsigi, vsigf + #xlogregs_vpr vsigi, vsigf jr ra nop @@ -252,8 +251,8 @@ OPUS_comb_fetch: srl dmem_idx, a1, 16 sll dmem_idx, 1 - #emux_log_string "OPUS_comb_fetch\n" - #emux_dump_gpr dmem_idx, samples_left + #xlog_string "OPUS_comb_fetch\n" + #xlogregs_gpr dmem_idx, samples_left #if RSPQ_DEBUG andi t0, a0, 7 diff --git a/src/audio/rsp_opus_fft_prerot.S b/src/audio/rsp_opus_fft_prerot.S index e446598a1..699e779b7 100644 --- a/src/audio/rsp_opus_fft_prerot.S +++ b/src/audio/rsp_opus_fft_prerot.S @@ -2,7 +2,6 @@ # rsp_opus_fft_prerot.S # # Authors: Giovanni Bajo -# ############################################################ #include "rsp_opus_fft.inc" #include "rsp_opus_fft_twiddles.inc" diff --git a/src/audio/rsp_ulc.S b/src/audio/rsp_ulc.S new file mode 100644 index 000000000..1fd9bf08f --- /dev/null +++ b/src/audio/rsp_ulc.S @@ -0,0 +1,1398 @@ +################################################################################ +# rsp_ulc.S +# +# Authors: Dominic Szablewski +# +# ULC RSP overlay: IMDCT, overlap-add, output scaling, and stereo interleaving. +# Signal processing uses signed 16-bit fixed-point values with 14 fractional +# bits (Q1.14). +################################################################################ + +#include + +.set noreorder +.set noat +.set nomacro + +.data + RSPQ_BeginOverlayHeader + RSPQ_DefineCommand synthesize, 16 + RSPQ_DefineCommand stereo_interleave, 12 + RSPQ_EndOverlayHeader + + RSPQ_EmptySavedState + + .align 1 + ROTATIONS: .half 11585, 11585, 0, 16384, 15137, 6270, 11585, 11585, 16069, 3196, 15137, 6270, 16305, 1606, 16069, 3196, 16364, 804, 16305, 1606, 16379, 402, 16364, 804, 16383, 201, 16379, 402, 16384, 101, 16383, 201, 16384, 50, 16384, 101, 16384, 25, 16384, 50, 16384, 13, 16384, 25, 16384, 6, 16384, 13, 16384, 3, 16384, 6, 16384, 2, 16384, 3, 16384, 1, 16384, 2, 16384, 0, 16384, 1, 16384, 0, 16384, 0 + .align 4 + WINDOW_C: .half 16305, 15679, 14449, 12665, 10394, 7723, 4756, 1606, 16364, 16207, 15893, 15426, 14811, 14053, 13160, 12140, 16379, 16340, 16261, 16143, 15986, 15791, 15557, 15286, 16383, 16373, 16353, 16324, 16284, 16235, 16176, 16107, 16384, 16381, 16376, 16369, 16359, 16347, 16332, 16315, 16384, 16383, 16382, 16380, 16378, 16375, 16371, 16367, 16384, 16384, 16384, 16383, 16382, 16382, 16381, 16380, 16384, 16384, 16384, 16384, 16384, 16383, 16383, 16383 + .align 4 + WINDOW_S: .half 1606, 4756, 7723, 10394, 12665, 14449, 15679, 16305, 804, 2404, 3981, 5520, 7005, 8423, 9760, 11003, 402, 1205, 2006, 2801, 3590, 4370, 5139, 5897, 201, 603, 1005, 1406, 1806, 2205, 2603, 2999, 101, 302, 503, 704, 904, 1105, 1306, 1506, 50, 151, 251, 352, 452, 553, 653, 754, 25, 75, 126, 176, 226, 276, 327, 377, 13, 38, 63, 88, 113, 138, 163, 188 + .align 1 + WINDOW_JUMP: .half 0, 16384, 11585, 11585, 15137, 6270, 16069, 3196, 16305, 1606, 16364, 804, 16379, 402, 16383, 201 + +.bss + TEMP_STATE_MEM_START: + .align 1 + # WORK and OUTPUT form one contiguous 2048-byte arena during a maximum-size + # transform. OUTPUT is only independent scratch after the transform ends. + WORK: .ds.b 1024 + .align 1 + OUTPUT: .ds.b 1024 + .align 1 + LAP: .ds.b 1024 + TEMP_STATE_MEM_END: + +.text + +#define coeff_rdram a0 +#define lap_rdram a1 +#define output_rdram a2 +#define command_flags a3 +#define transform_size s7 +#define transform_bytes s6 + +#define FLAG_TRANSFORM 0x80000000 +#define FLAG_LAP 0x40000000 +#define FLAG_CLEAR_LAP 0x20000000 +#define FLAG_DISCARD_OUTPUT 0x10000000 +#define FLAG_SCALE_SAMPLES 0x08000000 +#define FLAGS_SHIFT 27 + +# ------------------------------------------------------------------------------ +# Synthesize command +# performs imdct, clearing the lap buffer, lapping and output scaling depending +# on the input flags. +# ------------------------------------------------------------------------------ + +synthesize: + # Bits 0..10 contain the transform size, bits 16..26 the overlap size, + # and bits 27..31 the operation flags defined above. + andi transform_size, command_flags, 0x07ff + + # DMA the transform coefficients into DMEM. + sll transform_bytes, transform_size, 1 + addiu t0, transform_bytes, -1 + ori s4, zero, %lo(WORK) + addiu t2, zero, 12 + jal DMAExec + or s0, zero, coeff_rdram + + # TRANSFORM is the sign bit, allowing a direct signed branch here. + # Skip it when the input already contains transformed samples. + bgez command_flags, .imdct_transform_done + srl s5, transform_size, 1 + + jal swap_odd_reverse + sll s3, transform_size, 1 + + jal load_rotation_step + or v0, zero, s5 + vor $v27, $v00, $v01 + jal load_rotation_4 + vor $v28, $v00, $v02 + + ori s2, zero, %lo(WORK) + ori s1, zero, %lo(WORK) + addu s1, s1, transform_bytes + addiu t3, zero, 0x55 + ctc2 t3, COP2_CTRL_VCC + jal pre_twiddle + vsub $v22, $v00, $v23.v + +# Fused radix-4/radix-2 DIF FFT. Vector stages run from largest to smallest, +# followed by scalar length-4/2 stages. The result is reordered from +# bit-reversed to natural order before post-twiddling. + +#define complex_count $21 +#define fft_length $16 +#define fft_half $30 +#define rotation_size $2 +#define rotation_cursor $10 +#define radix2_pending $9 +#define fft_quarter $8 +#define fft_block $26 +#define stage_end $25 +#define odd_offset $29 +#define block_stride $27 +#define even_ptr $24 +#define odd_ptr $15 +#define butterfly $14 +#define radix4_ptr1 $13 +#define radix4_ptr2 $12 +#define radix4_ptr3 $11 + + + or fft_length, complex_count, $0 + andi radix2_pending, complex_count, 0x2a0 + srl rotation_size, fft_length, 2 + addiu rotation_cursor, $0, %lo(ROTATIONS) - 8 +.fft_rotation_address: + srl rotation_size, rotation_size, 1 + bnez rotation_size, .fft_rotation_address + addiu rotation_cursor, rotation_cursor, 8 + +.fft_stage_setup: + srl fft_half, fft_length, 1 + + # Load and broadcast this stage's one-step twiddle rotation. + lsv $v26.e0, 4, rotation_cursor + lsv $v25.e0, 6, rotation_cursor + addiu rotation_cursor, rotation_cursor, -8 + sll odd_offset, fft_half, 2 + vor $v26, $v00, $v26.e0 + vor $v25, $v00, $v25.e0 + + # Build four consecutive twiddles and their four-step jump. + vxor $v19, $v00, $v31.e1 + vxor $v18, $v00, $v00 + vsub $v17, $v00, $v25.v + vxor $v23, $v00, $v00 + vxor $v22, $v00, $v00 + vmov $v23.e0, $v19.e0 + vmov $v23.e1, $v19.e0 + vmov $v22.e0, $v18.e0 + vmov $v22.e1, $v18.e0 + + # r0 -> r1 + vor $v16, $v00, $v19 + vor $v15, $v00, $v18 + vmulf $v19, $v16, $v26.v + vmacf $v19, $v15, $v17.v + vmulf $v18, $v16, $v25.v + vmacf $v18, $v15, $v26.v + vadd $v19, $v19, $v19.v + vadd $v18, $v18, $v18.v + vmov $v23.e2, $v19.e0 + vmov $v23.e3, $v19.e0 + vmov $v22.e2, $v18.e0 + vmov $v22.e3, $v18.e0 + + # r1 -> r2 + vor $v16, $v00, $v19 + vor $v15, $v00, $v18 + vmulf $v19, $v16, $v26.v + vmacf $v19, $v15, $v17.v + vmulf $v18, $v16, $v25.v + vmacf $v18, $v15, $v26.v + vadd $v19, $v19, $v19.v + vadd $v18, $v18, $v18.v + vmov $v23.e4, $v19.e0 + vmov $v23.e5, $v19.e0 + vmov $v22.e4, $v18.e0 + vmov $v22.e5, $v18.e0 + + # r2 -> r3 + vor $v16, $v00, $v19 + vor $v15, $v00, $v18 + vmulf $v19, $v16, $v26.v + vmacf $v19, $v15, $v17.v + vmulf $v18, $v16, $v25.v + vmacf $v18, $v15, $v26.v + vadd $v19, $v19, $v19.v + vadd $v18, $v18, $v18.v + vmov $v23.e6, $v19.e0 + vmov $v23.e7, $v19.e0 + vmov $v22.e6, $v18.e0 + vmov $v22.e7, $v18.e0 + + # Reuse r3 and advance once for the four-twiddle jump r4. + vor $v16, $v00, $v19 + vor $v15, $v00, $v18 + vmulf $v19, $v16, $v26.v + vmacf $v19, $v15, $v17.v + vmulf $v18, $v16, $v25.v + vmacf $v18, $v15, $v26.v + vadd $v19, $v19, $v19.v + vadd $v18, $v18, $v18.v + vor $v21, $v00, $v19 + vor $v20, $v00, $v18 + + vxor $v19, $v00, $v00.e0 + vsub $v19, $v19, $v20.v + + # Set up the byte offsets and stage bounds shared by both kernels. + sll block_stride, fft_length, 2 + ori fft_block, $0, %lo(WORK) + ori stage_end, $0, %lo(WORK) + sll even_ptr, complex_count, 2 + addu stage_end, stage_end, even_ptr + beqz radix2_pending, .fft_radix4_setup + nop + + # Process every four-butterfly group in this FFT stage. + vor $v28, $v00, $v23 + +.fft_vector_block: + vor $v27, $v00, $v22 + or even_ptr, fft_block, $0 + addu odd_ptr, fft_block, odd_offset + or butterfly, $0, $0 + +.fft_vector_butterfly: + lqv $v18, 0, 0, even_ptr + lqv $v17, 0, 0, odd_ptr + vor $v15, $v00, $v18.q1 + vor $v16, $v00, $v18.q0 + addiu butterfly, butterfly, 4 + + # DIF butterfly: sum stays in the even half; rotate the difference. + vsub $v12, $v00, $v17.q0 + vsub $v11, $v00, $v17.q1 + vsub $v12, $v16, $v12.v + vsub $v11, $v15, $v11.v + vsub $v10, $v16, $v17.q0 + vsub $v09, $v15, $v17.q1 + + vsub $v08, $v00, $v27.v + vmulf $v14, $v10, $v28.v + vmacf $v14, $v09, $v27.v + vmulf $v13, $v09, $v28.v + vmacf $v13, $v10, $v08.v + + # Pack the sum into a dead input register. This leaves v18/v17 available for + # the twiddle copies while the multiply results become readable. + vmrg $v16, $v12, $v11 + vadd $v14, $v14, $v14.v + vadd $v13, $v13, $v13.v + vor $v18, $v00, $v28 + vor $v17, $v00, $v27 + sqv $v16, 0, 0, even_ptr + vmrg $v16, $v14, $v13 + + vmulf $v28, $v18, $v21.v + vmacf $v28, $v17, $v19.v + vmulf $v27, $v18, $v20.v + sqv $v16, 0, 0, odd_ptr + vmacf $v27, $v17, $v21.v + addiu even_ptr, even_ptr, 16 + addiu odd_ptr, odd_ptr, 16 + vadd $v28, $v28, $v28.v + bne butterfly, fft_half, .fft_vector_butterfly + vadd $v27, $v27, $v27.v + + addu fft_block, fft_block, block_stride + bne fft_block, stage_end, .fft_vector_block + vor $v28, $v00, $v23 + + or radix2_pending, $0, $0 + b .fft_stage_setup + srl fft_length, fft_length, 1 + +.fft_radix4_setup: + srl fft_quarter, fft_length, 2 + vor $v28, $v00, $v23 + +.fft_radix4_block: + vor $v27, $v00, $v22 + or even_ptr, fft_block, $0 + addu radix4_ptr1, even_ptr, fft_length + addu radix4_ptr2, radix4_ptr1, fft_length + addu radix4_ptr3, radix4_ptr2, fft_length + or butterfly, $0, $0 + +.fft_radix4_butterfly: + # r2 = r * r for the second fused DIF level. + vsub $v24, $v00, $v27.v + + # Interleave the four input loads with the independent r2 calculation. + lqv $v29, 0, 0, even_ptr + vmulf $v26, $v28, $v28.v + lqv $v30, 0, 0, radix4_ptr1 + lqv $v14, 0, 0, radix4_ptr2 + lqv $v12, 0, 0, radix4_ptr3 + vmacf $v26, $v27, $v24.v + addiu butterfly, butterfly, 4 + vmulf $v25, $v28, $v27.v + vmacf $v25, $v28, $v27.v + vor $v18, $v00, $v29.q0 + vadd $v26, $v26, $v26.v + vor $v15, $v00, $v30.q1 + vadd $v25, $v25, $v25.v + vor $v16, $v00, $v30.q0 + vor $v17, $v00, $v29.q1 + vsub $v04, $v00, $v25.v + + # First DIF level: (x0,x2), rotated by r. + vsub $v06, $v18, $v14.q0 + vsub $v05, $v17, $v14.q1 + vsub $v02, $v00, $v14.q0 + vsub $v01, $v00, $v14.q1 + vmulf $v10, $v06, $v28.v + vmacf $v10, $v05, $v27.v + vmulf $v09, $v05, $v28.v + vmacf $v09, $v06, $v24.v + vsub $v18, $v18, $v02.v + vsub $v17, $v17, $v01.v + vadd $v10, $v10, $v10.v + vadd $v09, $v09, $v09.v + + # First DIF level: (x1,x3), rotated by r * -i. + vsub $v06, $v16, $v12.q0 + vsub $v05, $v15, $v12.q1 + vsub $v02, $v00, $v12.q0 + vsub $v01, $v00, $v12.q1 + vsub $v03, $v00, $v06.v + vmulf $v08, $v05, $v28.v + vmacf $v08, $v06, $v24.v + vmulf $v07, $v03, $v28.v + vmacf $v07, $v05, $v24.v + vsub $v16, $v16, $v02.v + vsub $v15, $v15, $v01.v + vadd $v08, $v08, $v08.v + vadd $v07, $v07, $v07.v + + # Second DIF level for the sums, rotated by r2. + vsub $v06, $v18, $v16.v + vsub $v05, $v17, $v15.v + vsub $v02, $v00, $v16.v + vsub $v01, $v00, $v15.v + vmulf $v14, $v06, $v26.v + vmacf $v14, $v05, $v25.v + vmulf $v13, $v05, $v26.v + vmacf $v13, $v06, $v04.v + vsub $v18, $v18, $v02.v + vsub $v17, $v17, $v01.v + vadd $v14, $v14, $v14.v + vadd $v13, $v13, $v13.v + + # Second DIF level for the differences, rotated by r2. + vsub $v06, $v10, $v08.v + vsub $v05, $v09, $v07.v + vsub $v02, $v00, $v08.v + vsub $v01, $v00, $v07.v + vmulf $v12, $v06, $v26.v + vmacf $v12, $v05, $v25.v + vmulf $v11, $v05, $v26.v + vmacf $v11, $v06, $v04.v + vsub $v10, $v10, $v02.v + vsub $v09, $v09, $v01.v + vadd $v12, $v12, $v12.v + vadd $v11, $v11, $v11.v + + # Pack and store the four fused-stage outputs. + vmrg $v29, $v18, $v17 + vmrg $v16, $v14, $v13 + + # Advance the four-lane twiddle by r4. + vor $v18, $v00, $v28 + vor $v17, $v00, $v27 + vmrg $v15, $v10, $v09 + sqv $v29, 0, 0, even_ptr + addiu even_ptr, even_ptr, 16 + vmrg $v14, $v12, $v11 + sqv $v16, 0, 0, radix4_ptr1 + vmulf $v28, $v18, $v21.v + addiu radix4_ptr1, radix4_ptr1, 16 + vmacf $v28, $v17, $v19.v + sqv $v15, 0, 0, radix4_ptr2 + vmulf $v27, $v18, $v20.v + addiu radix4_ptr2, radix4_ptr2, 16 + vmacf $v27, $v17, $v21.v + sqv $v14, 0, 0, radix4_ptr3 + addiu radix4_ptr3, radix4_ptr3, 16 + vadd $v28, $v28, $v28.v + bne butterfly, fft_quarter, .fft_radix4_butterfly + vadd $v27, $v27, $v27.v + + addu fft_block, fft_block, block_stride + bne fft_block, stage_end, .fft_radix4_block + vor $v28, $v00, $v23 + + addiu rotation_size, $0, 4 + srl fft_length, fft_length, 2 + bne fft_length, rotation_size, .fft_stage_setup + addiu rotation_cursor, rotation_cursor, -8 + +.fft_stages_done: + # Fuse the final length-4 and length-2 passes into one radix-4 DIF stage. + ori even_ptr, $0, %lo(WORK) + ori stage_end, $0, %lo(WORK) + sll odd_ptr, complex_count, 2 + addu stage_end, stage_end, odd_ptr + +.fft_radix4_final: + lsv $v23.e0, 0, even_ptr + lsv $v22.e0, 2, even_ptr + lsv $v21.e0, 4, even_ptr + lsv $v20.e0, 6, even_ptr + lsv $v19.e0, 8, even_ptr + lsv $v18.e0, 10, even_ptr + lsv $v17.e0, 12, even_ptr + lsv $v16.e0, 14, even_ptr + + # First DIF level: (x0,x2) and (x1,x3). + vsub $v15, $v00, $v19.v + vsub $v14, $v00, $v18.v + vsub $v11, $v00, $v17.v + vsub $v10, $v00, $v16.v + vsub $v13, $v23, $v19.v + vsub $v12, $v22, $v18.v + vsub $v09, $v21, $v17.v + vsub $v08, $v20, $v16.v + vsub $v15, $v23, $v15.v + vsub $v14, $v22, $v14.v + vsub $v11, $v21, $v11.v + vsub $v10, $v20, $v10.v + vsub $v03, $v00, $v09.v # -i * (v09 + i*v08) = v08 - i*v09 + + # Second DIF level for the two unrotated sums. + vsub $v07, $v00, $v11.v + vsub $v06, $v00, $v10.v + vsub $v05, $v15, $v11.v + vsub $v04, $v14, $v10.v + vsub $v07, $v15, $v07.v + vsub $v06, $v14, $v06.v + ssv $v05, 0, 4, even_ptr + ssv $v04, 0, 6, even_ptr + ssv $v07, 0, 0, even_ptr + + # Second DIF level for the two rotated differences. + vsub $v07, $v00, $v08.v + ssv $v06, 0, 2, even_ptr + vsub $v06, $v00, $v03.v + vsub $v05, $v13, $v08.v + vsub $v04, $v12, $v03.v + vsub $v07, $v13, $v07.v + vsub $v06, $v12, $v06.v + ssv $v05, 0, 12, even_ptr + ssv $v04, 0, 14, even_ptr + ssv $v07, 0, 8, even_ptr + ssv $v06, 0, 10, even_ptr + + addiu even_ptr, even_ptr, 16 + bne even_ptr, stage_end, .fft_radix4_final + nop + + jal dif_reorder + nop + +#undef complex_count +#undef fft_length +#undef fft_half +#undef rotation_size +#undef rotation_cursor +#undef radix2_pending +#undef fft_quarter +#undef fft_block +#undef stage_end +#undef odd_offset +#undef block_stride +#undef even_ptr +#undef odd_ptr +#undef butterfly +#undef radix4_ptr1 +#undef radix4_ptr2 +#undef radix4_ptr3 + + jal load_rotation_step + or v0, zero, s5 + vor $v27, $v00, $v01 + jal load_rotation_4 + vor $v28, $v00, $v02 + + ori s2, zero, %lo(WORK) + jal post_twiddle + vsub $v22, $v00, $v23.v + +.imdct_transform_done: + # Shifting LAP into the sign bit allows another direct signed branch. + # Without lapping, return the transformed coefficients to coeff_rdram. + sll t3, command_flags, 1 + bgez t3, .raw_output + lui t3, 0x07ff + + # Decode lapping flags and fetch or clear the persistent 512-sample lap state. + and s5, command_flags, t3 + srl s2, command_flags, FLAGS_SHIFT + andi s1, s2, (FLAG_SCALE_SAMPLES >> FLAGS_SHIFT) + srl s3, s2, 4 + xori s3, s3, 1 + + andi t4, s2, (FLAG_CLEAR_LAP >> FLAGS_SHIFT) + bnez t4, .clear_lap + srl s5, s5, 16 + + addiu t0, zero, 1023 + ori s4, zero, %lo(LAP) + addiu t2, zero, 12 + jal DMAExec + or s0, zero, lap_rdram + b .lap_ready + nop + +.clear_lap: + ori t4, zero, %lo(LAP) + addiu t5, t4, 1024 + vxor $v31, $v31, $v31 +.clear_lap_next: + sqv $v31, 0, 0, t4 + addiu t4, t4, 16 + bne t4, t5, .clear_lap_next + nop + +.lap_ready: + + # Save the lower transform half as the next lap state. + addiu t0, transform_size, -1 + ori s4, zero, %lo(WORK) + addiu t2, zero, -32756 + jal DMAExec + or s0, zero, lap_rdram + + ori fp, zero, %lo(LAP) + addu fp, fp, transform_size + addiu fp, fp, -2 + ori sp, zero, %lo(WORK) + addu sp, sp, transform_size + ori k1, zero, %lo(WORK) + subu k0, transform_size, s5 + srl k0, k0, 1 + jal lap_prefix + subu s0, s1, s3 + + beqz s5, .lap_complete + srl t4, s5, 1 + jal load_window_8 + vxor $v24, $v00, $v00 + jal lap_windowed + vsub $v24, $v24, $v25.v + +.lap_complete: + beqz s3, .scaled_output + nop + + # Reassemble and merge a short subblock into the 1024-sample state. + jal copy_lap_high + ori fp, zero, %lo(LAP) + + addiu t0, transform_size, -1 + ori s4, zero, %lo(LAP) + addiu t2, zero, 12 + jal DMAExec + or s0, zero, lap_rdram + + jal reshuffle_lap + addiu t0, zero, 1024 + + addiu t0, t0, -1 + ori s4, zero, %lo(LAP) + addiu t2, zero, -32756 + jal DMAExec + or s0, zero, lap_rdram + + andi t4, s2, (FLAG_DISCARD_OUTPUT >> FLAGS_SHIFT) + bnez t4, RSPQ_Loop + nop + + sll fp, transform_size, 1 + addiu t0, fp, -1 + ori s4, zero, %lo(OUTPUT) + addiu t2, zero, -32756 + jal DMAExec + or s0, zero, output_rdram + j RSPQ_Loop + nop + +.scaled_output: + andi t4, s2, (FLAG_DISCARD_OUTPUT >> FLAGS_SHIFT) + bnez t4, RSPQ_Loop + nop + + addiu t0, transform_size, -1 + ori s4, zero, %lo(WORK) + addiu t2, zero, -32756 + jal DMAExec + or s0, zero, output_rdram + + addu fp, output_rdram, transform_size + addiu t0, transform_size, -1 + ori s4, zero, %lo(LAP) + addiu t2, zero, -32756 + jal DMAExec + or s0, zero, fp + j RSPQ_Loop + nop + +.raw_output: + sll t0, transform_size, 1 + addiu t0, t0, -1 + ori s4, zero, %lo(WORK) + addiu t2, zero, -32756 + jal DMAExec + or s0, zero, coeff_rdram + j RSPQ_Loop + nop + +#undef coeff_rdram +#undef lap_rdram +#undef command_flags +#undef output_rdram +#undef transform_size +#undef transform_bytes +#undef FLAG_TRANSFORM +#undef FLAG_LAP +#undef FLAG_CLEAR_LAP +#undef FLAG_DISCARD_OUTPUT +#undef FLAG_SCALE_SAMPLES +#undef FLAGS_SHIFT + + +# ------------------------------------------------------------------------------ +# Transform reordering +# ------------------------------------------------------------------------------ + +# Convert the bit-reversed DIF output to natural order without an in-place +# bit-reversal pass. LAP temporarily holds the lower natural-order half. +# Inputs: s5 = complex count, s7 = transform size. +# Clobbers: s0, fp, sp, k1, k0, t9..t4. +#define reorder_half_bytes $16 +#define reorder_rev $30 +#define reorder_bit $29 +#define reorder_dst $27 +#define reorder_groups $26 +#define reorder_index $25 +#define reorder_src0 $24 +#define reorder_src2 $15 +#define reorder_src1 $14 +#define reorder_src3 $13 +#define reorder_value $12 + +dif_reorder: + srl reorder_half_bytes, $23, 1 + srl reorder_groups, $21, 3 + or reorder_index, $0, $0 + or reorder_rev, $0, $0 + addiu reorder_dst, $0, %lo(LAP) + sll reorder_src0, reorder_rev, 3 + +# Natural indices 0..N/2-1 live at even DIF positions. Four consecutive +# natural indices select quadrants in the order 0, 2, 1, 3. +.dif_reorder_low: + addiu reorder_src0, reorder_src0, %lo(WORK) + addu reorder_src2, reorder_src0, $23 + addu reorder_src1, reorder_src0, reorder_half_bytes + addu reorder_src3, reorder_src2, reorder_half_bytes + llv $v29.e0, 0, reorder_src0 + llv $v29.e2, 0, reorder_src2 + llv $v29.e4, 0, reorder_src1 + llv $v29.e6, 0, reorder_src3 + addiu reorder_index, reorder_index, 1 + srl reorder_bit, reorder_groups, 1 + beq reorder_index, reorder_groups, .dif_compact_odd_setup + sqv $v29, 0, 0, reorder_dst + + addiu reorder_dst, reorder_dst, 16 +.dif_reorder_low_carry: + xor reorder_rev, reorder_rev, reorder_bit + and reorder_value, reorder_rev, reorder_bit + beqz reorder_value, .dif_reorder_low_carry + srl reorder_bit, reorder_bit, 1 + b .dif_reorder_low + sll reorder_src0, reorder_rev, 3 + +# Preserve all odd DIF positions compactly before WORK is overwritten. +.dif_compact_odd_setup: + addiu reorder_src0, $0, %lo(WORK) + 4 + addiu reorder_dst, $0, %lo(WORK) + srl reorder_index, $21, 1 +.dif_compact_odd: + llv $v29.e0, 0, reorder_src0 + llv $v29.e2, 8, reorder_src0 + llv $v29.e4, 16, reorder_src0 + llv $v29.e6, 24, reorder_src0 + addiu reorder_src0, reorder_src0, 32 + addiu reorder_index, reorder_index, -4 + addiu reorder_dst, reorder_dst, 16 + bnez reorder_index, .dif_compact_odd + sqv $v29, 0, -16, reorder_dst + +# Natural indices N/2..N-1 now come from the compact odd-position array. + or reorder_index, $0, $0 + or reorder_rev, $0, $0 + addiu reorder_dst, $23, %lo(WORK) + sll reorder_src0, reorder_rev, 2 +.dif_reorder_high: + addiu reorder_src0, reorder_src0, %lo(WORK) + addu reorder_src2, reorder_src0, reorder_half_bytes + srl reorder_bit, reorder_half_bytes, 1 + addu reorder_src1, reorder_src0, reorder_bit + addu reorder_src3, reorder_src2, reorder_bit + llv $v29.e0, 0, reorder_src0 + llv $v29.e2, 0, reorder_src2 + llv $v29.e4, 0, reorder_src1 + llv $v29.e6, 0, reorder_src3 + addiu reorder_index, reorder_index, 1 + srl reorder_bit, reorder_groups, 1 + beq reorder_index, reorder_groups, .dif_restore_low_setup + sqv $v29, 0, 0, reorder_dst + + addiu reorder_dst, reorder_dst, 16 +.dif_reorder_high_carry: + xor reorder_rev, reorder_rev, reorder_bit + and reorder_value, reorder_rev, reorder_bit + beqz reorder_value, .dif_reorder_high_carry + srl reorder_bit, reorder_bit, 1 + b .dif_reorder_high + sll reorder_src0, reorder_rev, 2 + +# Restore the lower natural-order half from LAP with vector copies. +.dif_restore_low_setup: + addiu reorder_src0, $0, %lo(LAP) + addiu reorder_dst, $0, %lo(WORK) + addu reorder_src1, reorder_dst, $23 +.dif_restore_low: + lqv $v29, 0, 0, reorder_src0 + addiu reorder_dst, reorder_dst, 16 + addiu reorder_src0, reorder_src0, 16 + bne reorder_dst, reorder_src1, .dif_restore_low + sqv $v29, 0, -16, reorder_dst + + jr $31 + nop + +#undef reorder_half_bytes +#undef reorder_rev +#undef reorder_bit +#undef reorder_dst +#undef reorder_groups +#undef reorder_index +#undef reorder_src0 +#undef reorder_src2 +#undef reorder_src1 +#undef reorder_src3 +#undef reorder_value + + +# Swap WORK's odd-indexed samples with the same sequence in reverse order. +# This packs/unpacks the real/imaginary FFT representation. +# Input: s7 = transform size. +# Clobbers: t8, t7, t6, v29..v26. +# Processes four odd-lane pairs per iteration; supported sizes are powers of +# two greater than or equal to 32. +#define swap_left $24 +#define swap_right $15 +#define swaps_left $14 + +swap_odd_reverse: + addiu swap_left, $0, %lo(WORK) + sll swap_right, $23, 1 + addiu swap_right, swap_right, %lo(WORK) - 16 + srl swaps_left, $23, 2 + +.swap_odd_reverse_loop: + lqv $v29, 0, 0, swap_left + lqv $v28, 0, 0, swap_right + vor $v27, $v00, $v29 + vor $v26, $v00, $v28 + vmov $v27.e1, $v28.e7 + vmov $v27.e3, $v28.e5 + vmov $v27.e5, $v28.e3 + vmov $v27.e7, $v28.e1 + vmov $v26.e1, $v29.e7 + vmov $v26.e3, $v29.e5 + vmov $v26.e5, $v29.e3 + vmov $v26.e7, $v29.e1 + sqv $v27, 0, 0, swap_left + addiu swaps_left, swaps_left, -4 + addiu swap_left, swap_left, 16 + sqv $v26, 0, 0, swap_right + bnez swaps_left, .swap_odd_reverse_loop + addiu swap_right, swap_right, -16 + + jr $31 + nop + +#undef swap_left +#undef swap_right +#undef swaps_left + + +# ------------------------------------------------------------------------------ +# Overlap-add helpers +# ------------------------------------------------------------------------------ + +# Load the first eight overlap-window coefficients and the eight-sample +# rotation step. Nonzero overlaps are powers of two in the range 32..1024. +# Input: s5 = overlap. +# Outputs: v28 = c, v27 = s, v26 = jump c, v25 = jump s. +# Clobbers: t9, t8, t7, t6, t5. + +load_window_8: + addiu $25, $0, %lo(WINDOW_C) # t9: c address + addiu $24, $0, %lo(WINDOW_S) # t8: s address + addiu $15, $0, %lo(WINDOW_JUMP) # t7: jump address + or $14, $21, $0 # t6: remaining + addiu $13, $0, 8 + +# overlap >= 32, so at least one advance is always required. +.load_window_next: + srl $14, $14, 1 + addiu $25, $25, 16 + addiu $24, $24, 16 + bne $14, $13, .load_window_next + addiu $15, $15, 4 + + lqv $v28, 0, 0, $25 + lqv $v27, 0, 0, $24 + lsv $v26.e0, 0, $15 + lsv $v25.e0, 2, $15 + vor $v26, $v00, $v26.e0 + jr $31 + vor $v25, $v00, $v25.e0 + + +# Merge one decoded short block into the persistent 1024-sample lap layout. +# Exactly size samples are staged in OUTPUT. LAP receives the remaining old +# state followed by the unused decoded samples, both stored backwards. +# Inputs: s7 = size, s1 = mono output shift (0 or 1). +# Clobbers: k0, t9, t8, t7, t6, t5, fp, t4. + +#define available $26 +#define lap_dst $25 +#define lap_src $24 +#define decoded_src $15 +#define output_dst $14 +#define shared_count $13 +#define samples_left $30 +#define sample $12 + +reshuffle_lap: + addiu available, $0, 1024 # k0: available + subu available, available, $23 + or shared_count, $23, $0 # t5: shared = size + srl available, available, 1 + sltu sample, available, shared_count + addiu decoded_src, $0, %lo(WORK) # t7: decoded source + addiu lap_dst, $0, %lo(LAP) + 1024 # t9: lap destination + addiu output_dst, $0, %lo(OUTPUT) # t6: output destination + beqz sample, .reshuffle_shared_ready + or lap_src, lap_dst, $0 # t8: lap source + or shared_count, available, $0 # shared = available + +.reshuffle_shared_ready: + or samples_left, shared_count, $0 # fp: remaining = shared + +# Emit the shared tail of the old lap state in reverse order. +.reshuffle_output_lap: + lh sample, -2(lap_src) + addiu samples_left, samples_left, -1 + addiu lap_src, lap_src, -2 + sllv sample, sample, $17 + sh sample, 0(output_dst) + bnez samples_left, .reshuffle_output_lap + addiu output_dst, output_dst, 2 + +# If size exceeds available, emit the remainder from the decoded block. + subu samples_left, $23, shared_count + beqz samples_left, .reshuffle_lap_old_setup + nop +.reshuffle_output_decoded: + lh sample, 0(decoded_src) + addiu decoded_src, decoded_src, 2 + addiu samples_left, samples_left, -1 + sllv sample, sample, $17 + sh sample, 0(output_dst) + bnez samples_left, .reshuffle_output_decoded + addiu output_dst, output_dst, 2 + +# Preserve any old lap samples which were not emitted. +.reshuffle_lap_old_setup: + subu samples_left, available, shared_count + beqz samples_left, .reshuffle_lap_decoded_setup + nop +.reshuffle_lap_old: + addiu lap_dst, lap_dst, -2 + addiu samples_left, samples_left, -1 + lh sample, -2(lap_src) + addiu lap_src, lap_src, -2 + bnez samples_left, .reshuffle_lap_old + sh sample, 0(lap_dst) + +# Fill the rest of the new lap state from the decoded block. +.reshuffle_lap_decoded_setup: + or samples_left, shared_count, $0 +.reshuffle_lap_decoded: + lh sample, 0(decoded_src) + addiu lap_dst, lap_dst, -2 + addiu decoded_src, decoded_src, 2 + addiu samples_left, samples_left, -1 + bnez samples_left, .reshuffle_lap_decoded + sh sample, 0(lap_dst) + + jr $31 + nop + +#undef available +#undef lap_dst +#undef lap_src +#undef decoded_src +#undef output_dst +#undef shared_count +#undef samples_left +#undef sample + + + +# Emit the unwindowed prefix of an IMDCT block and replace the consumed lap +# samples with the reversed high transform half. +# Inputs/outputs: fp = lap pointer, sp = transform pointer, +# k1 = output pointer, k0 = sample count. +# Input: s0 = 1 to scale mono output; any other value leaves it unscaled. +# Clobbers: fp, sp, k1, k0, t9, t8, t7, v28, v27. + +#define lap_ptr $30 +#define transform_ptr $29 +#define output_ptr $27 +#define samples_left $26 +#define load_a $25 +#define one $24 +#define load_b $15 + +lap_prefix: + beqz samples_left, .lap_prefix_done + addiu one, $0, 1 # t8: direct-mono comparison + +.lap_prefix_next: + lh load_a, 0(lap_ptr) + lh load_b, -2(lap_ptr) + vxor $v28, $v00, $v00 + mtc2 load_b, $v28.e1 + mtc2 load_a, $v28.e0 + lh load_a, -4(lap_ptr) + lh load_b, -6(lap_ptr) + mtc2 load_b, $v28.e3 + mtc2 load_a, $v28.e2 + lh load_a, -8(lap_ptr) + lh load_b, -10(lap_ptr) + mtc2 load_b, $v28.e5 + mtc2 load_a, $v28.e4 + lh load_a, -12(lap_ptr) + lh load_b, -14(lap_ptr) + mtc2 load_a, $v28.e6 + mtc2 load_b, $v28.e7 + lqv $v27, 0, 0, transform_ptr + bne $16, one, .lap_prefix_store + nop + vaddc $v28, $v28, $v28.v + vaddc $v27, $v27, $v27.v + +.lap_prefix_store: + sqv $v28, 0, 0, output_ptr + ssv $v27, 0, 0, lap_ptr + ssv $v27, 2, -2, lap_ptr + ssv $v27, 4, -4, lap_ptr + ssv $v27, 6, -6, lap_ptr + ssv $v27, 8, -8, lap_ptr + ssv $v27, 10, -10, lap_ptr + ssv $v27, 12, -12, lap_ptr + ssv $v27, 14, -14, lap_ptr + addiu transform_ptr, transform_ptr, 16 + addiu output_ptr, output_ptr, 16 + addiu samples_left, samples_left, -8 + bnez samples_left, .lap_prefix_next + addiu lap_ptr, lap_ptr, -16 + +.lap_prefix_done: + jr $31 + nop + +#undef lap_ptr +#undef transform_ptr +#undef output_ptr +#undef samples_left +#undef load_a +#undef one +#undef load_b + + + +# Apply the windowed overlap region, producing low output in WORK and the +# reversed high output in LAP while advancing the window rotation. +# Inputs/outputs: fp = lap pointer, sp = transform pointer, +# k1 = output pointer. +# Inputs: t4 = sample count; s0 = 1 to scale mono output; +# v28/v27 = c/s, v26/v25 = jump c/s, v24 = -jump s. +# Clobbers: fp, sp, k1, t4, t9, t8, t7, v23..v17. + +#define lap_ptr $30 +#define transform_ptr $29 +#define output_ptr $27 +#define samples_left $12 +#define load_a $25 +#define one $24 +#define load_b $15 + +lap_windowed: + addiu one, $0, 1 + +.lap_windowed_next: + lh load_a, 0(lap_ptr) + lh load_b, -2(lap_ptr) + vxor $v23, $v00, $v00 + mtc2 load_b, $v23.e1 + mtc2 load_a, $v23.e0 + lh load_a, -4(lap_ptr) + lh load_b, -6(lap_ptr) + mtc2 load_b, $v23.e3 + mtc2 load_a, $v23.e2 + lh load_a, -8(lap_ptr) + lh load_b, -10(lap_ptr) + mtc2 load_b, $v23.e5 + mtc2 load_a, $v23.e4 + lh load_a, -12(lap_ptr) + lh load_b, -14(lap_ptr) + mtc2 load_a, $v23.e6 + mtc2 load_b, $v23.e7 + lqv $v22, 0, 0, transform_ptr + + vsub $v21, $v00, $v27.v + vmulf $v20, $v23, $v28.v + vmacf $v20, $v22, $v21.v + vmulf $v19, $v23, $v27.v + vmacf $v19, $v22, $v28.v + vadd $v20, $v20, $v20.v + bne $16, one, .lap_windowed_store + vadd $v19, $v19, $v19.v + vaddc $v20, $v20, $v20.v + vaddc $v19, $v19, $v19.v + +.lap_windowed_store: + + vor $v18, $v00, $v28 + sqv $v20, 0, 0, output_ptr + vor $v17, $v00, $v27 + ssv $v19, 0, 0, lap_ptr + ssv $v19, 2, -2, lap_ptr + addiu samples_left, samples_left, -8 + vmulf $v28, $v18, $v26.v + addiu output_ptr, output_ptr, 16 + vmacf $v28, $v17, $v24.v + ssv $v19, 4, -4, lap_ptr + addiu transform_ptr, transform_ptr, 16 + vmulf $v27, $v18, $v25.v + ssv $v19, 6, -6, lap_ptr + ssv $v19, 8, -8, lap_ptr + vmacf $v27, $v17, $v26.v + vadd $v28, $v28, $v28.v + ssv $v19, 10, -10, lap_ptr + ssv $v19, 12, -12, lap_ptr + ssv $v19, 14, -14, lap_ptr + vadd $v27, $v27, $v27.v + bnez samples_left, .lap_windowed_next + + addiu lap_ptr, lap_ptr, -16 + + jr $31 + nop + +#undef lap_ptr +#undef transform_ptr +#undef output_ptr +#undef samples_left +#undef load_a +#undef one +#undef load_b + + +# ------------------------------------------------------------------------------ +# Rotation and twiddle helpers +# ------------------------------------------------------------------------------ + +# Load and broadcast the {wc,ws} rotation step for a power-of-two size. +# The table address walk and scalar-to-vector transfers are scheduled as one +# leaf operation instead of separate RSPL helpers. +# Input: $2 = size. +# Outputs: v2 = wc, v1 = ws. +# Clobbers: $2, $3, v2, v1. + +load_rotation_step: + addiu $3, $0, %lo(ROTATIONS) - 8 + +.rotation_step_address: + srl $2, $2, 1 + bnez $2, .rotation_step_address + addiu $3, $3, 8 + + lsv $v02.e0, 4, $3 + lsv $v01.e0, 6, $3 + vor $v02, $v00, $v02.e0 + jr $31 + vor $v01, $v00, $v01.e0 + + +# Build four consecutive rotations in paired lanes and a four-step jump. +# Inputs: s3 = initial size, v28/v27 = step c/s. +# Outputs: v26/v25 = c/s, v24/v23 = jump c/s. +# Clobbers: t3, t2, v22..v18. + +load_rotation_4: + or $11, $19, $0 + addiu $10, $0, %lo(ROTATIONS) - 8 +.rotation4_address: + srl $11, $11, 1 + bnez $11, .rotation4_address + addiu $10, $10, 8 + lh $11, 0($10) + lh $10, 2($10) + vxor $v21, $v00, $v00 + vxor $v25, $v00, $v00 + vsub $v20, $v00, $v27.v + + vxor $v22, $v00, $v00 + mtc2 $10, $v21.e0 + mtc2 $11, $v22.e0 + vxor $v26, $v00, $v00 + vor $v21, $v00, $v21.e0 + vor $v22, $v00, $v22.e0 + vmov $v25.e0, $v21.e0 + vmov $v26.e1, $v22.e0 + + vor $v19, $v00, $v22 + vor $v18, $v00, $v21 + vmov $v26.e0, $v22.e0 + vmov $v25.e1, $v21.e0 + vmulf $v22, $v19, $v28.v + vmacf $v22, $v18, $v20.v + vmulf $v21, $v19, $v27.v + vmacf $v21, $v18, $v28.v + vadd $v22, $v22, $v22.v + vadd $v21, $v21, $v21.v + vmov $v26.e2, $v22.e0 + vmov $v26.e3, $v22.e0 + + vor $v19, $v00, $v22 + vor $v18, $v00, $v21 + vmov $v25.e2, $v21.e0 + vmov $v25.e3, $v21.e0 + vmulf $v22, $v19, $v28.v + vmacf $v22, $v18, $v20.v + vmulf $v21, $v19, $v27.v + vmacf $v21, $v18, $v28.v + vadd $v22, $v22, $v22.v + vadd $v21, $v21, $v21.v + vmov $v26.e4, $v22.e0 + + vor $v19, $v00, $v22 + vmov $v25.e4, $v21.e0 + vor $v18, $v00, $v21 + vmov $v26.e5, $v22.e0 + vmov $v25.e5, $v21.e0 + vmulf $v22, $v19, $v28.v + vmacf $v22, $v18, $v20.v + vmulf $v21, $v19, $v27.v + vmacf $v21, $v18, $v28.v + vadd $v22, $v22, $v22.v + + vxor $v24, $v00, $v31.e1 + vadd $v21, $v21, $v21.v + vxor $v23, $v00, $v00 + vmov $v26.e7, $v22.e0 + vmov $v26.e6, $v22.e0 + +# Advance the jump rotation four times. + vor $v19, $v00, $v24 + vor $v18, $v00, $v23 + vmov $v25.e6, $v21.e0 + vmov $v25.e7, $v21.e0 + vmulf $v24, $v19, $v28.v + vmacf $v24, $v18, $v20.v + vmulf $v23, $v19, $v27.v + vmacf $v23, $v18, $v28.v + vadd $v24, $v24, $v24.v + vadd $v23, $v23, $v23.v + + vor $v19, $v00, $v24 + vor $v18, $v00, $v23 + vmulf $v24, $v19, $v28.v + vmacf $v24, $v18, $v20.v + vmulf $v23, $v19, $v27.v + vmacf $v23, $v18, $v28.v + vadd $v24, $v24, $v24.v + vadd $v23, $v23, $v23.v + + vor $v19, $v00, $v24 + vor $v18, $v00, $v23 + vmulf $v24, $v19, $v28.v + vmacf $v24, $v18, $v20.v + vmulf $v23, $v19, $v27.v + vmacf $v23, $v18, $v28.v + vadd $v24, $v24, $v24.v + vadd $v23, $v23, $v23.v + + vor $v19, $v00, $v24 + vor $v18, $v00, $v23 + vmulf $v24, $v19, $v28.v + vmacf $v24, $v18, $v20.v + vmulf $v23, $v19, $v27.v + vmacf $v23, $v18, $v28.v + vadd $v24, $v24, $v24.v + jr $31 + vadd $v23, $v23, $v23.v + + +# Fixed register layout shared by the pre- and post-twiddle passes: +# s2/s1 = address/end; v26/v25 = c/s; v24/v23 = jump c/s; +# v22 = -jump s. +# Both passes clobber s2 and v21..v16. post_twiddle tail-calls +# swap_odd_reverse and therefore also clobbers t8, t7, t6, v29..v26. + + +pre_twiddle: +.pre_twiddle_next: + vsub $v16, $v00, $v25.v + lqv $v21, 0, 0, $18 + vor $v20, $v00, $v25 + vmulf $v18, $v26, $v21.q0 + vmacf $v18, $v25, $v21.q1 + vmulf $v17, $v26, $v21.q1 + vmacf $v17, $v16, $v21.q0 + vor $v21, $v00, $v26 + vadd $v18, $v18, $v18.v + vadd $v17, $v17, $v17.v + vmulf $v26, $v21, $v24.v + vmacf $v26, $v20, $v22.v + vmulf $v25, $v21, $v23.v + vmacf $v25, $v20, $v24.v + vmrg $v21, $v18, $v17 + vadd $v26, $v26, $v26.v + addiu $18, $18, 16 + vadd $v25, $v25, $v25.v + bne $18, $17, .pre_twiddle_next + sqv $v21, 0, -16, $18 + + jr $31 + nop + +post_twiddle: +.post_twiddle_next: + vsub $v16, $v00, $v25.v + lqv $v21, 0, 0, $18 + vor $v20, $v00, $v25 + vmulf $v18, $v26, $v21.q0 + vmacf $v18, $v25, $v21.q1 + vmulf $v17, $v26, $v21.q1 + vmacf $v17, $v16, $v21.q0 + vor $v21, $v00, $v26 + vadd $v18, $v18, $v18.v + vadd $v17, $v17, $v17.v + vmulf $v26, $v21, $v24.v + vmacf $v26, $v20, $v22.v + vsub $v17, $v00, $v17.v + vmulf $v25, $v21, $v23.v + vmacf $v25, $v20, $v24.v + vadd $v26, $v26, $v26.v + vmrg $v21, $v18, $v17 + addiu $18, $18, 16 + vadd $v25, $v25, $v25.v + bne $18, $17, .post_twiddle_next + sqv $v21, 0, -16, $18 + + # This pass is always followed by reverse unpacking. Preserve ra and + # tail-call the helper directly. + j swap_odd_reverse + nop + + +# ------------------------------------------------------------------------------ +# Final overlap assembly helper +# ------------------------------------------------------------------------------ + +# Append the high output half staged in LAP to the low half in WORK. +# fp = source, k1 = destination, sp = destination end. +# Clobbers: fp, k1, v28. +# Every supported subblock contributes at least one 16-byte vector. + +copy_lap_high: +.copy_lap_high_next: + lqv $v28, 0, 0, $30 + addiu $27, $27, 16 + addiu $30, $30, 16 + bne $27, $29, .copy_lap_high_next + sqv $v28, 0, -16, $27 + + jr $31 + nop + + +# ------------------------------------------------------------------------------ +# Stereo interleave command +# Interleaves the stereo samples from mid/side planar blocks and performs +# output scalind. +# ------------------------------------------------------------------------------ + +#define mid_rdram a0 +#define side_rdram a1 +#define stereo_rdram a2 + +stereo_interleave: + # Reconstruct and interleave four independent 256-sample chunks. + or s5, zero, stereo_rdram + or s6, zero, side_rdram + or s7, zero, mid_rdram + addiu s3, zero, 4 + addiu t3, zero, 0x55 + ctc2 t3, COP2_CTRL_VCC + +.stereo_chunk: + # Load 256 side and mid samples into separate DMEM buffers. + addiu t0, zero, 511 + ori s4, zero, %lo(WORK) + addiu t2, zero, 12 + jal DMAExec + or s0, zero, s6 + addiu t0, zero, 511 + ori s4, zero, %lo(OUTPUT) + addiu t2, zero, 12 + jal DMAExec + or s0, zero, s7 + + # LAP is independent output staging, so neither input can be overwritten. + addiu t8, zero, %lo(OUTPUT) + # Each iteration stores the previous result while computing the current one. + # The first dummy store lands in OUTPUT's unused tail; the 33rd iteration + # stores the final real vector while computing one harmless dummy result. + addiu t7, zero, %lo(LAP) - 32 + addiu t6, zero, 33 +.stereo_vectors: + lqv $v28, 0, 0, t8 + lqv $v27, 0, -1024, t8 + vadd $v26, $v28, $v27.v + vsub $v25, $v28, $v27.v + slv $v24, 0, 0, t7 + slv $v23, 0, 4, t7 + slv $v24, 4, 8, t7 + vadd $v26, $v26, $v26.v + slv $v23, 4, 12, t7 + vadd $v25, $v25, $v25.v + slv $v24, 8, 16, t7 + slv $v23, 8, 20, t7 + slv $v24, 12, 24, t7 + slv $v23, 12, 28, t7 + vor $v23, $v00, $v26.q1 + addiu t6, t6, -1 + vor $v24, $v00, $v26.q0 + addiu t7, t7, 32 + addiu t8, t8, 16 + vmrg $v23, $v23, $v25.q1 + bnez t6, .stereo_vectors + vmrg $v24, $v24, $v25.q0 + +.stereo_vectors_end: + # Commit this interleaved chunk, then advance all RDRAM cursors. + addiu t0, zero, 1023 + ori s4, zero, %lo(LAP) + addiu t2, zero, -32756 + jal DMAExec + or s0, zero, s5 + addiu s5, s5, 1024 + addiu s6, s6, 512 + addiu s3, s3, -1 + bnez s3, .stereo_chunk + addiu s7, s7, 512 + j RSPQ_Loop + nop + +#undef mid_rdram +#undef side_rdram +#undef stereo_rdram \ No newline at end of file diff --git a/src/audio/samplebuffer.c b/src/audio/samplebuffer.c index 622508616..bf1849d13 100644 --- a/src/audio/samplebuffer.c +++ b/src/audio/samplebuffer.c @@ -6,53 +6,182 @@ */ #include "mixer.h" +#include "mixer_internal.h" #include "samplebuffer.h" #include "n64sys.h" #include "n64types.h" +#include "dma.h" #include "utils.h" #include "debug.h" +#include "profile.h" +#include "rspq.h" #include /** @brief Set to 1 to activate debug logs */ #define MIXER_TRACE 0 +///@cond #if MIXER_TRACE -/** @brief like debugf(), but writes only if #MIXER_TRACE is not 0 */ #define tracef(fmt, ...) debugf(fmt, ##__VA_ARGS__) #else -/** @brief like debugf(), but writes only if #MIXER_TRACE is not 0 */ #define tracef(fmt, ...) ({ }) #endif +///@endcond -void samplebuffer_init(samplebuffer_t *buf, uint8_t* uncached_mem, int nbytes) { +static inline uint8_t *samplebuffer_base(const samplebuffer_t *buf) { + return SAMPLES_PTR(buf); +} + +/** Round a unit count so that count * unit_bytes is a multiple of 8. */ +static int samplebuffer_align_len(const samplebuffer_t *buf, int len) { + int ub = buf->unit_bytes; + assert(ub > 0); + if (ub & (ub - 1)) + return ROUND_UP(len, 8); + // PCM: ub is 1, 2, or 4 → ROUND_UP(len << bps, 8) >> bps (no division). + int bps = ub == 1 ? 0 : (ub == 2 ? 1 : 2); + return ((len + ((8 >> bps) - 1)) >> (3 - bps)) << (3 - bps); +} + +extern inline int samplebuffer_margin_units(int append_units); +extern inline int samplebuffer_align_units(const samplebuffer_t *buf); + +void samplebuffer_dma_wait(samplebuffer_t *buf) { + if (buf->dma_ticket) { + dma_wait_finished(buf->dma_ticket); + buf->dma_ticket = 0; + } +} + +/** + * Copy exactly @p nbytes inside the sample area. + * + * A waveform that declares #waveform_t::rsp_written produces samples through + * the RSP queue: the copy has to travel through the same queue so it lands + * after them. Every other producer writes with the CPU or a PI transfer, so + * the bytes are already in RDRAM and a memcpy is enough (after any PI DMA). + */ +static void samplebuffer_copy(samplebuffer_t *buf, uint8_t *dst, uint8_t *src, int nbytes) { + if (buf->wave && buf->wave->rsp_written) { + // The queued copy moves whole 8-byte blocks, and the bytes past + // nbytes at the destination are not ours to change: stage them at the + // source so that the tail of the copy rewrites them unchanged. + int n8 = ROUND_UP(nbytes, 8); + if (n8 > nbytes) + memcpy(src + nbytes, dst + nbytes, n8 - nbytes); + if (__mixer_rdram_copy(dst, src, n8)) + return; + } + samplebuffer_dma_wait(buf); + memcpy(dst, src, nbytes); +} + +/** Flush a pending spill from the margin back to the start of the ring. */ +static void samplebuffer_unspill(samplebuffer_t *buf) { + if (buf->spill <= 0) + return; + uint8_t *base = samplebuffer_base(buf); + int nbytes = buf->spill * buf->unit_bytes; + buf->spill = 0; + // Exactly the spilled units: anything past them is a unit still live in + // the ring (for VADPCM, a different 9-byte frame). + samplebuffer_copy(buf, base, base + buf->size * buf->unit_bytes, nbytes); +} + +void samplebuffer_init(samplebuffer_t *buf, uint8_t* uncached_mem, int nbytes, int state_size) { memset(buf, 0, sizeof(samplebuffer_t)); - // Store the buffer pointer as uncached address. We don't want to access - // it with CPU as we want to build samples with RSP, and all APIs assume - // that content is committed to RDRAM (not cache). - assertf(UncachedAddr(uncached_mem) == uncached_mem, + assertf(UncachedAddr(uncached_mem) == uncached_mem, "specified buffer must be in the uncached segment.\nTry using malloc_uncached() to allocate it"); - buf->ptr_and_flags = (uint32_t)uncached_mem; + buf->ptr_and_flags = (uint32_t)(uintptr_t)uncached_mem; assert((buf->ptr_and_flags & 7) == 0); - buf->size = nbytes; + // size is in units, so it stays 0 until the unit size is known. + buf->capacity_bytes = nbytes; + buf->margin_units = SAMPLEBUFFER_MARGIN_UNITS; + + if (state_size) { + buf->state = uncached_mem + nbytes; + buf->state_size = state_size; + } + buf->wnext = -1; } -void samplebuffer_set_bps(samplebuffer_t *buf, int bits_per_sample) { - assert(bits_per_sample == 8 || bits_per_sample == 16 || bits_per_sample == 32); - assertf(buf->widx == 0 && buf->ridx == 0 && buf->wpos == 0, - "samplebuffer_set_bps can only be called on an empty samplebuffer"); +/** + * Recompute the usable size in units, from the allocated bytes, the unit size + * and the margin. The samplebuffer must be empty. + */ +static void samplebuffer_recalc_size(samplebuffer_t *buf) { + int ub = buf->unit_bytes; + assert(ub > 0); + + int margin = samplebuffer_margin_units(buf->append_units); + buf->margin_units = margin; + + int usable = buf->capacity_bytes - margin * ub; + assertf(usable >= ub, "samplebuffer too small for margin"); + + // size * unit_bytes must be a multiple of 8, so that a waveform position + // keeps its byte phase when it wraps around the samplebuffer. + int n; + if ((ub & (ub - 1)) == 0) { + int bps = ub == 1 ? 0 : (ub == 2 ? 1 : 2); + n = (usable & ~7) >> bps; + } else { + n = usable / ub; + while (n > 0 && ((n * ub) & 7)) + n--; + } + assertf(n >= SAMPLEBUFFER_MARGIN_UNITS, "samplebuffer too small"); + + // Changing the modulus invalidates the physical position of any window the + // RSP may still be reading. A full highpri sync is the safe barrier; the + // samplebuffer itself does not track rounds. The first configuration of a + // buffer has handed out no window yet, so it never syncs. + if (buf->size > 0 && n != buf->size) { + rspq_highpri_sync(); + // No old window is live after the sync, so restart at the aligned base. + // Keeping head here would also reinterpret an index from the old ring + // modulus as an index in the new one. + buf->head = 0; + } - int nbytes = buf->size << SAMPLES_BPS_SHIFT(buf); + buf->size = n; + // The buffer is empty, but head is where the next stream restarts, and the + // ring it indexes just changed size. Put it back in the phase an RSP + // producer needs: a waveform that does not write through the RSP can have + // left it anywhere. + buf->head %= n; + if (buf->wave && buf->wave->rsp_written) + buf->head = samplebuffer_align_len(buf, buf->head) % n; +} - int bps = bits_per_sample == 8 ? 0 : (bits_per_sample == 16 ? 1 : 2); - buf->ptr_and_flags = SAMPLES_PTR_MAKE(SAMPLES_PTR(buf), bps); - buf->size = nbytes >> bps; +void samplebuffer_set_unit_bytes(samplebuffer_t *buf, int unit_bytes) { + assert(unit_bytes > 0); + assertf(buf->widx == 0 && buf->ridx == 0 && buf->wpos == 0, + "samplebuffer_set_unit_bytes can only be called on an empty samplebuffer"); + + // head is an index in units. It cannot be carried across a unit-size + // change: the same numeric index denotes a different byte address, and an + // address aligned for stereo PCM can be unaligned for mono PCM. Drain old + // windows and restart the newly configured ring at its aligned base. + if (buf->unit_bytes && buf->unit_bytes != unit_bytes) { + rspq_highpri_sync(); + buf->head = 0; + buf->size = 0; + } + buf->unit_bytes = (uint8_t)unit_bytes; + samplebuffer_recalc_size(buf); } -void samplebuffer_set_waveform(samplebuffer_t *buf, WaveformRead read, void *ctx) { +void samplebuffer_set_waveform(samplebuffer_t *buf, waveform_t *wave, WaveformRead read) { + buf->wave = wave; buf->wv_read = read; - buf->wv_ctx = ctx; + assert(wave->state_size <= buf->state_size); + + buf->append_units = wave->append_units; + if (buf->unit_bytes && buf->widx == 0 && buf->ridx == 0) + samplebuffer_recalc_size(buf); } bool samplebuffer_is_inited(samplebuffer_t *buf) @@ -61,186 +190,207 @@ bool samplebuffer_is_inited(samplebuffer_t *buf) } void samplebuffer_close(samplebuffer_t *buf) { + samplebuffer_dma_wait(buf); void *ptr = SAMPLES_PTR(buf); if (ptr) free_uncached(ptr); memset(buf, 0, sizeof(samplebuffer_t)); } +/** Physical slot for relative index `rel` (0 = wpos). */ +static inline int samplebuffer_slot(const samplebuffer_t *buf, int rel) { + return (buf->head + rel) % buf->size; +} + +/** @brief Units left ahead that trigger a refill. + * + * A WaveformRead costs mostly per call, so refilling late (in bigger chunks) + * is cheaper; refilling too late means the mixer has to fetch synchronously + * and wait for the transfer. */ +#define SAMPLEBUFFER_PREFETCH_LOW_UNITS 32 + void* samplebuffer_get(samplebuffer_t *buf, int wpos, int *wlen) { - // ROUNDUP8_BPS rounds up the specified number of samples - // (given the bps shift) so that they span an exact multiple - // of 8 bytes. This will be applied to the number of samples - // requested to wv_read(), to make sure that we always - // keep the sample buffer filled with a multiple of 8 bytes. - // This is not strictly required because dma_read() can do wonders, - // but it results in slightly faster DMA transfers and its almost free - // to do here. - /// @cond - #define ROUNDUP8_BPS(nsamples, bps) \ - (((nsamples)+((8>>(bps))-1)) >> (3-(bps)) << (3-(bps))) - /// @endcond - - int bps = SAMPLES_BPS_SHIFT(buf); + void *ret = NULL; + PROFILE_SCOPE(PS_SBUF_GET) { + int ub = buf->unit_bytes; + int ring = buf->size; + int round_len = samplebuffer_align_len(buf, *wlen); tracef("samplebuffer_get: wpos=%x wlen=%x\n", wpos, *wlen); - if (buf->widx == 0 || wpos < buf->wpos || wpos > buf->wpos+buf->widx) { - // If the requested position is totally outside - // the existing range (and not even consecutive), - // Flush the buffer and decode from scratch. - // Normally this is because of seeking, but might - // also be just because we discarded all the contents - // of the buffer, so be sure to set seeking only if - // the position is different from what we expected. - tracef("samplebuffer_get: flushing buffer: buf->widx=%x buf->wpos=%x buf->wnext=%x\n", buf->widx, buf->wpos, buf->wnext); - bool seeking = wpos != buf->wnext; + if (buf->widx == 0 || wpos < buf->wpos || wpos > buf->wpos + buf->widx) { + tracef("samplebuffer_get: seek/flush wpos=%x (had %x+%x)\n", wpos, buf->wpos, buf->widx); + assertf(buf->wv_read, "samplebuffer_get: no reader to seek to %x (had %x+%x)", + wpos, buf->wpos, buf->widx); + bool seeking = (buf->wnext < 0) || (wpos != buf->wnext); + // Discard any pending spill with the flush: copying it back would + // rewrite samples that this seek is about to throw away. samplebuffer_flush(buf); buf->wpos = wpos; - // Avoid setting a position that is odd, because it would case a - // 2-byte phase change in the sample buffer, which would make - // impossible to call dma_read. - int len = *wlen; - if ((buf->wpos << bps) & 1) { + int len = round_len; + // 8-bit PCM: dma_read only accepts a source and a destination of equal + // parity, so the ring slot the stream restarts on has to keep the phase + // of its waveform position (see samplebuffer_append). + if (ub == 1 && ((buf->head - buf->wpos) & 1)) { buf->wpos--; len++; } - buf->wv_read(buf->wv_ctx, buf, buf->wpos, ROUNDUP8_BPS(len, bps), seeking); + buf->wv_read(buf->wave->ctx, buf, buf->wpos, len, seeking); + samplebuffer_unspill(buf); buf->wnext = buf->wpos + buf->widx; } else { - // Record first sample that we still need to keep in the sample - // buffer. This is important to do now because decoder_read might - // push more samples than required into the buffer and force - // to compact the buffer. We thus need to know which samples - // are still required. + samplebuffer_unspill(buf); buf->ridx = wpos - buf->wpos; - - // Part of the requested samples are already in the sample buffer. - // Check how many we can reuse. For instance, if there's a waveform - // loop, the whole loop might already be in the sample buffer, so - // no further decoding is necessary. int reuse = buf->wpos + buf->widx - wpos; - - // If the existing samples are not enough, read the missing - // through the callback. if (reuse < *wlen) { tracef("samplebuffer_get: read missing: reuse=%x wpos=%x wlen=%x\n", reuse, wpos, *wlen); + assertf(buf->wv_read, "samplebuffer_get: no reader to extend to %x", wpos+reuse); assertf(wpos+reuse == buf->wnext, "wpos:%x reuse:%x buf->wnext:%x", wpos, reuse, buf->wnext); - buf->wv_read(buf->wv_ctx, buf, wpos+reuse, ROUNDUP8_BPS(*wlen-reuse, bps), false); + int missing = samplebuffer_align_len(buf, *wlen - reuse); + // Free space without discarding past the live read cursor. + missing = MIN(missing, ring - (buf->widx - buf->ridx)); + buf->wv_read(buf->wave->ctx, buf, wpos+reuse, missing, false); + samplebuffer_unspill(buf); buf->wnext = buf->wpos + buf->widx; } } - assertf(wpos >= buf->wpos && wpos < buf->wpos+buf->widx, + assertf(wpos >= buf->wpos && wpos < buf->wpos+buf->widx, "samplebuffer_get: logic error\n" "wpos:%x buf->wpos:%x buf->widx:%x", wpos, buf->wpos, buf->widx); - int idx = wpos - buf->wpos; - - // If the sample buffer contains less samples than requested, - // report that by updating *wlen. This will cause cracks in the - // audio as silence will be inserted by the mixer. - int len = buf->widx - idx; + int rel = wpos - buf->wpos; + buf->ridx = rel; + int len = buf->widx - rel; if (len < *wlen) *wlen = len; - return SAMPLES_PTR(buf) + (idx << SAMPLES_BPS_SHIFT(buf)); -} - -void* samplebuffer_append(samplebuffer_t *buf, int wlen) { - // If the requested number of samples doesn't fit the buffer, we - // need to make space for it by discarding older samples. - if (buf->widx + wlen > buf->size) { - // Make space in the buffer by discarding everything up to the - // ridx index, which is the first sample that we still need for playback. - assertf(buf->widx >= buf->ridx, - "samplebuffer_append: invalid consistency check\n" - "widx:%x ridx:%x\n", buf->widx, buf->ridx); - - // Rollback ridx until it hit a 8-byte aligned position. - // This preserves the guarantee that samplebuffer_append - // will always return a 8-byte aligned pointer, which is - // good for DMA purposes. - int ridx = buf->ridx; - while ((ridx << SAMPLES_BPS_SHIFT(buf)) & 7) - ridx--; - samplebuffer_discard(buf, buf->wpos+ridx); + int slot = samplebuffer_slot(buf, rel); + assertf(*wlen <= SAMPLEBUFFER_MARGIN_UNITS || slot + *wlen <= ring, + "samplebuffer_get: window %x not contiguous (slot=%x size=%x)", *wlen, slot, ring); + + // Window straddles the wrap: mirror the leading units into the margin so + // the RSP sees one linear DMA. wlen already includes the mixer's overread. + int need = slot + *wlen - ring; + if (need > 0) { + assertf(need <= buf->margin_units, + "samplebuffer_get: mirror %x > margin %x", need, buf->margin_units); + uint8_t *base = samplebuffer_base(buf); + samplebuffer_copy(buf, base + ring * ub, base, ROUND_UP(need * ub, 8)); } - assertf(((buf->wpos<< SAMPLES_BPS_SHIFT(buf)) % 2) == 0, "buf->wpos:%x", buf->wpos); + ret = samplebuffer_base(buf) + slot * ub; + } + return ret; +} - // If there is still not space in the buffer, it means that the - // buffer is too small for this append call. This is a logic error, - // so better assert right away. - // TODO: in principle, we could bubble this error up to the callers, - // let them fill less samples than requested, and obtain some cracks - // in the audio. Is it worth it? - assertf(buf->widx + wlen <= buf->size, - "samplebuffer_append: buffer too small\n" - "ridx:%x widx:%x wlen:%x size:%x", buf->ridx, buf->widx, wlen, buf->size); +void samplebuffer_prefetch(samplebuffer_t *buf, int wpos, int wlen) { + if (!buf->wave || !buf->wv_read || !buf->wave->async_read || + buf->widx == 0 || buf->wnext < 0) + return; + samplebuffer_unspill(buf); - void *data = SAMPLES_PTR(buf) + (buf->widx << SAMPLES_BPS_SHIFT(buf)); - buf->widx += wlen; - return data; -} + // Only extend the current stream: on a seek the samplebuffer is flushed, + // so anything fetched now would be thrown away. + if (wpos < buf->wpos || wpos > buf->wnext) + return; + // Fill back up to the watermark, so that one transfer covers several rounds. + int ahead = buf->wnext - wpos; + int missing = MAX(SAMPLEBUFFER_PREFETCH_UNITS, wlen) - ahead; + if (missing <= 0 || + (ahead > SAMPLEBUFFER_PREFETCH_LOW_UNITS && ahead >= wlen)) + return; + missing = samplebuffer_align_len(buf, missing); + // Never ask for more than a margin: readers split bigger requests into + // several appends, and since a buffer tracks a single in-flight transfer, + // the second append would have to wait for the first one here. + missing = MIN(missing, SAMPLEBUFFER_MARGIN_UNITS); + // Never fetch into space that a live window still needs. + missing = MIN(missing, buf->size - (buf->widx - buf->ridx)); + if (missing <= 0) + return; -void samplebuffer_undo(samplebuffer_t *buf, int wlen) { - assertf(buf->widx >= wlen, "samplebuffer_append_undo: invalid wlen:%x widx:%x", wlen, buf->widx); - buf->widx -= wlen; + // Leave the transfer in flight: samplebuffer_get will sync it. + PROFILE_SCOPE(PS_SBUF_GET) { + buf->wv_read(buf->wave->ctx, buf, buf->wnext, missing, false); + } + buf->wnext = buf->wpos + buf->widx; } -void samplebuffer_discard(samplebuffer_t *buf, int wpos) { - // Compute the index of the first sample that will be preserved (and thus - // will be moved to position 0 of the buffer). - int idx = wpos - buf->wpos; - if (idx <= 0) - return; - if (idx > buf->widx) - idx = buf->widx; - - // Make sure moving this sample at the beginning of the buffer doesn't change - // the 2-byte phase of the waveform address. This is not strictly required, - // but it helps waveform implementations that want to use dma_read(). - if ((idx << SAMPLES_BPS_SHIFT(buf)) & 1) { - idx--; - if (idx == 0) - return; +void* samplebuffer_append(samplebuffer_t *buf, int wlen) { + samplebuffer_unspill(buf); + + int ub = buf->unit_bytes; + int ring = buf->size; + // PCM: the ring slot and the waveform position must keep the same byte + // phase, as dma_read only accepts source and destination of equal parity + // (see raw_waveform_read). head and wpos move together on a discard, so + // the phase is preserved even when both become odd; a seek resets head + // and has to fix wpos up (see samplebuffer_get). + assertf((ub & (ub - 1)) != 0 || (((buf->head - buf->wpos) * ub) & 1) == 0, + "buf->wpos:%x buf->head:%x", buf->wpos, buf->head); + + // Make room by discarding the oldest units past the read cursor. + while (buf->widx + wlen > ring) { + int drop = buf->widx + wlen - ring; + assertf(drop <= buf->ridx, + "samplebuffer_append: buffer too small\n" + "ridx:%x widx:%x wlen:%x size:%x", buf->ridx, buf->widx, wlen, ring); + buf->wpos += drop; + buf->widx -= drop; + buf->ridx -= drop; + buf->head = (buf->head + drop) % ring; } - tracef("samplebuffer_discard: wpos=%x idx:%x buf->wpos=%x buf->widx=%x\n", wpos, idx, buf->wpos, buf->widx); - int kept_bytes = (buf->widx - idx) << SAMPLES_BPS_SHIFT(buf); - if (kept_bytes > 0) { - tracef("samplebuffer_discard: compacting buffer, moving 0x%x bytes\n", kept_bytes); - - // FIXME: this violates the zero-copy principle as we do a memmove here. - // The problem is that the RSP ucode doesn't fully support a circular - // buffer of samples (and also our samplebuffer_t isn't structured for - // this). Luckily, this is a rare chance and in most cases just a few - // samples are moved (in the normal playback case, it should be just 1, - // as in general a sample could be used more than once for resampling). - uint8_t *src = SAMPLES_PTR(buf) + (idx << SAMPLES_BPS_SHIFT(buf)); - uint8_t *dst = SAMPLES_PTR(buf); - assert(((uint32_t)dst & 7) == 0); - - // Optimized copy of samples. We work on uncached memory directly - // so that we don't need to flush, and use only 64-bits ops. We round up - // to a multiple of 8 the amount of bytes, as it doesn't matter if we - // copy more, as long as we're fast. - // This has been benchmarked to be faster than memmove() + cache flush. - kept_bytes = ROUND_UP(kept_bytes, 8); - u_uint64_t *src64 = (u_uint64_t*)src; - uint64_t *dst64 = (uint64_t*)dst; - for (int i=0;iwidx); + // Contiguous write: either fits before the usable end, or spills into the + // margin (≤ margin_units). + if (slot + wlen > ring) { + assertf(wlen <= buf->margin_units, + "samplebuffer_append: append of %x units wraps and does not fit the\n" + "margin (slot:%x margin:%x): declare it in waveform_t::append_units", + wlen, slot, buf->margin_units); + buf->spill = slot + wlen - ring; } - buf->wpos += idx; - buf->widx -= idx; - buf->ridx -= idx; - if (buf->ridx < 0) - buf->ridx = 0; + buf->widx += wlen; + uint8_t *ret = samplebuffer_base(buf) + slot * ub; + // An RSP producer writes through SP DMA, which ignores the low 3 bits of + // the RDRAM address: an unaligned slot would silently land elsewhere. + assertf(!buf->wave || !buf->wave->rsp_written || ((uintptr_t)ret & 7) == 0, + "samplebuffer_append: unaligned slot %x for an RSP-written waveform", slot); + return ret; +} + +void samplebuffer_undo(samplebuffer_t *buf, int wlen) { + samplebuffer_unspill(buf); + tracef("samplebuffer_undo: wlen=%x\n", wlen); + assertf(buf->widx >= wlen, "samplebuffer_undo: invalid wlen:%x widx:%x", wlen, buf->widx); + buf->widx -= wlen; + if (buf->wnext >= 0) + buf->wnext = buf->wpos + buf->widx; } void samplebuffer_flush(samplebuffer_t *buf) { + samplebuffer_dma_wait(buf); + // Discard any pending spill: the samples are being thrown away, and + // copying them back could overwrite a window a prior round still holds. + buf->spill = 0; + // Restart the ring where the last append left off, rather than rewinding it + // to the top. Rounds already emitted read from the window that was live + // here, and the RSP can still be behind them: the CPU is allowed to run + // that far ahead (#mixer_poll_async). Writing forward leaves the refill + // a whole turn of the ring away from those samples. + if (buf->size > 0) { + int head = buf->head + buf->widx; + // An RSP producer needs an 8-byte aligned destination (see + // #samplebuffer_append), and undoing a partial block (ULC trims the + // tail of a waveform, and a loop restarts right there) leaves the + // write cursor out of that phase. Restarting a few units further is + // as safe as restarting exactly where the stream stopped. + if (buf->wave && buf->wave->rsp_written) + head = samplebuffer_align_len(buf, head); + buf->head = head % buf->size; + } buf->wpos = buf->widx = buf->ridx = 0; buf->wnext = -1; } diff --git a/src/audio/sf64.c b/src/audio/sf64.c new file mode 100644 index 000000000..53f22df50 --- /dev/null +++ b/src/audio/sf64.c @@ -0,0 +1,121 @@ +/** + * @file sf64.c + * @author Giovanni Bajo + * @brief SF64 sound bank loader + * @ingroup mixer + */ +#include "sf64.h" +#include "sf64_internal.h" +#include "wav64.h" +#include "wav64_internal.h" +#include "debug.h" +#include "asset_internal.h" +#include +#include +#include +#include +#include + +sf64_bank_t *sf64_load(const char *fn) +{ + int fd = must_open(fn); + + sf64_header_t head; + read(fd, &head, sizeof(head)); + if (memcmp(head.magic, SF64_ID, 4) != 0) { + assertf(memcmp(head.magic, "RIFF", 4) != 0, + "cannot load SF2 file: %s\nPlease convert to SF64 with audioconv64", fn); + assertf(0, "cannot load SF64 file: %s\nFile corrupted", fn); + } + assertf(head.version == SF64_VERSION, + "cannot load SF64 file: %s\nInvalid version %d (expected %d); please convert again with audioconv64", + fn, head.version, SF64_VERSION); + + lseek(fd, head.metadata_offset, SEEK_SET); + int meta_sz = head.metadata_size; + void *meta = asset_loadfd(fd, &meta_sz); + + int tables = (int)head.num_presets * (int)sizeof(sf64_preset_t) + + (int)head.num_regions * (int)sizeof(sf64_region_t) + + (int)head.num_samples * (int)sizeof(sf64_sample_t); + assertf(meta_sz >= tables, "cannot load SF64 file: %s\nTruncated metadata", fn); + + sf64_bank_t *bank = malloc(sizeof(*bank) + head.num_samples * sizeof(wav64_t *)); + assert(bank); + bank->fd = fd; + bank->num_presets = head.num_presets; + bank->num_regions = head.num_regions; + bank->num_samples = head.num_samples; + bank->meta = meta; + bank->presets = (sf64_preset_t *)meta; + bank->regions = (sf64_region_t *)(bank->presets + head.num_presets); + bank->samples = (sf64_sample_t *)(bank->regions + head.num_regions); + + for (int i = 0; i < head.num_samples; i++) { + char name[128]; + snprintf(name, sizeof(name), "%s[%d]", fn, i); + lseek(fd, bank->samples[i].wav64_offset, SEEK_SET); + bank->waves[i] = wav64_loadfd(fd, name, NULL); + } + + return bank; +} + +void sf64_close(sf64_bank_t *bank) +{ + assert(bank); + for (int i = 0; i < bank->num_samples; i++) + wav64_close(bank->waves[i]); + close(bank->fd); + free(bank->meta); + free(bank); +} + +int sf64_find_preset(sf64_bank_t *bank, int midi_bank, int program) +{ + assert(bank); + for (int i = 0; i < bank->num_presets; i++) { + if (bank->presets[i].bank == midi_bank && bank->presets[i].program == program) + return i; + } + return -1; +} + +int sf64_preset_count(sf64_bank_t *bank) +{ + assert(bank); + return bank->num_presets; +} + +const char *sf64_preset_name(sf64_bank_t *bank, int index) +{ + assert(bank); + assertf(index >= 0 && index < bank->num_presets, + "sf64: preset index %d out of range (0..%d)", index, bank->num_presets - 1); + return (const char *)bank->meta + bank->presets[index].name_offset; +} + +void sf64_preset_id(sf64_bank_t *bank, int index, int *midi_bank, int *program) +{ + assert(bank); + assertf(index >= 0 && index < bank->num_presets, + "sf64: preset index %d out of range (0..%d)", index, bank->num_presets - 1); + if (midi_bank) *midi_bank = bank->presets[index].bank; + if (program) *program = bank->presets[index].program; +} + +int sf64_find_region(sf64_bank_t *bank, int preset_index, int key, int velocity) +{ + assert(bank); + assertf(preset_index >= 0 && preset_index < bank->num_presets, + "sf64: preset index %d out of range (0..%d)", preset_index, bank->num_presets - 1); + sf64_preset_t *p = &bank->presets[preset_index]; + for (int i = 0; i < p->num_regions; i++) { + int ri = p->first_region + i; + sf64_region_t *r = &bank->regions[ri]; + if (key >= r->key_min && key <= r->key_max && + velocity >= r->velocity_min && velocity <= r->velocity_max) + return ri; + } + return -1; +} diff --git a/src/audio/sf64_internal.h b/src/audio/sf64_internal.h new file mode 100644 index 000000000..fbca1ce50 --- /dev/null +++ b/src/audio/sf64_internal.h @@ -0,0 +1,135 @@ +/** + * @file sf64_internal.h + * @author Giovanni Bajo + * @brief On-disk layout and runtime state of the SF64 sound bank format + * @ingroup mixer + * + * SF64 is the host-converted form of a SoundFont 2 bank: resolved presets and + * regions plus embedded WAV64 samples. See tools/audioconv64/conv_sf64.cpp. + */ +#ifndef LIBDRAGON_SF64_INTERNAL_H +#define LIBDRAGON_SF64_INTERNAL_H + +#include + +///@cond +typedef struct wav64_s wav64_t; +///@endcond + +#define SF64_ID "SF64" ///< SF64 file identifier +#define SF64_VERSION 4 ///< On-disk format version + +/** Loop modes for #sf64_region_t::loop_mode (SF2 SampleModes). */ +#define SF64_LOOP_NONE 0 ///< No loop +#define SF64_LOOP_CONTINUOUS 1 ///< Continuous loop +#define SF64_LOOP_SUSTAIN 2 ///< Sustain loop + +/** + * @brief DAHDSR envelope in SF2 units (timecents). + * + * For the amp envelope, #sustain_gain is linear amplitude (1.0 = peak). + * For the mod envelope, it is the unit sustain level (1.0 = peak) after the + * SF2 sustain-attenuation conversion done by audioconv64. + */ +typedef struct __attribute__((packed)) { + int16_t delay_timecents; ///< Delay before attack (timecents) + int16_t attack_timecents; ///< Attack duration (timecents) + int16_t hold_timecents; ///< Hold duration (timecents) + int16_t decay_timecents; ///< Decay duration (timecents) + float sustain_gain; ///< Sustain level (1.0 = peak); see struct note + int16_t release_timecents; ///< Release duration (timecents) + int16_t keynum_to_hold; ///< Key-number scaling of hold (timecents/key) + int16_t keynum_to_decay; ///< Key-number scaling of decay (timecents/key) +} sf64_envelope_t; + +/** @brief File header (big-endian on disk). */ +typedef struct __attribute__((packed)) { + char magic[4]; ///< Magic (#SF64_ID) + uint8_t version; ///< Format version + uint8_t flags; ///< Reserved flags + uint16_t num_presets; ///< Number of presets + uint16_t num_regions; ///< Number of regions + uint16_t num_samples; ///< Number of embedded samples + uint32_t metadata_offset; ///< Offset of asset-compressed metadata + uint32_t metadata_size; ///< Compressed metadata size (for #asset_loadfd) + uint32_t sample_data_offset;///< Offset of the first embedded WAV64 +} sf64_header_t; + +/** @brief MIDI preset → contiguous region range. */ +typedef struct __attribute__((packed)) { + uint16_t bank; ///< MIDI bank number + uint8_t program; ///< MIDI program number + uint8_t flags; ///< Reserved flags + uint16_t first_region; ///< Index of the first region in the region table + uint16_t num_regions; ///< Number of contiguous regions + uint32_t name_offset; ///< Offset of the preset name in uncompressed metadata +} sf64_preset_t; + +/** @brief Resolved instrument zone (key/vel filtered). */ +typedef struct __attribute__((packed)) { + uint16_t sample_index; ///< Index into the sample table + uint8_t key_min; ///< Lowest MIDI key (inclusive) + uint8_t key_max; ///< Highest MIDI key (inclusive) + uint8_t velocity_min; ///< Lowest velocity (inclusive) + uint8_t velocity_max; ///< Highest velocity (inclusive) + uint8_t loop_mode; ///< Loop mode (#SF64_LOOP_NONE / CONTINUOUS / SUSTAIN) + uint8_t exclusive_group; ///< Exclusive class (0 = none) + int8_t root_key; ///< Root key / overriding root key + int8_t coarse_tune; ///< Coarse tune in semitones + int16_t fine_tune; ///< Fine tune in cents + int16_t pitch_keytrack; ///< Pitch key tracking (cents per key, SF2 ScaleTuning) + int16_t pan; ///< Pan in SF2 units (−500…500) + float gain; ///< Initial attenuation as linear gain (1.0 = full) + sf64_envelope_t amp_env; ///< Amplitude envelope + sf64_envelope_t mod_env; ///< Modulation envelope (pitch / filter destinations) + int16_t mod_env_to_pitch; ///< Pitch at mod-env peak (cents) + uint16_t reserved_flags; ///< Reserved flags for future generators +} sf64_region_t; + +/** @brief Embedded WAV64 sample variant. */ +typedef struct __attribute__((packed)) { + uint32_t wav64_offset; ///< Absolute file offset of the WAV64 blob + uint32_t wav64_size; ///< Size of the WAV64 blob in bytes + uint64_t pcm_hash; ///< CRC-64 of the PCM variant (for deduplication) + uint32_t sample_start; ///< Start sample (always 0 in v1; WAV64 already cropped) + uint32_t sample_end; ///< End sample (exclusive length of the waveform) + uint32_t loop_start; ///< Inclusive loop start (0 if no loop) + uint32_t loop_end; ///< Exclusive loop end (0 if no loop) + uint32_t sample_rate; ///< Native sample rate in Hz + uint8_t channels; ///< Channel count (1 = mono) + uint8_t flags; ///< Reserved flags + uint16_t reserved; ///< Reserved +} sf64_sample_t; + +/** + * @brief Loaded SF64 bank (opaque to API users). + * + * Immutable after #sf64_load. Safe to share across synthesizers. + */ +typedef struct sf64_bank_s { + int fd; ///< Open SF64 file (shared by embedded WAV64s) + uint16_t num_presets; ///< Number of presets + uint16_t num_regions; ///< Number of regions + uint16_t num_samples; ///< Number of embedded samples + sf64_preset_t *presets; ///< Preset table (into #meta) + sf64_region_t *regions; ///< Region table (into #meta) + sf64_sample_t *samples; ///< Sample table (into #meta) + void *meta; ///< Decompressed metadata blob + wav64_t *waves[]; ///< Embedded waveforms (#num_samples) +} sf64_bank_t; + +#ifdef __cplusplus +static_assert(sizeof(sf64_header_t) == 24, "invalid sf64_header size"); +static_assert(sizeof(sf64_preset_t) == 12, "invalid sf64_preset size"); +static_assert(sizeof(sf64_envelope_t) == 18, "invalid sf64_envelope size"); +static_assert(sizeof(sf64_region_t) == 60, "invalid sf64_region size"); +static_assert(sizeof(sf64_sample_t) == 40, "invalid sf64_sample size"); +#else +_Static_assert(sizeof(sf64_header_t) == 24, "invalid sf64_header size"); +_Static_assert(sizeof(sf64_preset_t) == 12, "invalid sf64_preset size"); +_Static_assert(sizeof(sf64_envelope_t) == 18, "invalid sf64_envelope size"); +_Static_assert(sizeof(sf64_region_t) == 60, "invalid sf64_region size"); +_Static_assert(sizeof(sf64_sample_t) == 40, "invalid sf64_sample size"); +#endif + +#endif diff --git a/src/audio/sf64_midi.c b/src/audio/sf64_midi.c new file mode 100644 index 000000000..4494c663d --- /dev/null +++ b/src/audio/sf64_midi.c @@ -0,0 +1,330 @@ +/** + * @file sf64_midi.c + * @author Giovanni Bajo + * @brief MIDI control plane for the SF64 synthesizer + * @ingroup mixer + * + * Implements #midi_target_t callbacks, per-channel controllers, Channel Mode + * Messages (CC120/121/123), RPN pitch-bend sensitivity, bank select, Native/GM1 + * mode, and GM1 System On. Voice lifecycle stays in sf64_synth.c. + */ +#include "sf64_synth_internal.h" +#include "debug.h" +#include +#include + +static void midi_all_sound_off(sf64_synth_t *s, int midi_channel) +{ + if (s->num_channels <= 0) + return; + for (int ch = s->first_channel; ch < s->first_channel + s->num_channels; ch++) { + if (s->voices[ch].midi_channel == midi_channel) + voice_stop(s, ch); + } +} + +static void midi_all_notes_off(sf64_synth_t *s, int midi_channel) +{ + if (s->num_channels <= 0) + return; + bool pedal = s->midi[midi_channel].sustain >= 64; + for (int ch = s->first_channel; ch < s->first_channel + s->num_channels; ch++) { + sf64_voice_t *v = &s->voices[ch]; + if (v->midi_channel != midi_channel || !voice_key_down(v)) + continue; + if (pedal) { + v->key_released = true; + v->held_by_sustain = true; + } else { + voice_enter_release(s, ch); + } + } +} + +static void midi_rpn_apply_data_entry(sf64_synth_t *s, int midi_channel) +{ + sf64_midi_channel_t *mc = &s->midi[midi_channel]; + if (mc->rpn_msb != 0 || mc->rpn_lsb != 0) + return; + int cents = (int)mc->data_entry_msb * 100 + (int)mc->data_entry_lsb; + if (cents > 12700) cents = 12700; + mc->pitch_range_cents = (int16_t)cents; + if (s->num_channels > 0) + midi_apply_bend(s, midi_channel); +} + +static void midi_channel_reset_program(sf64_synth_t *s, int midi_channel) +{ + sf64_midi_channel_t *mc = &s->midi[midi_channel]; + mc->bank = (midi_channel == SF64_DRUM_CHANNEL) ? SF64_DRUM_BANK : 0; + mc->bank_msb = 0; + mc->bank_lsb = 0; + mc->program = 0; + mc->preset_index = (int16_t)sf64_find_preset(s->bank, mc->bank, 0); +} + +static void midi_channel_reset_controllers(sf64_synth_t *s, int midi_channel) +{ + sf64_midi_channel_t *mc = &s->midi[midi_channel]; + bool pedal = mc->sustain >= 64; + mc->expression = 127; + mc->pan = 64; + mc->modulation = 0; + mc->sustain = 0; + mc->rpn_msb = 0x7F; + mc->rpn_lsb = 0x7F; + mc->data_entry_msb = 0; + mc->data_entry_lsb = 0; + mc->pitch_bend = 8192; + mc->pitch_range_cents = 200; + if (s->num_channels <= 0) + return; + if (pedal) { + for (int ch = s->first_channel; ch < s->first_channel + s->num_channels; ch++) { + sf64_voice_t *v = &s->voices[ch]; + if (v->midi_channel == midi_channel && v->held_by_sustain) + voice_enter_release(s, ch); + } + } + midi_apply_vol(s, midi_channel); + midi_apply_bend(s, midi_channel); +} + +static void midi_channel_reset_all(sf64_synth_t *s, int midi_channel) +{ + s->midi[midi_channel].volume = 127; + midi_channel_reset_program(s, midi_channel); + midi_channel_reset_controllers(s, midi_channel); +} + +/** Reset every MIDI channel's program and controllers to defaults. */ +void midi_channels_reset(sf64_synth_t *synth) +{ + for (int i = 0; i < SF64_MIDI_CHANNELS; i++) + midi_channel_reset_all(synth, i); +} + +static void sf64_mt_note_on(midi_target_t *t, int ch, int key, int vel, int64_t now) +{ + (void)now; + sf64_synth_note_on((sf64_synth_t *)t, ch, key, vel); +} + +static void sf64_mt_note_off(midi_target_t *t, int ch, int key, int vel, int64_t now) +{ + (void)vel; (void)now; + sf64_synth_note_off((sf64_synth_t *)t, ch, key); +} + +static void sf64_mt_cc(midi_target_t *t, int ch, int cc, int value, int64_t now) +{ + (void)now; + sf64_synth_t *s = (sf64_synth_t *)t; + sf64_midi_channel_t *mc = &s->midi[ch]; + switch (cc) { + case 7: sf64_synth_set_volume(s, ch, value); break; + case 11: sf64_synth_set_expression(s, ch, value); break; + case 10: sf64_synth_set_pan(s, ch, value); break; + case 64: sf64_synth_set_sustain(s, ch, value); break; + case 0: + mc->bank_msb = (uint8_t)value; + // GM1: melodic presets stay on bank 0; drum channel stays on 128. + if (s->mode == SF64_MODE_NATIVE) + mc->bank = (uint16_t)value; + break; + case 32: + mc->bank_lsb = (uint8_t)value; + break; + case 6: + mc->data_entry_msb = (uint8_t)value; + midi_rpn_apply_data_entry(s, ch); + break; + case 38: + mc->data_entry_lsb = (uint8_t)value; + midi_rpn_apply_data_entry(s, ch); + break; + case 100: + mc->rpn_lsb = (uint8_t)value; + break; + case 101: + mc->rpn_msb = (uint8_t)value; + break; + case 120: midi_all_sound_off(s, ch); break; + case 121: midi_channel_reset_controllers(s, ch); break; + case 123: midi_all_notes_off(s, ch); break; + } +} + +static void sf64_mt_program(midi_target_t *t, int ch, int program, int64_t now) +{ + (void)now; + sf64_synth_t *s = (sf64_synth_t *)t; + if (s->mode == SF64_MODE_GM1 && ch == SF64_DRUM_CHANNEL) + return; + int bank = (s->mode == SF64_MODE_GM1) ? 0 : (int)s->midi[ch].bank; + sf64_synth_set_program(s, ch, bank, program); +} + +static void sf64_mt_pitch_bend(midi_target_t *t, int ch, int value, int64_t now) +{ + (void)now; + sf64_synth_set_pitch_bend((sf64_synth_t *)t, ch, value); +} + +static void sf64_mt_reset(midi_target_t *t, int64_t now) +{ + sf64_synth_t *s = (sf64_synth_t *)t; + voices_stop_all(s); + midi_channels_reset(s); + s->now = now; +} + +static void sf64_mt_system_reset(midi_target_t *t, midi_system_t system, int64_t now) +{ + sf64_synth_t *s = (sf64_synth_t *)t; + assert(system == MIDI_SYSTEM_GM1); + s->mode = SF64_MODE_GM1; + sf64_mt_reset(t, now); +} + +static void sf64_mt_finish(midi_target_t *t, int64_t now) +{ + sf64_synth_t *s = (sf64_synth_t *)t; + (void)now; + for (int i = 0; i < SF64_MIDI_CHANNELS; i++) + s->midi[i].sustain = 0; + if (s->num_channels <= 0) + return; + for (int ch = s->first_channel; ch < s->first_channel + s->num_channels; ch++) { + if (voice_sounding(&s->voices[ch])) + voice_enter_release(s, ch); + } +} + +static int64_t sf64_mt_process(midi_target_t *t, int64_t now) +{ + sf64_synth_t *s = (sf64_synth_t *)t; + int64_t elapsed = now - s->now; + assertf(elapsed >= 0, "sf64 midi_target: clock went backwards"); + assertf(elapsed <= INT_MAX, "sf64 midi_target: process span too large"); + int next = sf64_synth_process(s, (int)elapsed); + if (next < 0) return INT64_MAX; + return s->now + next; +} + +const midi_target_ops_t sf64_midi_ops = { + .note_on = sf64_mt_note_on, + .note_off = sf64_mt_note_off, + .control_change = sf64_mt_cc, + .program_change = sf64_mt_program, + .pitch_bend = sf64_mt_pitch_bend, + .poly_pressure = NULL, + .channel_pressure = NULL, + .finish = sf64_mt_finish, + .reset = sf64_mt_reset, + .system_reset = sf64_mt_system_reset, + .process = sf64_mt_process, +}; + +midi_target_t *sf64_synth_midi_target(sf64_synth_t *synth) +{ + assert(synth); + return &synth->midi_target; +} + +void sf64_synth_set_mode(sf64_synth_t *synth, sf64_synth_mode_t mode) +{ + assert(synth); + synth->mode = mode; +} + +sf64_synth_mode_t sf64_synth_get_mode(sf64_synth_t *synth) +{ + assert(synth); + return synth->mode; +} + +bool sf64_synth_set_program(sf64_synth_t *synth, int midi_channel, + int bank, int program) +{ + assert(synth); + assert(midi_channel >= 0 && midi_channel < SF64_MIDI_CHANNELS); + int idx = sf64_find_preset(synth->bank, bank, program); + if (idx < 0) + return false; + sf64_midi_channel_t *mc = &synth->midi[midi_channel]; + mc->bank = (uint16_t)bank; + mc->bank_msb = (bank > 127) ? 0 : (uint8_t)bank; + mc->program = (uint8_t)program; + mc->preset_index = (int16_t)idx; + return true; +} + +void sf64_synth_set_volume(sf64_synth_t *synth, int midi_channel, int volume) +{ + assert(synth); + assert(midi_channel >= 0 && midi_channel < SF64_MIDI_CHANNELS); + assert(volume >= 0 && volume <= 127); + synth->midi[midi_channel].volume = (uint8_t)volume; + if (synth->num_channels > 0) + midi_apply_vol(synth, midi_channel); +} + +void sf64_synth_mute_channel(sf64_synth_t *synth, int midi_channel, bool mute) +{ + assert(synth); + assert(midi_channel >= 0 && midi_channel < SF64_MIDI_CHANNELS); + synth->midi[midi_channel].muted = mute; + if (mute) + midi_all_sound_off(synth, midi_channel); +} + +void sf64_synth_set_expression(sf64_synth_t *synth, int midi_channel, int expression) +{ + assert(synth); + assert(midi_channel >= 0 && midi_channel < SF64_MIDI_CHANNELS); + assert(expression >= 0 && expression <= 127); + synth->midi[midi_channel].expression = (uint8_t)expression; + if (synth->num_channels > 0) + midi_apply_vol(synth, midi_channel); +} + +void sf64_synth_set_pan(sf64_synth_t *synth, int midi_channel, int pan) +{ + assert(synth); + assert(midi_channel >= 0 && midi_channel < SF64_MIDI_CHANNELS); + assert(pan >= 0 && pan <= 127); + synth->midi[midi_channel].pan = (uint8_t)pan; + if (synth->num_channels > 0) + midi_apply_vol(synth, midi_channel); +} + +void sf64_synth_set_pitch_bend(sf64_synth_t *synth, int midi_channel, int pitch_bend) +{ + assert(synth); + assert(midi_channel >= 0 && midi_channel < SF64_MIDI_CHANNELS); + assert(pitch_bend >= 0 && pitch_bend <= 16383); + synth->midi[midi_channel].pitch_bend = (uint16_t)pitch_bend; + if (synth->num_channels > 0) + midi_apply_bend(synth, midi_channel); +} + +void sf64_synth_set_sustain(sf64_synth_t *synth, int midi_channel, int value) +{ + assert(synth); + assert(midi_channel >= 0 && midi_channel < SF64_MIDI_CHANNELS); + assert(value >= 0 && value <= 127); + + bool was_down = synth->midi[midi_channel].sustain >= 64; + synth->midi[midi_channel].sustain = (uint8_t)value; + bool down = value >= 64; + if (down || !was_down || synth->num_channels <= 0) + return; + + for (int ch = synth->first_channel; + ch < synth->first_channel + synth->num_channels; ch++) { + sf64_voice_t *v = &synth->voices[ch]; + if (v->midi_channel == midi_channel && v->held_by_sustain) + voice_enter_release(synth, ch); + } +} diff --git a/src/audio/sf64_synth.c b/src/audio/sf64_synth.c new file mode 100644 index 000000000..ad11ea6e2 --- /dev/null +++ b/src/audio/sf64_synth.c @@ -0,0 +1,801 @@ +/** + * @file sf64_synth.c + * @author Giovanni Bajo + * @brief Voice engine for the SF64 synthesizer + * @ingroup mixer + * + * Owns mixer-channel allocation, DAHDSR envelopes, exclusive class, and + * #sf64_synth_process. The MIDI control plane lives in sf64_midi.c. + * + * A note-on can start several regions (SF2 layers). Those voices share one + * note identity; note-off releases the oldest still-held identity for that + * key on that MIDI channel. Allocation is atomic: #mixer_ch_alloc plans the + * slots, and if fewer than the matching regions are available nothing starts. + * Saturated pools steal existing voices (release before sustain before attack, + * then quieter/older), expanding each victim to its full note identity. + * Exclusive-class victims are advertised to the planner at + * #MIXER_PRIORITY_MIN and choked only after the plan succeeds. + * + * Two mixer controls, kept independent so CC updates never rebuild envelopes: + * - Amp envelope → #mixer_ch_set_gain_ramp + * - Region × velocity × CC7 × CC11 × pan → #mixer_ch_set_vol + * Amp attack is #mixer_ramp_linear (SF2's "convex in dB" intent: linear + * amplitude). Decay and release use #mixer_ramp_exp. Hold/decay honor SF2 + * keynum scaling. Sustain-loop regions keep looping until release begins. + * + * When #sf64_region_t::mod_env_to_pitch is set, a second DAHDSR drives + * #mixer_ch_set_freq_ramp. Mod attack is SF2 convex in level, then linear in + * cents; later phases use #mixer_ramp_exp. Pitch bend re-targets the current + * mod frequency without cancelling the remaining ramp duration. + * + * Velocity / CC7 / CC11 use the SF2.01 default `(x/127)²` gain curve. Region + * attenuation is preconverted by audioconv64 so note-on never runs `powf`. + * + * #sf64_synth_process advances absolute sample time, drains every overdue + * amp and mod envelope phase in one call, and reclaims mixer channels whose + * one-shot samples finished early. Pass the same sample counts used with + * #mixer_poll. + */ +#include "sf64_synth_internal.h" +#include "wav64.h" +#include "audio.h" +#include "debug.h" +#include +#include +#include +#include + +/** + * SF2.01 default modulators for note-on velocity, CC7 and CC11 → initial + * attenuation (concave, decreasing, unipolar, amount 960 cB) collapse to + * `(x/127)²` in linear gain. + */ +static float sf2_gain(int x) +{ + float t = x * (1.0f / 127.0f); + return t * t; +} + +/** Stop one mixer voice and mark it free. */ +void voice_stop(sf64_synth_t *synth, int ch) +{ + sf64_voice_t *v = &synth->voices[ch]; + if (v->phase == SF64_VOICE_OFF) + return; + mixer_ch_stop(ch); + v->phase = SF64_VOICE_OFF; + v->deadline = INT64_MAX; + v->midi_channel = -1; + v->key = -1; + v->preset_index = -1; + v->region_index = -1; + v->note_id = 0; + v->sustain_loop = false; + v->key_released = false; + v->held_by_sustain = false; + v->mod_phase = SF64_VOICE_OFF; + v->mod_deadline = INT64_MAX; + v->mod_env_level = 0; +} + +/** Stop every voice in the synthesizer's channel range. */ +void voices_stop_all(sf64_synth_t *synth) +{ + if (synth->num_channels <= 0) + return; + for (int ch = synth->first_channel; + ch < synth->first_channel + synth->num_channels; ch++) + voice_stop(synth, ch); +} + +/** Stealing priority for the current envelope phase (above #sf64_synth_t::priority). */ +static int voice_priority(sf64_synth_t *synth, const sf64_voice_t *v) +{ + int base = synth->priority; + switch (v->phase) { + case SF64_VOICE_OFF: + return MIXER_PRIORITY_MIN; + case SF64_VOICE_RELEASE: + return base + 1; + case SF64_VOICE_SUSTAIN: + return v->held_by_sustain ? base + 2 : base + 3; + default: + return base + 4; // DELAY / ATTACK / HOLD / DECAY + } +} + +/** Push phase (and exclusive-class) priorities into the mixer for this range. */ +static void refresh_priorities(sf64_synth_t *synth, uint32_t exclusive_mask) +{ + for (int ch = synth->first_channel; + ch < synth->first_channel + synth->num_channels; ch++) { + int prio = (exclusive_mask & (1u << ch)) + ? MIXER_PRIORITY_MIN + : voice_priority(synth, &synth->voices[ch]); + mixer_ch_set_priority(ch, prio); + } +} + +/** Stop every voice that shares @p note_id (layered note identity). */ +static void voice_stop_note(sf64_synth_t *synth, uint32_t note_id) +{ + if (note_id == 0) + return; + for (int ch = synth->first_channel; + ch < synth->first_channel + synth->num_channels; ch++) { + if (synth->voices[ch].note_id == note_id) + voice_stop(synth, ch); + } +} + +/** SF2 timecents -> output samples; <= -12000 is instant. */ +static int timecents_to_samples(int16_t tc) +{ + if (tc <= -12000) + return 0; + int rate = audio_get_frequency(); + assert(rate > 0); + float sec = powf(2.0f, tc / 1200.0f); + int n = (int)lroundf(sec * (float)rate); + return n > 0 ? n : 0; +} + +/** Timecents with optional SF2 keynum scaling: base + scale*(60-key). */ +static int env_scaled_samples(int16_t base_tc, int16_t keynum_scale, int key) +{ + int tc = base_tc; + if (keynum_scale) + tc += (int)keynum_scale * (60 - key); + if (tc < -12000) tc = -12000; + if (tc > 8000) tc = 8000; + return timecents_to_samples((int16_t)tc); +} + +/** Refresh #sf64_voice_t::channel_gain from the owning MIDI channel. */ +static void voice_refresh_channel_gain(sf64_synth_t *synth, int ch) +{ + sf64_voice_t *v = &synth->voices[ch]; + sf64_midi_channel_t *mc = &synth->midi[v->midi_channel]; + v->channel_gain = sf2_gain(mc->volume) * sf2_gain(mc->expression); +} + +/** Push region×velocity×CC7×CC11×pan into #mixer_ch_set_vol (not the envelope). */ +static void voice_set_vol(sf64_synth_t *synth, int ch) +{ + sf64_voice_t *v = &synth->voices[ch]; + sf64_midi_channel_t *mc = &synth->midi[v->midi_channel]; + sf64_region_t *r = &synth->bank->regions[v->region_index]; + float gain = v->base_gain * v->channel_gain; + int pan_sf = r->pan + (int)lroundf((mc->pan - 64) * (500.0f / 64.0f)); + if (pan_sf < -500) pan_sf = -500; + if (pan_sf > 500) pan_sf = 500; + float pan = (pan_sf + 500) / 1000.0f; + // Duration 0: immediate (no #mixer_ch_set_vol declick). Note-on and CC + // updates must land before the next mixed sample or peak tests see the + // tail of the previous level. + mixer_ch_set_vol_ramp(ch, gain * (1.0f - pan), gain * pan, 0); +} + +/** Playback Hz for @p mod_level in [0,1] (modEnvToPitch × level). */ +static float voice_freq_at(sf64_synth_t *synth, int ch, float mod_level) +{ + sf64_voice_t *v = &synth->voices[ch]; + sf64_midi_channel_t *mc = &synth->midi[v->midi_channel]; + sf64_region_t *r = &synth->bank->regions[v->region_index]; + sf64_sample_t *s = &synth->bank->samples[r->sample_index]; + float bend = (mc->pitch_bend - 8192) * + (float)mc->pitch_range_cents / 8192.0f; + float cents = (v->key - r->root_key) * (float)r->pitch_keytrack + + r->coarse_tune * 100.0f + r->fine_tune + bend + + r->mod_env_to_pitch * mod_level; + return s->sample_rate * powf(2.0f, cents / 1200.0f); +} + +/** + * SF2.01 / FluidSynth unipolar convex on [0, 1] (see fluid_convex). + * Used for envelope attacks + */ +static float sf64_convex(float u) +{ + if (u <= 0.0f) return 0.0f; + if (u >= 1.0f) return 1.0f; + float c = 1.0f - (-20.0f / 96.0f) * log10f(u * u); + if (c < 0.0f) c = 0.0f; + if (c > 1.0f) c = 1.0f; + return c; +} + +/** Mod-env attack → pitch: SF2 convex level, then linear cents. */ +static float sf64_ramp_mod_attack(float start, float end, float u) +{ + return mixer_ramp_exp(start, end, sf64_convex(u)); +} + +/** Push mod-env target @p level into the mixer (immediate or curved ramp). */ +static void voice_apply_mod_freq(sf64_synth_t *synth, int ch, float level, + int duration, mixer_ramp_fn_t curve, int started_ago) +{ + sf64_voice_t *v = &synth->voices[ch]; + v->mod_env_level = level; + float hz = voice_freq_at(synth, ch, level); + if (duration > 0) + mixer_ch_set_freq_ramp(ch, hz, duration, curve, started_ago); + else + mixer_ch_set_freq(ch, hz); +} + +/** Retarget frequency after pitch bend, preserving a running mod ramp. */ +static void voice_retarget_freq(sf64_synth_t *synth, int ch) +{ + sf64_voice_t *v = &synth->voices[ch]; + int rem = 0; + mixer_ramp_fn_t curve = mixer_ramp_exp; + if (v->mod_phase == SF64_VOICE_ATTACK || v->mod_phase == SF64_VOICE_DECAY || + v->mod_phase == SF64_VOICE_RELEASE) { + int64_t left = v->mod_deadline - synth->now; + if (left > 0) + rem = (int)left; + if (v->mod_phase == SF64_VOICE_ATTACK) + curve = sf64_ramp_mod_attack; + } + voice_apply_mod_freq(synth, ch, v->mod_env_level, rem, curve, 0); +} + +/** Reapply #mixer_ch_set_vol after a MIDI volume/expression/pan change. */ +static void voice_apply_vol(sf64_synth_t *synth, int ch) +{ + if (synth->voices[ch].phase == SF64_VOICE_OFF) + return; + voice_refresh_channel_gain(synth, ch); + voice_set_vol(synth, ch); +} + +/** Reapply volume×expression×pan to all voices on a MIDI channel. */ +void midi_apply_vol(sf64_synth_t *synth, int midi_channel) +{ + for (int ch = synth->first_channel; + ch < synth->first_channel + synth->num_channels; ch++) { + if (synth->voices[ch].phase != SF64_VOICE_OFF && + synth->voices[ch].midi_channel == midi_channel) + voice_apply_vol(synth, ch); + } +} + +/** Retarget pitch for all voices on a MIDI channel after a bend change. */ +void midi_apply_bend(sf64_synth_t *synth, int midi_channel) +{ + for (int ch = synth->first_channel; + ch < synth->first_channel + synth->num_channels; ch++) { + if (synth->voices[ch].phase != SF64_VOICE_OFF && + synth->voices[ch].midi_channel == midi_channel) + voice_retarget_freq(synth, ch); + } +} + +sf64_synth_t *sf64_synth_create(sf64_bank_t *bank) +{ + assert(bank); + sf64_synth_t *synth = calloc(1, sizeof(*synth)); + assert(synth); + synth->midi_target.ops = &sf64_midi_ops; + synth->bank = bank; + synth->first_channel = -1; + synth->num_channels = 0; + synth->now = 0; + synth->next_note_id = 1; + midi_channels_reset(synth); + for (int i = 0; i < MIXER_MAX_CHANNELS; i++) { + synth->voices[i].phase = SF64_VOICE_OFF; + synth->voices[i].mod_phase = SF64_VOICE_OFF; + synth->voices[i].deadline = INT64_MAX; + synth->voices[i].mod_deadline = INT64_MAX; + synth->voices[i].midi_channel = -1; + synth->voices[i].key = -1; + synth->voices[i].preset_index = -1; + synth->voices[i].region_index = -1; + } + return synth; +} + +void sf64_synth_close(sf64_synth_t *synth) +{ + assert(synth); + voices_stop_all(synth); + free(synth); +} + +void sf64_synth_set_channels(sf64_synth_t *synth, int first_channel, + int num_channels, int priority) +{ + assert(synth); + assert(first_channel >= 0); + assert(num_channels >= 1); + assert(first_channel + num_channels <= MIXER_MAX_CHANNELS); + assertf(priority >= MIXER_PRIORITY_MIN && priority <= MIXER_PRIORITY_MAX - 4, + "sf64_synth_set_channels: priority %d leaves no room for phase offsets", + priority); + voices_stop_all(synth); + synth->first_channel = first_channel; + synth->num_channels = num_channels; + synth->priority = priority; +} + +/** Samples since phase boundary @p at (for #mixer_ch_set_*_ramp started_ago). */ +static int voice_started_ago(sf64_synth_t *synth, int64_t at) +{ + int64_t d = synth->now - at; + assertf(d >= 0, "sf64: phase boundary in the future"); + assertf(d <= INT_MAX, "sf64: started_ago overflow"); + return (int)d; +} + +/** Begin mod-env decay (or sustain) at absolute sample @p at. */ +static void voice_mod_enter_decay(sf64_synth_t *synth, int ch, int64_t at) +{ + sf64_voice_t *v = &synth->voices[ch]; + sf64_region_t *r = &synth->bank->regions[v->region_index]; + int decay = env_scaled_samples(r->mod_env.decay_timecents, + r->mod_env.keynum_to_decay, v->key); + float sus = r->mod_env.sustain_gain; + // SF2/TSF: decay time is to zero; linear segment ends at sustain earlier. + int dur = (int)lroundf((float)decay * (1.0f - sus)); + int ago = voice_started_ago(synth, at); + + if (dur > 0 && sus < 1.0f) { + voice_apply_mod_freq(synth, ch, sus, dur, mixer_ramp_exp, ago); + v->mod_phase = SF64_VOICE_DECAY; + v->mod_deadline = at + dur; + } else { + voice_apply_mod_freq(synth, ch, sus, 0, mixer_ramp_exp, 0); + v->mod_phase = SF64_VOICE_SUSTAIN; + v->mod_deadline = INT64_MAX; + } +} + +/** After mod attack: hold at peak, or go straight to decay. */ +static void voice_mod_enter_hold_or_decay(sf64_synth_t *synth, int ch, int64_t at) +{ + sf64_voice_t *v = &synth->voices[ch]; + sf64_region_t *r = &synth->bank->regions[v->region_index]; + int hold = env_scaled_samples(r->mod_env.hold_timecents, + r->mod_env.keynum_to_hold, v->key); + + voice_apply_mod_freq(synth, ch, 1.0f, 0, mixer_ramp_exp, 0); + if (hold > 0) { + v->mod_phase = SF64_VOICE_HOLD; + v->mod_deadline = at + hold; + } else { + voice_mod_enter_decay(synth, ch, at); + } +} + +/** Begin the mod-env attack at absolute sample @p at. */ +static void voice_mod_enter_attack(sf64_synth_t *synth, int ch, int64_t at) +{ + sf64_voice_t *v = &synth->voices[ch]; + sf64_region_t *r = &synth->bank->regions[v->region_index]; + int attack = timecents_to_samples(r->mod_env.attack_timecents); + + if (attack > 0) { + voice_apply_mod_freq(synth, ch, 1.0f, attack, sf64_ramp_mod_attack, + voice_started_ago(synth, at)); + v->mod_phase = SF64_VOICE_ATTACK; + v->mod_deadline = at + attack; + } else { + voice_mod_enter_hold_or_decay(synth, ch, at); + } +} + +/** Start the mod envelope, or pin base pitch when modEnvToPitch is unused. */ +static void voice_mod_start(sf64_synth_t *synth, int ch) +{ + sf64_voice_t *v = &synth->voices[ch]; + sf64_region_t *r = &synth->bank->regions[v->region_index]; + + // Pin the base pitch first: #mixer_ch_play just reset the channel to the + // waveform's own rate, and a ramp would otherwise start from there. + voice_apply_mod_freq(synth, ch, 0, 0, mixer_ramp_exp, 0); + + if (r->mod_env_to_pitch == 0) { + v->mod_phase = SF64_VOICE_OFF; + v->mod_deadline = INT64_MAX; + return; + } + + int delay = timecents_to_samples(r->mod_env.delay_timecents); + if (delay > 0) { + v->mod_phase = SF64_VOICE_DELAY; + v->mod_deadline = synth->now + delay; + } else { + voice_mod_enter_attack(synth, ch, synth->now); + } +} + +/** Note-off for the mod envelope (independent of amp release length). */ +static void voice_mod_enter_release(sf64_synth_t *synth, int ch) +{ + sf64_voice_t *v = &synth->voices[ch]; + if (v->mod_phase == SF64_VOICE_OFF || v->mod_phase == SF64_VOICE_RELEASE) + return; + + sf64_region_t *r = &synth->bank->regions[v->region_index]; + int release = timecents_to_samples(r->mod_env.release_timecents); + if (release <= 0) { + voice_apply_mod_freq(synth, ch, 0, 0, mixer_ramp_exp, 0); + v->mod_phase = SF64_VOICE_OFF; + v->mod_deadline = INT64_MAX; + return; + } + voice_apply_mod_freq(synth, ch, 0, release, mixer_ramp_exp, 0); + v->mod_phase = SF64_VOICE_RELEASE; + v->mod_deadline = synth->now + release; +} + +/** Drain overdue mod-envelope phases. */ +static void voice_mod_advance_overdue(sf64_synth_t *synth, int ch) +{ + sf64_voice_t *v = &synth->voices[ch]; + while (v->mod_phase != SF64_VOICE_OFF && v->mod_deadline <= synth->now) { + int64_t at = v->mod_deadline; + switch (v->mod_phase) { + case SF64_VOICE_DELAY: + voice_mod_enter_attack(synth, ch, at); + break; + case SF64_VOICE_ATTACK: + voice_mod_enter_hold_or_decay(synth, ch, at); + break; + case SF64_VOICE_HOLD: + voice_mod_enter_decay(synth, ch, at); + break; + case SF64_VOICE_DECAY: { + sf64_region_t *r = &synth->bank->regions[v->region_index]; + voice_apply_mod_freq(synth, ch, r->mod_env.sustain_gain, 0, mixer_ramp_exp, 0); + v->mod_phase = SF64_VOICE_SUSTAIN; + v->mod_deadline = INT64_MAX; + break; + } + case SF64_VOICE_RELEASE: + voice_apply_mod_freq(synth, ch, 0, 0, mixer_ramp_exp, 0); + v->mod_phase = SF64_VOICE_OFF; + v->mod_deadline = INT64_MAX; + return; + default: + return; + } + } +} + +/** Begin the amp (and mod) release phase for a sounding voice. */ +void voice_enter_release(sf64_synth_t *synth, int ch) +{ + sf64_voice_t *v = &synth->voices[ch]; + sf64_region_t *r = &synth->bank->regions[v->region_index]; + sf64_sample_t *s = &synth->bank->samples[r->sample_index]; + int release = timecents_to_samples(r->amp_env.release_timecents); + + v->key_released = true; + v->held_by_sustain = false; + voice_mod_enter_release(synth, ch); + + // Exit sustain loops so the SF2 release tail can play. Tiny leading + // loops are the "release sample" idiom (silence until note-off): + // without a low-pass filter, leaving them reveals a pitched body that + // sounds like a second note (e.g. GeneralUser Clavinet_rel). Keep + // looping silence for those until filters exist. + if (v->sustain_loop && s->loop_end > s->loop_start && + s->loop_end - s->loop_start >= 128) + mixer_ch_set_loop(ch, false); + + if (release <= 0) { + voice_stop(synth, ch); + return; + } + + v->envelope_gain = 0; + mixer_ch_set_gain_ramp(ch, 0, release, mixer_ramp_exp, 0); + v->phase = SF64_VOICE_RELEASE; + v->deadline = synth->now + release; +} + +/** Begin decay (or sustain) with phase boundary at absolute sample @p at. */ +static void voice_enter_decay(sf64_synth_t *synth, int ch, int64_t at) +{ + sf64_voice_t *v = &synth->voices[ch]; + sf64_region_t *r = &synth->bank->regions[v->region_index]; + int decay = env_scaled_samples(r->amp_env.decay_timecents, + r->amp_env.keynum_to_decay, v->key); + float sus = r->amp_env.sustain_gain; + + v->envelope_gain = sus; + if (decay > 0 && sus < 1.0f) { + mixer_ch_set_gain_ramp(ch, sus, decay, mixer_ramp_exp, + voice_started_ago(synth, at)); + v->phase = SF64_VOICE_DECAY; + v->deadline = at + decay; + } else { + mixer_ch_set_gain(ch, sus); + v->phase = SF64_VOICE_SUSTAIN; + v->deadline = INT64_MAX; + } +} + +/** After attack: hold at peak, or go straight to decay. */ +static void voice_enter_hold_or_decay(sf64_synth_t *synth, int ch, int64_t at) +{ + sf64_voice_t *v = &synth->voices[ch]; + sf64_region_t *r = &synth->bank->regions[v->region_index]; + int hold = env_scaled_samples(r->amp_env.hold_timecents, + r->amp_env.keynum_to_hold, v->key); + + v->envelope_gain = 1.0f; + mixer_ch_set_gain(ch, 1.0f); + if (hold > 0) { + v->phase = SF64_VOICE_HOLD; + v->deadline = at + hold; + } else { + voice_enter_decay(synth, ch, at); + } +} + +/** Begin the attack ramp with phase boundary at absolute sample @p at. */ +static void voice_enter_attack(sf64_synth_t *synth, int ch, int64_t at) +{ + sf64_voice_t *v = &synth->voices[ch]; + sf64_region_t *r = &synth->bank->regions[v->region_index]; + int attack = timecents_to_samples(r->amp_env.attack_timecents); + + if (attack > 0) { + v->envelope_gain = 1.0f; + mixer_ch_set_gain_ramp(ch, 1.0f, attack, mixer_ramp_linear, + voice_started_ago(synth, at)); + v->phase = SF64_VOICE_ATTACK; + v->deadline = at + attack; + } else { + voice_enter_hold_or_decay(synth, ch, at); + } +} + +/** Start a held voice on an already-reserved channel. */ +static void voice_start(sf64_synth_t *synth, int ch, int midi_channel, + int preset_index, int ri, int key, int velocity, uint32_t note_id) +{ + sf64_region_t *r = &synth->bank->regions[ri]; + int delay = timecents_to_samples(r->amp_env.delay_timecents); + sf64_voice_t *v = &synth->voices[ch]; + + v->midi_channel = (int8_t)midi_channel; + v->key = (int8_t)key; + v->preset_index = (int16_t)preset_index; + v->region_index = ri; + v->note_id = note_id; + v->sustain_loop = (r->loop_mode == SF64_LOOP_SUSTAIN); + v->key_released = false; + v->held_by_sustain = false; + v->base_gain = r->gain * sf2_gain(velocity); + voice_refresh_channel_gain(synth, ch); + v->envelope_gain = 0; + + mixer_ch_set_gain(ch, 0); + voice_set_vol(synth, ch); + mixer_ch_play(ch, &synth->bank->waves[r->sample_index]->wave); + voice_mod_start(synth, ch); + + if (delay > 0) { + v->phase = SF64_VOICE_DELAY; + v->deadline = synth->now + delay; + } else { + voice_enter_attack(synth, ch, synth->now); + } +} + +/** True if @p group is among the exclusive classes of @p matches. */ +static bool match_has_group(sf64_bank_t *bank, const int *matches, int nmatch, + uint8_t group) +{ + if (group == 0) + return false; + for (int i = 0; i < nmatch; i++) + if (bank->regions[matches[i]].exclusive_group == group) + return true; + return false; +} + +/** Bitmask of voices that share preset + exclusive class with @p matches. */ +static uint32_t exclusive_victim_mask(sf64_synth_t *synth, int preset_index, + const int *matches, int nmatch) +{ + uint32_t mask = 0; + for (int ch = synth->first_channel; + ch < synth->first_channel + synth->num_channels; ch++) { + sf64_voice_t *v = &synth->voices[ch]; + if (v->phase == SF64_VOICE_OFF || v->preset_index != preset_index) + continue; + uint8_t g = synth->bank->regions[v->region_index].exclusive_group; + if (match_has_group(synth->bank, matches, nmatch, g)) + mask |= 1u << ch; + } + return mask; +} + +/** Stop every voice listed in @p mask (absolute channel bits). */ +static void choke_exclusive_mask(sf64_synth_t *synth, uint32_t mask) +{ + for (int ch = synth->first_channel; + ch < synth->first_channel + synth->num_channels; ch++) { + if (mask & (1u << ch)) + voice_stop(synth, ch); + } +} + +uint32_t sf64_synth_note_on(sf64_synth_t *synth, int midi_channel, + int key, int velocity) +{ + assert(synth); + assert(midi_channel >= 0 && midi_channel < SF64_MIDI_CHANNELS); + assertf(synth->num_channels > 0, "sf64_synth: call sf64_synth_set_channels first"); + if (velocity <= 0) { + sf64_synth_note_off(synth, midi_channel, key); + return 0; + } + if (synth->midi[midi_channel].muted) + return 0; + + int preset_index = synth->midi[midi_channel].preset_index; + if (preset_index < 0) + return 0; + + sf64_preset_t *p = &synth->bank->presets[preset_index]; + int matches[MIXER_MAX_CHANNELS]; + int nmatch = 0; + for (int i = 0; i < p->num_regions; i++) { + int ri = p->first_region + i; + sf64_region_t *r = &synth->bank->regions[ri]; + if (key >= r->key_min && key <= r->key_max && + velocity >= r->velocity_min && velocity <= r->velocity_max) { + if (nmatch >= MIXER_MAX_CHANNELS) + return 0; + matches[nmatch++] = ri; + } + } + if (nmatch == 0) + return 0; + + // Advertise exclusive victims as free to the planner; choke only after + // the plan succeeds so a rejected note-on leaves them untouched. + uint32_t excl = exclusive_victim_mask(synth, preset_index, matches, nmatch); + refresh_priorities(synth, excl); + + int plan[MIXER_MAX_CHANNELS]; + int nplan = mixer_ch_alloc(synth->first_channel, synth->num_channels, + nmatch, false, synth->priority + 4, NULL, plan); + if (nplan < nmatch) + return 0; + + choke_exclusive_mask(synth, excl); + + // Steal: expand each still-occupied planned channel to its note identity + // so a layered victim is never cut in half. + for (int i = 0; i < nmatch; i++) { + sf64_voice_t *v = &synth->voices[plan[i]]; + if (v->phase != SF64_VOICE_OFF) + voice_stop_note(synth, v->note_id); + } + + uint32_t note_id = synth->next_note_id++; + if (synth->next_note_id == 0) + synth->next_note_id = 1; + + for (int i = 0; i < nmatch; i++) { + voice_start(synth, plan[i], midi_channel, preset_index, + matches[i], key, velocity, note_id); + } + return note_id; +} + +void sf64_synth_note_off(sf64_synth_t *synth, int midi_channel, int key) +{ + assert(synth); + assert(midi_channel >= 0 && midi_channel < SF64_MIDI_CHANNELS); + if (synth->num_channels <= 0) + return; + + // Oldest identity that still has the key down on this MIDI channel. + uint32_t oldest = 0; + for (int ch = synth->first_channel; + ch < synth->first_channel + synth->num_channels; ch++) { + sf64_voice_t *v = &synth->voices[ch]; + if (!voice_key_down(v) || v->midi_channel != midi_channel || v->key != key) + continue; + if (oldest == 0 || v->note_id < oldest) + oldest = v->note_id; + } + if (oldest == 0) + return; + + bool pedal = synth->midi[midi_channel].sustain >= 64; + for (int ch = synth->first_channel; + ch < synth->first_channel + synth->num_channels; ch++) { + sf64_voice_t *v = &synth->voices[ch]; + if (!voice_key_down(v) || v->midi_channel != midi_channel || + v->key != key || v->note_id != oldest) + continue; + + if (pedal) { + // Sustain loop stays on until the pedal releases. + v->key_released = true; + v->held_by_sustain = true; + continue; + } + voice_enter_release(synth, ch); + } +} + +/** Drain all envelope phases whose deadline is at or before @p now. */ +static void voice_advance_overdue(sf64_synth_t *synth, int ch) +{ + sf64_voice_t *v = &synth->voices[ch]; + while (v->phase != SF64_VOICE_OFF && v->deadline <= synth->now) { + int64_t at = v->deadline; + switch (v->phase) { + case SF64_VOICE_DELAY: + voice_enter_attack(synth, ch, at); + break; + case SF64_VOICE_ATTACK: + voice_enter_hold_or_decay(synth, ch, at); + break; + case SF64_VOICE_HOLD: + voice_enter_decay(synth, ch, at); + break; + case SF64_VOICE_DECAY: { + sf64_region_t *r = &synth->bank->regions[v->region_index]; + // Sustain at silence: reclaim the channel (common for piano). + if (r->amp_env.sustain_gain <= 0.0f) { + voice_stop(synth, ch); + return; + } + v->phase = SF64_VOICE_SUSTAIN; + v->deadline = INT64_MAX; + break; + } + case SF64_VOICE_RELEASE: + voice_stop(synth, ch); + return; + default: + return; + } + } +} + +int sf64_synth_process(sf64_synth_t *synth, int num_samples) +{ + assert(synth); + assert(num_samples >= 0); + + synth->now += num_samples; + + int64_t next = INT64_MAX; + for (int ch = synth->first_channel; + ch < synth->first_channel + synth->num_channels; ch++) { + sf64_voice_t *v = &synth->voices[ch]; + if (v->phase == SF64_VOICE_OFF) + continue; + // One-shot sample finished before the envelope: free the channel. + if (!mixer_ch_playing(ch)) { + voice_stop(synth, ch); + continue; + } + voice_advance_overdue(synth, ch); + if (v->phase == SF64_VOICE_OFF) + continue; + voice_mod_advance_overdue(synth, ch); + if (v->deadline < next) + next = v->deadline; + if (v->mod_deadline < next) + next = v->mod_deadline; + } + + if (next == INT64_MAX) + return -1; + int64_t delta = next - synth->now; + return delta > 0 ? (int)delta : 0; +} diff --git a/src/audio/sf64_synth_internal.h b/src/audio/sf64_synth_internal.h new file mode 100644 index 000000000..ebd058dbb --- /dev/null +++ b/src/audio/sf64_synth_internal.h @@ -0,0 +1,129 @@ +/** + * @file sf64_synth_internal.h + * @author Giovanni Bajo + * @brief Shared state and helpers for the SF64 synthesizer + * @ingroup mixer + * + * Shared by the MIDI control plane (sf64_midi.c) and the voice engine + * (sf64_synth.c). Not part of the public API. + */ +#ifndef LIBDRAGON_SF64_SYNTH_INTERNAL_H +#define LIBDRAGON_SF64_SYNTH_INTERNAL_H + +#include "sf64_synth.h" +#include "sf64_internal.h" +#include "mixer.h" +#include +#include +#include + +/** GM drum channel (MIDI channel 10 → index 9). */ +#define SF64_DRUM_CHANNEL 9 +/** Default SF2 percussion bank for #SF64_DRUM_CHANNEL. */ +#define SF64_DRUM_BANK 128 + +/** @brief Envelope phase of a synth voice. */ +typedef enum { + SF64_VOICE_OFF, ///< Idle; mixer channel is free + SF64_VOICE_DELAY, ///< Silent delay before attack + SF64_VOICE_ATTACK, ///< Ramping to peak gain + SF64_VOICE_HOLD, ///< Holding peak before decay + SF64_VOICE_DECAY, ///< Ramping from peak to the sustain level + SF64_VOICE_SUSTAIN, ///< Holding the sustain level until note-off + SF64_VOICE_RELEASE, ///< Ramping to silence after note-off +} sf64_voice_phase_t; + +/** @brief Per-MIDI-channel controllers and program */ +typedef struct { + uint16_t bank; ///< SF2 bank for program change (0–128) + uint8_t bank_msb; ///< CC0 bank select MSB + uint8_t bank_lsb; ///< CC32 bank select LSB + uint8_t program; ///< MIDI program + int16_t preset_index; ///< Index into sf64_bank_t.presets, or -1 + uint8_t volume; ///< CC7 (0–127); not cleared by CC121 + uint8_t expression; ///< CC11 (0–127) + uint8_t pan; ///< CC10 (0–127, 64 = center) + uint8_t modulation; ///< CC1 (0–127) + uint8_t sustain; ///< CC64 sustain pedal (`>= 64` = down) + uint8_t rpn_msb; ///< CC101; 0x7F/0x7F = null + uint8_t rpn_lsb; ///< CC100 + uint8_t data_entry_msb; ///< CC6 Data Entry MSB + uint8_t data_entry_lsb; ///< CC38 Data Entry LSB + uint16_t pitch_bend; ///< 14-bit bend (center 8192) + int16_t pitch_range_cents; ///< Full bend span in cents (default 200) + bool muted; ///< User mute; not cleared by MIDI reset +} sf64_midi_channel_t; + +/** @brief One voice bound to a mixer channel. */ +typedef struct { + sf64_voice_phase_t phase; ///< Current amp-envelope phase + sf64_voice_phase_t mod_phase; ///< Modulation-envelope phase (#SF64_VOICE_OFF if unused) + int8_t midi_channel; ///< MIDI channel that owns this voice, or -1 + int8_t key; ///< MIDI key that started this voice, or -1 + int16_t preset_index; ///< Preset used at note-on (for exclusive class) + int region_index; ///< Index into sf64_bank_t.regions + uint32_t note_id; ///< Note identity shared by layered voices; 0 if off + int64_t deadline; ///< Absolute sample time of the next amp phase; INT64_MAX = none + int64_t mod_deadline; ///< Absolute sample time of the next mod phase; INT64_MAX = none + float base_gain; ///< Region × velocity gain, fixed at note-on + float channel_gain; ///< MIDI volume × expression + float envelope_gain; ///< Amp-envelope target level for the current phase + float mod_env_level; ///< Mod-envelope target level for the current phase (0…1) + bool sustain_loop; ///< True if the region uses #SF64_LOOP_SUSTAIN + bool key_released; ///< Note-off received (may still be held by pedal) + bool held_by_sustain; ///< Sounding only because the sustain pedal is down +} sf64_voice_t; + +/** + * @brief Opaque synthesizer state. + * + * Immutable bank pointer; mutable MIDI channels and mixer voices. Not part of + * the public API — clients use the typedef in sf64_synth.h. + */ +typedef struct sf64_synth_s { + midi_target_t midi_target; ///< Must be first; see #sf64_synth_midi_target + sf64_bank_t *bank; ///< Bank this synth plays from + int first_channel; ///< First mixer channel of the allocated range + int num_channels; ///< Number of mixer channels reserved for voices + int priority; ///< Base voice-stealing priority for this synth + int64_t now; ///< Absolute sample time advanced by #sf64_synth_process + uint32_t next_note_id; ///< Next note identity to assign (`>= 1`) + sf64_synth_mode_t mode; ///< Native vs GM1 channel semantics + sf64_midi_channel_t midi[SF64_MIDI_CHANNELS]; ///< Per-MIDI-channel state + sf64_voice_t voices[MIXER_MAX_CHANNELS]; ///< Per-mixer-channel voice state +} sf64_synth_t; + +/** True while the voice is in a pre-release envelope phase. */ +static inline bool voice_sounding(const sf64_voice_t *v) +{ + return v->phase == SF64_VOICE_DELAY || + v->phase == SF64_VOICE_ATTACK || + v->phase == SF64_VOICE_HOLD || + v->phase == SF64_VOICE_DECAY || + v->phase == SF64_VOICE_SUSTAIN; +} + +/** True while the key is still down (note-off not yet received). */ +static inline bool voice_key_down(const sf64_voice_t *v) +{ + return voice_sounding(v) && !v->key_released; +} + +/** #midi_target_ops_t for this synthesizer (defined in sf64_midi.c). */ +extern const midi_target_ops_t sf64_midi_ops; + +/** Reset every MIDI channel's program and controllers to defaults. */ +void midi_channels_reset(sf64_synth_t *synth); + +/** Stop one mixer voice and mark it free. */ +void voice_stop(sf64_synth_t *synth, int ch); +/** Stop every voice in the synthesizer's channel range. */ +void voices_stop_all(sf64_synth_t *synth); +/** Begin the amp (and mod) release phase for a sounding voice. */ +void voice_enter_release(sf64_synth_t *synth, int ch); +/** Reapply volume×expression×pan to all voices on a MIDI channel. */ +void midi_apply_vol(sf64_synth_t *synth, int midi_channel); +/** Retarget pitch for all voices on a MIDI channel after a bend change. */ +void midi_apply_bend(sf64_synth_t *synth, int midi_channel); + +#endif diff --git a/src/audio/wav64.c b/src/audio/wav64.c index e092deaf0..c3106bd16 100644 --- a/src/audio/wav64.c +++ b/src/audio/wav64.c @@ -9,6 +9,7 @@ #include "wav64_internal.h" #include "wav64_vadpcm_internal.h" #include "wav64_opus_internal.h" +#include "wav64_ulc_internal.h" #include "mixer.h" #include "mixer_internal.h" #include "dragonfs.h" @@ -24,25 +25,23 @@ #include #include #include -#include #include +#include #include #include +#include /** ID of a standard WAV file */ #define WAV_RIFF_ID "RIFF" /** ID of a WAVX file (big-endian WAV) */ #define WAV_RIFX_ID "RIFX" -/** @brief Profile of DMA usage by WAV64, used for debugging purposes. */ -int64_t __wav64_profile_dma = 0; - /** @brief None compression init function */ -static void wav64_none_init(wav64_t *wav); +static void wav64_none_init(wav64_t *wav, int state_size); /** @brief None compression get_bitrate function */ static int wav64_none_get_bitrate(wav64_t *wav); -static wav64_compression_t algos[4] = { +static wav64_compression_t algos[WAV64_NUM_FORMATS] = { // None compression [WAV64_FORMAT_RAW] = { .init = wav64_none_init, @@ -54,107 +53,291 @@ static wav64_compression_t algos[4] = { .init = wav64_vadpcm_init, .close = wav64_vadpcm_close, .get_bitrate = wav64_vadpcm_get_bitrate, + .adjust_seek = wav64_vadpcm_adjust_seek, }, }; -void raw_waveform_read(samplebuffer_t *sbuf, int current_fd, int wpos, int wlen, int bps) { - uint8_t* ram_addr = (uint8_t*)samplebuffer_append(sbuf, wlen); - int bytes = wlen << bps; - - // FIXME: remove CachedAddr() when read() supports uncached addresses - uint32_t t0 = TICKS_READ(); - read(current_fd, CachedAddr(ram_addr), bytes); - __wav64_profile_dma += TICKS_READ() - t0; -} +static void raw_waveform_read(samplebuffer_t *sbuf, wav64_t *wav, int wpos, int wlen, int bps) { + uint32_t rom = wav->st->rom_base; + while (wlen > 0) { + int n = MIN(wlen, SAMPLEBUFFER_MARGIN_UNITS); + uint8_t* ram_addr = (uint8_t*)samplebuffer_append(sbuf, n); + int bytes = n << bps; + uint32_t pi_addr = rom + wav->st->base_offset + (wpos << bps); -void raw_waveform_read_address(samplebuffer_t *sbuf, int base_rom_addr, int wpos, int wlen, int bps) { - uint32_t rom_addr = base_rom_addr + (wpos << bps); - uint8_t* ram_addr = (uint8_t*)samplebuffer_append(sbuf, wlen); - int bytes = wlen << bps; - - uint32_t t0 = TICKS_READ(); - // Run the DMA transfer. We rely on libdragon's PI DMA function which works - // also for misaligned addresses and odd lengths. - // The mixer/samplebuffer guarantees that ROM/RAM addresses are always - // on the same 2-byte phase, as the only requirement of dma_read. - dma_read(ram_addr, rom_addr, bytes); - __wav64_profile_dma += TICKS_READ() - t0; + if (rom && !(((uint32_t)ram_addr ^ pi_addr) & 1)) { + samplebuffer_dma_wait(sbuf); + sbuf->dma_ticket = dma_read_async(ram_addr, pi_addr, bytes); + } else { + lseek(wav->st->current_fd, wav->st->base_offset + (wpos << bps), SEEK_SET); + // FIXME: remove CachedAddr() when read() supports uncached addresses + read(wav->st->current_fd, CachedAddr(ram_addr), bytes); + } + wlen -= n; + wpos += n; + } } -static void waveform_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) { - wav64_t *wav = (wav64_t*)ctx; +static void wav64_none_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) { + wav64_t *wav = (wav64_t*)sbuf->wave; int bps = (wav->wave.bits == 8 ? 0 : 1) + (wav->wave.channels == 2 ? 1 : 0); - if (seeking) { - lseek(wav->current_fd, wav->base_offset + (wpos << bps), SEEK_SET); - } - raw_waveform_read(sbuf, wav->current_fd, wpos, wlen, bps); + (void)ctx; (void)seeking; + raw_waveform_read(sbuf, wav, wpos, wlen, bps); } -static void wav64_none_init(wav64_t *wav) { - // Initialize none compression - wav->wave.read = waveform_read; - wav->wave.ctx = wav; +static void wav64_none_init(wav64_t *wav, int state_size) { + // Initialize none compression. Setup read callback for streaming; + // preloaded waveforms set wave->mem and need no read callback. + if (!wav->st->samples) { + wav->wave.read = wav64_none_read; + // Samples come straight from ROM via async PI DMA; a file on any other + // medium is read synchronously instead. + wav->wave.async_read = wav->st->rom_base != 0; + } else { + wav->wave.read = NULL; + wav->wave.async_read = false; + wav->wave.mem = wav->st->samples; + } + // Also clear start callback (needed in the preloading codepath) + wav->wave.start = NULL; } static int wav64_none_get_bitrate(wav64_t *wav) { return wav->wave.frequency * wav->wave.channels * wav->wave.bits; } -void wav64_open(wav64_t *wav, const char *file_name) { - memset(wav, 0, sizeof(*wav)); - - // For backwards compatibility with old versions of this file, we support - // an unprefixed file name as a dfs file. This is deprecated and not documented - // but we just want to avoid breaking existing code - if (strchr(file_name, ':') == NULL) { - char* dfs_name = alloca(5 + strlen(file_name) + 1); - strcpy(dfs_name, "rom:/"); - strcat(dfs_name, file_name); - file_name = dfs_name; +static wav64_t* internal_open(wav64_t *wav, int file_handle, const char *file_name, wav64_loadparms_t *parms) +{ + wav64_loadparms_t default_parms = {0}; + if (!parms) parms = &default_parms; + + // Open the input file, and read the header + int start_offset = 0; + bool owned_fd = false; + if (file_handle < 0) { + // For backwards compatibility with old versions of this file, we support + // an unprefixed file name as a dfs file. This is deprecated and not documented + // but we just want to avoid breaking existing code + if (file_name && strchr(file_name, ':') == NULL) { + char* dfs_name = alloca(5 + strlen(file_name) + 1); + strcpy(dfs_name, "rom:/"); + strcat(dfs_name, file_name); + file_name = dfs_name; + } + + file_handle = must_open(file_name); + owned_fd = true; + } else { + start_offset = lseek(file_handle, 0, SEEK_CUR); } - // Open the input file. - int file_handle = must_open(file_name); - wav64_header_t head = {0}; + wav64_header_t head; read(file_handle, &head, sizeof(head)); + if (memcmp(head.id, WAV64_ID, 4) != 0) { assertf(memcmp(head.id, WAV_RIFF_ID, 4) != 0 && memcmp(head.id, WAV_RIFX_ID, 4) != 0, "wav64 %s: use audioconv64 to convert to wav64 format", file_name); assertf(0, "wav64 %s: invalid ID: %02x%02x%02x%02x\n", file_name, head.id[0], head.id[1], head.id[2], head.id[3]); } - assertf(head.version == WAV64_FILE_VERSION, "wav64 %s: invalid version: %02x\n", + assertf(head.version == 10, + "wav64 %s: unsupported version %02x; reconvert with audioconv64)", file_name, head.version); + assertf(head.format < WAV64_NUM_FORMATS, "Unknown wav64 compression format %d; corrupted file?", head.format); + assertf(head.format < WAV64_NUM_FORMATS && algos[head.format].init != NULL, + "wav64: compression level %d not initialized. Call wav64_init_compression(%d) at initialization time", head.format, head.format); + + int ext_size = head.start_offset - sizeof(wav64_header_t); + bool preload = parms->streaming_mode == WAV64_STREAMING_NONE; + // VADPCM preloads stay compressed, laid out fully planar in RDRAM for + // direct MIX_CHANNEL addressing. The frame size depends on the residual + // width, which lives in the extended header: that is only read into its + // final home further down, so peek at its prefix to size the buffer. + bool preload_vadpcm = false; + int vframe_bytes = 0; + if (head.format == WAV64_FORMAT_VADPCM) { + uint8_t prefix[offsetof(wav64_header_vadpcm_t, huff_tbl)]; + read(file_handle, prefix, sizeof(prefix)); + lseek(file_handle, -(int)sizeof(prefix), SEEK_CUR); + int bits = prefix[offsetof(wav64_header_vadpcm_t, residual_bits)]; + vframe_bytes = VADPCM_FRAME_BYTES(VADPCM_RESIDUAL_BITS(bits)); + uint16_t vflags; + memcpy(&vflags, prefix + offsetof(wav64_header_vadpcm_t, flags), sizeof(vflags)); + if (vflags & VADPCM_FLAG_RESIDENT) + preload = true; + preload_vadpcm = preload; + } + int nframes = WAV64_VADPCM_FRAMES(head.len); + int preload_size = preload_vadpcm + ? ROUND_UP(nframes * vframe_bytes * head.channels, 16) + : ROUND_UP(head.len * head.channels * (head.nbits >> 3), 16); + if (preload && !preload_vadpcm) { + int ub = head.channels * (head.nbits >> 3); + preload_size += SAMPLEBUFFER_MARGIN_UNITS * ub; + preload_size = ROUND_UP(preload_size, 16); + } + int preload_extra_alloc = ROUND_UP( + (head.format == WAV64_FORMAT_RAW || preload_vadpcm) ? 0 : 4096, 16); + int state_size = ROUND_UP(head.state_size, 16); + + // Calculate required allocation + int heap_size = 0; + heap_size += ROUND_UP(sizeof(wav64_state_t), 16); // wav64_state_t + + int heap_off_waveform = heap_size; + if (!wav) heap_size += ROUND_UP(sizeof(wav64_t), 16); // wav64_t (wave + st) + + int heap_off_name = heap_size; + heap_size += ROUND_UP(strlen(file_name) + 1, 16); // Filename - wav->wave.name = file_name; + int heap_off_samples = heap_size; + if (preload) heap_size += preload_size; // Preloaded samples + + int heap_off_preload_end = heap_size; + if (preload) heap_size += preload_extra_alloc + state_size; // Extra allocation for preload + + int heap_off_ext = heap_size; + heap_size += ROUND_UP(ext_size, 16); // Extended header data + + // Allocate heap memory + assert(heap_size % 16 == 0); + void *heap = memalign(16, heap_size); + assertf(heap, "Out of memory"); + if (!wav) wav = heap + heap_off_waveform; + wav->st = heap; + wav->st->ext = heap + heap_off_ext; + wav->st->samples = NULL; + + // Fill waveforms struct + memset(&wav->wave, 0, sizeof(waveform_t)); + wav->wave.name = heap + heap_off_name; + strcpy(heap + heap_off_name, file_name); wav->wave.channels = head.channels; wav->wave.bits = head.nbits; wav->wave.frequency = head.freq; wav->wave.len = head.len; - wav->wave.loop_len = head.loop_len; - wav->current_fd = file_handle; - wav->base_offset = head.start_offset; - wav->format = head.format; + wav->wave.loop_len = head.loop_len; + wav->wave.loop_end = head.loop_end; + wav->wave.state_size = head.state_size; + int wloop_end = head.loop_end ? head.loop_end : head.len; + assertf(head.loop_len >= 0 && head.loop_len <= wloop_end && + head.loop_end >= 0 && head.loop_end <= head.len, + "wav64 %s: invalid loop %d..%d (len %d)", file_name, + wloop_end - (int)head.loop_len, wloop_end, (int)head.len); - assertf(head.format < WAV64_NUM_FORMATS, "Unknown wav64 compression format %d; corrupted file?", head.format); - assertf(head.format < WAV64_NUM_FORMATS && algos[head.format].init != NULL, - "wav64: compression level %d not initialized. Call wav64_init_compression(%d) at initialization time", head.format, head.format); + // Read ext data + read(file_handle, wav->st->ext, ext_size); + data_cache_hit_writeback(wav->st->ext, ext_size); + + // Finish initialization of wav64 state + wav->st->format = head.format; + wav->st->current_fd = file_handle; + wav->st->base_offset = head.start_offset + start_offset; + wav->st->flags = (owned_fd ? WAV64_FLAG_OWNED_FD : 0) | + (preload ? WAV64_FLAG_PRELOAD : 0); + wav->st->rom_base = 0; + ioctl(file_handle, IODFS_GET_ROM_BASE, &wav->st->rom_base); + + // Initialize the compression algorithm + algos[wav->st->format].init(wav, head.state_size); + + // Preload the samples if requested + if (preload) { + data_cache_hit_invalidate(heap + heap_off_samples, preload_size + preload_extra_alloc + state_size); + wav->st->samples = UncachedAddr(heap + heap_off_samples); + + if (wav->wave.format == WAVEFORM_FORMAT_VADPCM) { + wav64_vadpcm_preload(wav, wav->st->samples); + wav->wave.mem = wav->st->samples; + if (algos[wav->st->format].close) + algos[wav->st->format].close(wav); + } else { + samplebuffer_t sbuf; + samplebuffer_init(&sbuf, wav->st->samples, preload_size + preload_extra_alloc, state_size); + int wlen = wav->wave.len; + samplebuffer_set_bps(&sbuf, wav->wave.bits * wav->wave.channels); + samplebuffer_set_waveform(&sbuf, &wav->wave, wav->wave.read); + if (wav->wave.start) wav->wave.start(wav->wave.ctx, &sbuf); + samplebuffer_get(&sbuf, 0, &wlen); + samplebuffer_dma_wait(&sbuf); + rspq_highpri_sync(); + assertf(wlen == wav->wave.len, "wav64: preload failed for %s: wlen=%x/%x", wav->wave.name, wlen, wav->wave.len); + + // Terminal loops: the RSP overreads past len, so pad the margin + // with the loop start. Non-terminal loops keep the release tail + // intact; streamed playback fills overread via #waveform_read. + if (wav->wave.loop_len && (!wav->wave.loop_end || wav->wave.loop_end == wav->wave.len)) { + int ub = wav->wave.channels * (wav->wave.bits >> 3); + int loop_start = wav->wave.len - wav->wave.loop_len; + uint8_t *base = (uint8_t *)wav->st->samples; + for (int i = 0; i < SAMPLEBUFFER_MARGIN_UNITS; i++) + memcpy(base + (wav->wave.len + i) * ub, + base + (loop_start + i % wav->wave.loop_len) * ub, ub); + } - algos[head.format].init(wav); + if (algos[wav->st->format].close) + algos[wav->st->format].close(wav); - lseek(wav->current_fd, wav->base_offset, SEEK_SET); + wav->st = realloc(wav->st, heap_off_preload_end); + wav->st->ext = NULL; + + // Reinitialize as RAW format after preloading + wav->st->format = WAV64_FORMAT_RAW; + algos[wav->st->format].init(wav, head.state_size); + } + + if (wav->st->current_fd >= 0 && (wav->st->flags & WAV64_FLAG_OWNED_FD)) { + close(wav->st->current_fd); + wav->st->current_fd = -1; + wav->st->flags &= ~WAV64_FLAG_OWNED_FD; + } + } + + return wav; +} + + +void wav64_open(wav64_t *wav, const char *file_name) +{ + internal_open(wav, -1, file_name, NULL); +} + +wav64_t* wav64_load(const char *file_name, wav64_loadparms_t *parms) +{ + return internal_open(NULL, -1, file_name, parms); +} + +wav64_t* wav64_loadfd(int fd, const char *debug_file_name, wav64_loadparms_t *parms) +{ + return internal_open(NULL, fd, debug_file_name, parms); } void wav64_play(wav64_t *wav, int ch) { - // Update the context pointer, so that we try to catch cases where the - // wav64_t instance was moved. - wav->wave.ctx = wav; mixer_ch_play(ch, &wav->wave); } +double wav64_seek(wav64_t *wav, int ch, double time_sec) +{ + // Compute a target sample position (rounded to nearest integer sample) + int spos = (int)(time_sec * wav->wave.frequency + 0.5); + spos = CLAMP(spos, 0, wav->wave.len); + + // Apply codec-specific seek constraints (if any). + if (algos[wav->st->format].adjust_seek) + spos = algos[wav->st->format].adjust_seek(wav, spos); + + mixer_ch_set_pos(ch, spos); + + // Return the adjusted time in seconds + return (double)spos / wav->wave.frequency; +} + void wav64_set_loop(wav64_t *wav, bool loop) { + // Full-file terminal loop (or disarm). Sustain loops with a release tail + // come from the file header; use #mixer_ch_set_loop to toggle those. wav->wave.loop_len = loop ? wav->wave.len : 0; + wav->wave.loop_end = 0; // Odd loop lengths are not supported for 8-bit waveforms because they would // change the 2-byte phase between ROM and RDRAM addresses during loop unrolling. @@ -165,23 +348,54 @@ void wav64_set_loop(wav64_t *wav, bool loop) { } int wav64_get_bitrate(wav64_t *wav) { - if (algos[wav->format].get_bitrate) - return algos[wav->format].get_bitrate(wav); + if (algos[wav->st->format].get_bitrate) + return algos[wav->st->format].get_bitrate(wav); return algos[WAV64_FORMAT_RAW].get_bitrate(wav); } void wav64_close(wav64_t *wav) { + // Heap allocation always begins at wav->st. + void *heap = wav->st; + + // For user-allocated wav64_t instances (opened via wav64_open), we allowed + // in the past to call this function multiple times without crashing. Let's + // keep this working, as it's easy to do. + if (!heap) + return; + // Stop playing the waveform on all channels - __mixer_wave_stopall(&wav->wave); + for (int i=0; iwave) + mixer_ch_stop(i); + } - if (algos[wav->format].close) - algos[wav->format].close(wav); + // Wait for in-flight mix/decode rounds that may still reference + // codebook/state/ext before freeing the heap. + rspq_highpri_sync(); - if (wav->current_fd >= 0) { - close(wav->current_fd); - wav->current_fd = -1; + if (algos[wav->st->format].close) + algos[wav->st->format].close(wav); + + if (wav->st->current_fd >= 0 && (wav->st->flags & WAV64_FLAG_OWNED_FD)) { + close(wav->st->current_fd); + wav->st->current_fd = -1; } + + memset(wav, 0, sizeof(wav64_t)); + + // Free the heap allocation (that might or might not include the wav64_t instance) + free(heap); +} + +/** @brief Initialize wav64 compression level 2 */ +void __wav64_init_compression_lvl2(void) +{ + algos[WAV64_FORMAT_ULC] = (wav64_compression_t){ + .init = wav64_ulc_init, + .get_bitrate = wav64_ulc_get_bitrate, + .adjust_seek = wav64_ulc_adjust_seek, + }; } /** @brief Initialize wav64 compression level 3 */ @@ -191,5 +405,6 @@ void __wav64_init_compression_lvl3(void) .init = wav64_opus_init, .close = wav64_opus_close, .get_bitrate = wav64_opus_get_bitrate, + .adjust_seek = wav64_opus_adjust_seek, }; } diff --git a/src/audio/wav64_internal.h b/src/audio/wav64_internal.h index 17f11688a..792308a63 100644 --- a/src/audio/wav64_internal.h +++ b/src/audio/wav64_internal.h @@ -5,71 +5,83 @@ #ifndef __LIBDRAGON_WAV64_INTERNAL_H #define __LIBDRAGON_WAV64_INTERNAL_H +#include "n64types.h" +#include "mixer_internal.h" + #define WAV64_ID "WV64" ///< WAV64 file identifier -#define WAV64_FILE_VERSION 2 ///< WAV64 file version #define WAV64_FORMAT_RAW 0 ///< Raw audio format #define WAV64_FORMAT_VADPCM 1 ///< VADPCM compressed format +#define WAV64_FORMAT_ULC 2 ///< ULC compressed format #define WAV64_FORMAT_OPUS 3 ///< Opus compressed format #define WAV64_NUM_FORMATS 4 ///< Number of supported formats +#define WAV64_FLAG_OWNED_FD (1 << 0) ///< Flag indicating the file descriptor is owned by the wav64 structure +#define WAV64_FLAG_PRELOAD (1 << 1) ///< Waveform will be fully preloaded in RDRAM + /// @cond typedef struct wav64_s wav64_t; +typedef struct wav64_loadparms_s wav64_loadparms_t; typedef struct samplebuffer_s samplebuffer_t; /// @endcond /** @brief Header of a WAV64 file. */ typedef struct __attribute__((packed)) { char id[4]; ///< ID of the file (WAV64_ID) - int8_t version; ///< Version of the file (WAV64_FILE_VERSION) + int8_t version; ///< Version of the file int8_t format; ///< Format of the file (WAV64_FORMAT_RAW) int8_t channels; ///< Number of interleaved channels int8_t nbits; ///< Width of sample in bits (8 or 16) int32_t freq; ///< Default playback frequency int32_t len; ///< Length of the file (in samples) - int32_t loop_len; ///< Length of the loop since file end (or 0 if no loop) - int32_t start_offset; ///< Offset of the first sample in the file + int32_t loop_len; ///< Length of the sustain loop (0 if no loop) + int32_t loop_end; ///< Exclusive end of the sustain loop (0 ⇒ #len) + uint32_t start_offset; ///< Offset of the first sample in the file + uint32_t state_size; ///< Size of per-mixer-channel state to allocate at runtime } wav64_header_t; -_Static_assert(sizeof(wav64_header_t) == 24, "invalid wav64_header size"); - -/** @brief A vector of audio samples */ -typedef struct __attribute__((aligned(8))) { - int16_t v[8]; ///< Samples -} wav64_vadpcm_vector_t; +_Static_assert(sizeof(wav64_header_t) == 32, "invalid wav64_header size"); -/** @brief Extended header for a WAV64 file with VADPCM compression. */ -typedef struct __attribute__((packed, aligned(8))) { - int8_t npredictors; ///< Number of predictors - int8_t order; ///< Order of the predictors - uint16_t padding; ///< padding - uint32_t padding1; ///< padding1 - wav64_vadpcm_vector_t loop_state[2];///< State at the loop point - wav64_vadpcm_vector_t state[2]; ///< Current decompression state - wav64_vadpcm_vector_t codebook[]; ///< Codebook of the predictors -} wav64_header_vadpcm_t; +/** @brief WAV64 state */ +typedef struct wav64_state_s { + int format; ///< Internal format of the file + void *ext; ///< Pointer to extended header data (format-dependent) + void *samples; ///< Pointer to the preloaded samples (if streaming is disabled) + int current_fd; ///< File descriptor for the wav64 file + int base_offset; ///< Start of Wav64 data (as offset from start of the file) + /** PI bus address of file start (0 if not a DFS file / async DMA unavailable). */ + pi_addr_t rom_base; + uint8_t flags; ///< Misc flags + /** + * First N compressed VADPCM frames kept in RDRAM so a note-on from the + * start of the waveform does not wait on the PI. NULL when unused + * (Huffman, full preload, or non-ROM source). #attack_n is the frame count. + */ + void *attack; + uint16_t attack_n; + /** Codec side-data for in-mixer VADPCM mono (will be stored in waveform_t::codec). */ + waveform_vadpcm_t vadpcm; +} wav64_state_t; /** @brief WAV64 pluggable compression algorithm */ typedef struct { /** @brief Init function: parses extra header information for the specific codec */ - void (*init)(wav64_t *wav); + void (*init)(wav64_t *wav, int state_size); /** @brief Close function: deallocates memory for codec-specific data */ void (*close)(wav64_t *wav); /** @brief Return the compressed bitrate, mainly used for statistics */ int (*get_bitrate)(wav64_t *wav); + /** + * @brief Adjust a requested seek position (in samples) to a valid codec-specific seek point. + * + * If NULL, the codec supports sample-accurate seeking and no adjustment is performed. + * If non-NULL, the function must return a valid seek point (in samples) for this codec. + */ + int (*adjust_seek)(wav64_t *wav, int wpos); } wav64_compression_t; /** - * @brief Utility function to help implementing #WaveformRead for uncompressed (raw) samples. - * - * This function uses a file descriptor to load samples from ROM into the sample buffer. - */ -void raw_waveform_read(samplebuffer_t *sbuf, int fd, int wpos, int wlen, int bps); + * Similar to #wav64_load, but uses a file descriptor instead of a filename. + */ +wav64_t *wav64_loadfd(int fd, const char *debug_file_name, wav64_loadparms_t *parms); -/** - * @brief Utility function to help implementing #WaveformRead for uncompressed (raw) samples. - * - * This function uses PI DMA to load samples from ROM into the sample buffer. - * Note: Tempory function should be removed when XM64 moves to using FILE*. - */ -void raw_waveform_read_address(samplebuffer_t *sbuf, int rom_addr, int wpos, int wlen, int bps); #endif diff --git a/src/audio/wav64_opus.c b/src/audio/wav64_opus.c index 505ad0d4e..9e996efdf 100644 --- a/src/audio/wav64_opus.c +++ b/src/audio/wav64_opus.c @@ -30,10 +30,12 @@ #include #include -#include +#include #include +#include #include "wav64.h" #include "wav64_internal.h" +#include "wav64_opus_internal.h" #include "samplebuffer.h" #include "debug.h" #include "dragonfs.h" @@ -45,50 +47,74 @@ #include "libopus_internal.h" -/// @brief Wav64 Opus header extension -typedef struct { - uint32_t frame_size; ///< Size of an audioframe in samples - uint32_t max_cmp_frame_size; ///< Maximum compressed frame size in bytes - uint32_t bitrate_bps; ///< Bitrate in bits per second -} wav64_opus_header_ext; +static wav64_opus_seekpoint_t *wav64_opus_find_seekpoint(wav64_opus_header_t *ext, int wpos) { + assert(wpos >= 0); -/// @brief Wav64 Opus state -typedef struct { - wav64_opus_header_ext xhead; ///< Opus header extension - OpusCustomMode *mode; ///< Opus custom mode for this file - OpusCustomDecoder *dec; ///< Opus decoder for this file -} wav64_opus_state; + int lo = 0, hi = ext->num_seekpoints; // [lo, hi) + while (lo < hi) { + int mid = (lo + hi) >> 1; + uint32_t off = ext->seekpoints[mid].sample_offset; + if (off <= (uint32_t)wpos) lo = mid + 1; + else hi = mid; + } + + int idx = lo - 1; + if (idx < 0) return NULL; + return &ext->seekpoints[idx]; +} + +static void waveform_opus_start(void *ctx, samplebuffer_t *sbuf) { + wav64_t *wav = (wav64_t*)sbuf->wave; + wav64_opus_header_t *ext = wav->st->ext; + + OpusCustomDecoder *dec = (OpusCustomDecoder*)CachedAddr(sbuf->state); + int err = opus_custom_decoder_init(dec, ext->mode, wav->wave.channels); + assert(err == OPUS_OK); +} static void waveform_opus_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) { - wav64_t *wav = (wav64_t*)ctx; - wav64_opus_state *st = (wav64_opus_state*)wav->ext; - - if (seeking) { - if (wpos == 0) { - lseek(wav->current_fd, wav->base_offset, SEEK_SET); - opus_custom_decoder_ctl(st->dec, OPUS_RESET_STATE); - } else { - assertf(0, "seeking not support in wav64 with opus compression"); - } - } + wav64_t *wav = (wav64_t*)sbuf->wave; + wav64_opus_header_t *ext = wav->st->ext; + OpusCustomDecoder *dec = (OpusCustomDecoder*)CachedAddr(sbuf->state); + int preroll_frames = 0; + int intra_skip = 0; + + if (seeking) { + if (wpos == 0) { + lseek(wav->st->current_fd, wav->st->base_offset, SEEK_SET); + } else { + wav64_opus_seekpoint_t *sp = wav64_opus_find_seekpoint(ext, wpos); + assertf(sp && sp->sample_offset == (uint32_t)wpos, + "wav64: %s: invalid Opus seeking point: %d", wav->wave.name, wpos); + + // Seek directly to the preroll start frame and reset decoder state. + lseek(wav->st->current_fd, wav->st->base_offset + sp->file_offset_preroll, SEEK_SET); + + // Clamp preroll (in case the target frame is at the beginning of the file) + preroll_frames = MIN(ext->preroll_frames, wpos / ext->frame_size); + + // Round intra_skip to 8 bytes, because our RSP memmove requires 8-byte alignment + intra_skip = ROUND_DOWN(sp->intra_skip, wav->wave.channels == 2 ? 2 : 4); + } + opus_custom_decoder_ctl(dec, OPUS_RESET_STATE); + } // Allocate stack buffer for reading compressed data. Align it to cacheline // to avoid any false sharing. - uint8_t alignas(16) buf[st->xhead.max_cmp_frame_size + 1]; - int nframes = DIVIDE_CEIL(wlen, st->xhead.frame_size); - - // Make space for the decoded samples. Call samplebuffer_append once as we - // use RSP in background, and each call to the function might trigger a - // memmove of internal samples. - int16_t *out = samplebuffer_append(sbuf, st->xhead.frame_size*nframes); - - for (int i=0; imax_cmp_frame_size + 1]; + int nframes = DIVIDE_CEIL(wlen + intra_skip, ext->frame_size); + + // Decode into one contiguous append (Opus frames are larger than the + // default samplebuffer margin; declare append_units so the mirrored + // tail covers a whole frame). + int16_t *out = samplebuffer_append(sbuf, ext->frame_size*nframes); + for (int i=0; iwave.len); // Read frame size uint16_t nb = 0; - read(wav->current_fd, &nb, 2); - assertf(nb <= st->xhead.max_cmp_frame_size, "opus frame size too large: %08X (%ld)", nb, st->xhead.max_cmp_frame_size); + read(wav->st->current_fd, &nb, 2); + assertf(nb <= ext->max_cmp_frame_size, "opus frame size too large: %08X (%ld)", nb, ext->max_cmp_frame_size); unsigned long aligned_frame_size = nb; if (aligned_frame_size & 1) { @@ -97,58 +123,68 @@ static void waveform_opus_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wl // Read frame data_cache_hit_writeback_invalidate(buf, aligned_frame_size); - int size = read(wav->current_fd, buf, aligned_frame_size); + int size = read(wav->st->current_fd, buf, aligned_frame_size); assertf(size == aligned_frame_size, "opus read past end: %d", size); // Decode frame - int err = opus_custom_decode(st->dec, buf, nb, out, st->xhead.frame_size); + int err = opus_custom_decode(dec, buf, nb, out, ext->frame_size); assertf(err > 0, "opus decode error: %s", opus_strerror(err)); - assertf(err == st->xhead.frame_size, "opus wrong frame size: %d (exp: %lx)", err, st->xhead.frame_size); - - out += st->xhead.frame_size * wav->wave.channels; - wpos += st->xhead.frame_size; - wlen -= st->xhead.frame_size; + assertf(err == ext->frame_size, "opus wrong frame size: %d (exp: %lx)", err, ext->frame_size); + + int frame_size = ext->frame_size; + if (i == preroll_frames && intra_skip > 0) { + rspq_highpri_begin(); + rsp_opus_memmove_bytes(out, out + intra_skip * wav->wave.channels, (ext->frame_size - intra_skip) * wav->wave.channels * sizeof(int16_t)); + rspq_highpri_end(); + samplebuffer_undo(sbuf, intra_skip); + frame_size -= intra_skip; + } + if (i >= preroll_frames) { + out += frame_size * wav->wave.channels; + wpos += frame_size; + wlen -= frame_size; + } } if (wav->wave.loop_len && wpos >= wav->wave.len) { assert(wav->wave.loop_len == wav->wave.len); - samplebuffer_undo(sbuf, wpos - wav->wave.len); + // Round the trim down for the same reason intra_skip is rounded above: + // the decoder writes through SP DMA, so the write cursor has to stay on + // a boundary the RSP can write to (see #samplebuffer_align_units), here + // for the loop overread the mixer appends right after this. + samplebuffer_undo(sbuf, ROUND_DOWN(wpos - wav->wave.len, samplebuffer_align_units(sbuf))); } } -void wav64_opus_init(wav64_t *wav) { - wav64_opus_header_ext xhead; - read(wav->current_fd, &xhead, sizeof(xhead)); - +void wav64_opus_init(wav64_t *wav, int state_size) { rsp_opus_init(); + wav64_opus_header_t *ext = wav->st->ext; int err = OPUS_OK; - OpusCustomMode *custom_mode = opus_custom_mode_create(wav->wave.frequency, xhead.frame_size, &err); + ext->mode = opus_custom_mode_create(wav->wave.frequency, ext->frame_size, &err); assertf(err == OPUS_OK, "%i", err); - OpusCustomDecoder *dec = opus_custom_decoder_create(custom_mode, wav->wave.channels, &err); - assert(err == OPUS_OK); + assertf(state_size >= opus_custom_decoder_get_size(ext->mode, wav->wave.channels), + "wav64: opus state_size=%d calc_size=%d\n", state_size, opus_custom_decoder_get_size(ext->mode, wav->wave.channels)); - // FIXME: try to avoid one allocation by allocating the decoder in the same malloc - wav64_opus_state *state = malloc(sizeof(wav64_opus_state)); - state->mode = custom_mode; - state->dec = dec; - state->xhead = xhead; - - wav->ext = state; wav->wave.read = waveform_opus_read; - wav->wave.ctx = wav; + wav->wave.start = waveform_opus_start; + wav->wave.append_units = ext->frame_size; + wav->wave.rsp_written = true; } void wav64_opus_close(wav64_t *wav) { - wav64_opus_state *st = (wav64_opus_state*)wav->ext; - - opus_custom_decoder_destroy(st->dec); - opus_custom_mode_destroy(st->mode); - free(st); - wav->ext = NULL; + wav64_opus_header_t *ext = wav->st->ext; + if (ext->mode) opus_custom_mode_destroy(ext->mode); + ext->mode = NULL; } int wav64_opus_get_bitrate(wav64_t *wav) { - wav64_opus_state *st = (wav64_opus_state*)wav->ext; - return st->xhead.bitrate_bps; + wav64_opus_header_t *ext = wav->st->ext; + return ext->bitrate_bps; +} + +int wav64_opus_adjust_seek(wav64_t *wav, int wpos) { + wav64_opus_header_t *ext = wav->st->ext; + wav64_opus_seekpoint_t *sp = wav64_opus_find_seekpoint(ext, wpos); + return sp ? (int)sp->sample_offset : 0; } diff --git a/src/audio/wav64_opus_internal.h b/src/audio/wav64_opus_internal.h index 36d83e743..4cff50afb 100644 --- a/src/audio/wav64_opus_internal.h +++ b/src/audio/wav64_opus_internal.h @@ -9,10 +9,41 @@ #include #include +#include "opus/modes.h" #include "wav64.h" +/** + * @brief WAV64 Opus seek point entry (stored in the extended header) + * + * file_offset_preroll is relative to wav->st->base_offset (start of samples data). + * intra_skip is the number of samples to skip at the beginning of the target frame. + */ +typedef struct __attribute__((packed)) { + uint32_t sample_offset; ///< Absolute sample offset (wpos) of this seek point + uint32_t file_offset_preroll; ///< Byte offset (relative to base_offset) of preroll start frame + uint16_t intra_skip; ///< Samples to skip inside the target frame + uint16_t padding; ///< Padding / reserved +} wav64_opus_seekpoint_t; + +/** + * @brief Wav64 Opus header extension + * + * This structure is stored in the extended header area right after wav64_header_t. + * It is followed by an array of wav64_opus_seekpoint_t entries. + */ +typedef struct __attribute__((packed, aligned(8))) { + uint32_t frame_size; ///< Size of an audio frame in samples + uint32_t max_cmp_frame_size; ///< Maximum compressed frame size in bytes + uint32_t bitrate_bps; ///< Bitrate in bits per second + OpusCustomMode* mode; ///< Opus custom mode pointer (at runtime) + uint16_t preroll_frames; ///< Number of preroll frames to decode/discard after seeking + uint16_t num_seekpoints; ///< Number of seek points entries following this header + uint32_t reserved; ///< Reserved / padding + wav64_opus_seekpoint_t seekpoints[]; ///< Seek points table +} wav64_opus_header_t; + /** @brief Initialize opus decompression on a wav64 file */ -void wav64_opus_init(wav64_t *wav); +void wav64_opus_init(wav64_t *wav, int state_size); /** @brief Shut down opus decompression on a wav64 file */ void wav64_opus_close(wav64_t *wav); @@ -20,4 +51,7 @@ void wav64_opus_close(wav64_t *wav); /** @brief Return the bitrate for a wav64 file */ int wav64_opus_get_bitrate(wav64_t *wav); +/** @brief Adjust a requested seek position (samples) to the previous Opus seek point (samples). */ +int wav64_opus_adjust_seek(wav64_t *wav, int wpos); + #endif diff --git a/src/audio/wav64_ulc.c b/src/audio/wav64_ulc.c new file mode 100644 index 000000000..8d65f22f6 --- /dev/null +++ b/src/audio/wav64_ulc.c @@ -0,0 +1,617 @@ +/** + * @file wav64_ulc.c + * @author Dominic Szablewski + * @brief Support for ulc-compressed WAV64 files + * + * ULC (Ultra-Low-Complexity Codec) is an MDCT-based transform codec, created + * by Aikku93 - https://github.com/Aikku93. For encoding, PCM audio is split + * into fixed-size blocks, transformed into frequency-domain coefficients, + * quantized, and then stored with a compact scheme. Each coefficient, zero run + * or noise fill is described by one or more nibbles, i.e. the stream is + * 4 bit aligned, though every new block starts at a byte boundary. + * + * This decoder is based on https://github.com/Aikku93/ulc-codec - the reference + * implementation and adapted for the N64. Specifically the IMDCT, lapping + * output scaling and stereo interleaving is done on the RSP, while bistream + * reading and unpacking of coefficients remains on the CPU. + * + * The RSP works entirely on 16bit values, interpreted as fixed point with + * 14 fractional bits. This produces a higher noise floor than the reference + * decoder, but with the benefit of fitting a block of 1024 coefficients/samples + * on the RSP. This is also the reason the block size of 1024 is fixed for this + * implementation, while ULC itself supports different block sizes. + * + * Great care has been taken to make this implementation as free of rspq_wait() + * and rspq_highpri_sync() calls as possible. Coefficients are unpacked into the + * space provided by samplebuffer_apppend(). For mono output the RSP can + * directly overwrite the coefficients. For stereo, we have to store the planar + * mid/side samples into a temp buffer and then - through another command in the + * RSP overlay - interleave them and write them to the samplebuffer. + * + * Because ULC has 2 blocks of "preroll" that produce no output, we need two + * more temp buffers to store the coefficients for those blocks. Otherwise the + * CPU decoder risks overwriting coefficients before the RSP has consumed them. + * + * To allow seeking to arbitrary position inside the stream, a sparse table with + * the byte offset of each 8th block is appended at the end of the wav64 file. + * We read the offset for the closest block before the target from this table + * and then seek through the bitstream until we reach the target. This sparse + * table adds just ~21 bytes per second to the encoded file size and requires + * no previously defined seek-points. + */ + +#include +#include +#include +#include +#include +#include +#include "wav64.h" +#include "wav64_internal.h" +#include "wav64_ulc_internal.h" +#include "samplebuffer.h" +#include "n64sys.h" +#include "rspq.h" +#include "utils.h" + +/** @brief Fixed transform block size (samples). + * + * Fixed so the RSP code can make size assumptions; 1024 is also the + * maximum that fits in RSP DMEM. + */ +#define ULC_BLOCK_SIZE 1024 + +/** @brief Number of blocks between consecutive seek-table entries. */ +#define ULC_SEEK_INTERVAL_BLOCKS 8 + +/** @brief Codec delay blocks that must be decoded before valid output. + * + * Required at stream start and after seeking. + */ +#define ULC_PREROLL_BLOCKS 2 + +/** @brief Per-channel ULC decoder state + * + * Stored in the samplebuffer state area. Immediately after this struct + * (64-byte aligned) live #temp_buffer and #transform_inv_lap. + */ +typedef struct ulc_state_t { + int channels; ///< Number of channels (1 or 2) + int last_sb_size; ///< Size of the last decoded subblock (for overlap) + uint32_t stream_offset; ///< Byte offset of the next compressed block from the stream base + uint32_t block_index; ///< Index of the next block to decode + int16_t *temp_buffer; ///< Scratch for planar mid/side and preroll coefficient retention + int16_t *transform_inv_lap; ///< Retained second half of previous IMDCT transforms (overlap-add) +} ulc_state_t; + +_Static_assert(sizeof(ulc_state_t) == 24, "invalid ULC decoder state size"); + +// Subblock decimation pattern +// Each subblock is coded in 4 bits (LSB to MSB): +// Bit0..2: Subblock shift (ie. block_size >> Shift) +// Bit3: Transient flag (ie. apply overlap scaling to that subblock) +static const uint16_t ulc_decimation_patterns[] = { + 0x0000 | 0x0000, // 0000: N/1 (Unused) + 0x0000 | 0x0008, // 0001: N/1* + 0x0011 | 0x0008, // 0010: N/2*,N/2 + 0x0011 | 0x0080, // 0011: N/2,N/2* + 0x0122 | 0x0008, // 0100: N/4*,N/4,N/2 + 0x0122 | 0x0080, // 0101: N/4,N/4*,N/2 + 0x0221 | 0x0080, // 0110: N/2,N/4*,N/4 + 0x0221 | 0x0800, // 0111: N/2,N/4,N/4* + 0x1233 | 0x0008, // 1000: N/8*,N/8,N/4,N/2 + 0x1233 | 0x0080, // 1001: N/8,N/8*,N/4,N/2 + 0x1332 | 0x0080, // 1010: N/4,N/8*,N/8,N/2 + 0x1332 | 0x0800, // 1011: N/4,N/8,N/8*,N/2 + 0x2331 | 0x0080, // 1100: N/2,N/8*,N/8,N/4 + 0x2331 | 0x0800, // 1101: N/2,N/8,N/8*,N/4 + 0x3321 | 0x0800, // 1110: N/2,N/4,N/8*,N/8 + 0x3321 | 0x8000, // 1111: N/2,N/4,N/8,N/8* +}; + +static uint32_t ulc_rsp_overlay_id = 0; + +/** @brief RSP overlay ID for the ULC decoder. */ +DEFINE_RSP_UCODE(rsp_ulc); + +enum { + ULC_RSP_CMD_synthesize = 0x0, + ULC_RSP_CMD_stereo_interleave = 0x1 +}; + +// These flags control which steps are executed on the RSP for CMD_synthesize. +// - TRANSFORM: apply the inverse MDCT, converting frequency coefficients +// into time-domain samples. +// - LAP: overlap-add the current transform with the retained second half of +// previous transforms, and update the retained lap state. +// - CLEAR_LAP: when seeking we need to clear the lap buffer for the very first +// decoded block or subblock for each channel. Be sure to unset this flag +// after the first call. +// - OUTPUT_DISCARD: preroll blocks need to be decoded and update the lap +// buffer, but we don't need to output samples. +// - SCALE_SAMPLES: for mono we can directly scale from q14 to q15 and write +// the final output to dst. For stereo, scaling is done in the separate +// stereo interleave call. + +#define ULC_RSP_TRANSFORM 0x80000000u ///< Apply the inverse MDCT, converting frequency coefficients into time-domain samples. +#define ULC_RSP_LAP 0x40000000u ///< Overlap-add the current transform with the retained second half of previous transforms, and update the retained lap state. +#define ULC_RSP_CLEAR_LAP 0x20000000u ///< Clear the lap buffer for the very first decoded block or subblock for each channel. +#define ULC_RSP_DISCARD_OUTPUT 0x10000000u ///< Preroll blocks need to be decoded and update the lap buffer, but we don't need to output samples. +#define ULC_RSP_SCALE_SAMPLES 0x08000000u ///< For mono we can directly scale from q14 to q15 and write the final output to dst. For stereo, scaling is done in the separate stereo interleave call. + + +// The RSP code works in int16 and assumes 14 fractional bits. We need to scale +// decoded coefficients accordingly. + +#define ULC_FP_BITS 14 ///< Fixed point precision bits. + +#define ULC_FP_ONE (1 << ULC_FP_BITS) ///< Fixed point one. + +/** @brief Escape sequence for stop code. */ +#define ULC_ESCAPE_SEQUENCE_STOP -1 + +/** @brief Escape sequence for noise fill to end. */ +#define ULC_ESCAPE_SEQUENCE_STOP_NOISEFILL -2 + +static inline uint32_t ulc_rand(void) { + static uint32_t seed = 1234567; + seed ^= seed << 13; // Xorshift + seed ^= seed >> 17; + seed ^= seed << 5; + return seed; +} + +static inline uint8_t ulc_read_nibble(const uint8_t **src, int *size) { + // Fetch and shift nybble + uint8_t x = *(*src); + *size += 4; + if ((*size) % 8u == 0) { + x >>= 4; + (*src)++; + } + return x & 0xF; +} + +static inline int ulc_read_quantizer(const uint8_t **src, int *size) { + // Fh,0h..Dh: Quantizer change + int qi = ulc_read_nibble(src, size); + if (qi == 0xF) { + // Fh,Fh,Zh,Yh,Xh: Noise fill (to end; exp-decay) + return ULC_ESCAPE_SEQUENCE_STOP_NOISEFILL; + } + if (qi == 0xE) { + // Fh,Eh,0h..Ch: Quantizer change (extended precision) + qi += ulc_read_nibble(src, size); + if (qi == 0xE + 0xF) { + return ULC_ESCAPE_SEQUENCE_STOP; + } + } + return qi; +} + +static inline float ulc_expand_quantizer(int qi) { + return ULC_FP_ONE * 0x1.0p-31f * ((1u<<(31-5)) >> qi); //! q14 / (2^5 * 2^qi) +} + +static void ulc_decode_coeffs(int16_t *coeff_dst, int sb_size, const uint8_t **src, int *size) { + int32_t n, v; + + // Check first quantizer for Stop code + v = ulc_read_quantizer(src, size); + if (v == ULC_ESCAPE_SEQUENCE_STOP) { + // [Fh,]Eh,Fh: Stop + do { + *coeff_dst++ = 0; + } while(--sb_size); + return; + } + + // Unpack the [sub]block's coefficients + int quantizer = v; + while (sb_size > 0) { + v = ulc_read_nibble(src, size); + + // -7h..-2h, +2..+7h: Normal coefficient + if (!(0x8103u & (1u << v))) { + // Dequantization is an exact power-of-two scale: + // q14 * 2^-5 * 2^-quantizer. + v = (v ^ 0x8) - 0x8; + int magnitude = (v * v * (ULC_FP_ONE >> 5)) >> quantizer; + *coeff_dst++ = v < 0 ? -magnitude : magnitude; + sb_size--; + } + + // 0h,0h..Fh: Zeros fill (1 .. 16 coefficients) + else if (v == 0x0) { + n = ulc_read_nibble(src, size) + 1; + sb_size -= n; + do { + *coeff_dst++ = 0; + } while (--n); + } + + // 1h,Yh,Xh: 33 .. 288 zeros fill + else if (v == 0x1) { + n = ulc_read_nibble(src, size); + n = ulc_read_nibble(src, size) | (n<<4); + n += 33; + sb_size -= n; + do { + *coeff_dst++ = 0; + } while (--n); + } + + // 8h,Zh,Yh,Xh: 16 .. 527 noise fill + else if (v == 0x8) { + n = ulc_read_nibble(src, size); + n = ulc_read_nibble(src, size) | (n<<4); + v = ulc_read_nibble(src, size); + n = (v&1) | (n<<1); + v = (v>>1) + 1; + n += 16; + sb_size -= n; + + float p = v * v * ulc_expand_quantizer(quantizer) * (1.0f/4.0f); + do { + if (ulc_rand() & 0x80000000) { + p = -p; + } + *coeff_dst++ = p; + } while (--n); + } + + // Fh,0h..Dh: quantizer change + else /* if (v == 0xF) */ { + // Fh,Eh,0h..Ch: quantizer change (extended precision) + v = ulc_read_quantizer(src, size); + if (v >= 0) { + quantizer = v; + } + + // Both noise-fill-to-end and zeros-fill-to-end finish the subblock. + else { + // Fh,Fh,Zh,Yh,Xh: Noise fill (to end; exp-decay) + if (v == ULC_ESCAPE_SEQUENCE_STOP_NOISEFILL) { + v = ulc_read_nibble(src, size) + 1; + n = ulc_read_nibble(src, size); + n = ulc_read_nibble(src, size) | (n<<4); + float p = v * v * ulc_expand_quantizer(quantizer) * (1.0f/16.0f); + float r = 1.0f + n * n * -0x1.0p-19f; + do { + if (ulc_rand() & 0x80000000) { + p = -p; + } + *coeff_dst++ = p; + p = p * r; + } while (--sb_size); + } + + // Fh,Eh,Dh: Unused + // Fh,Eh,Eh: Unused + // Fh,Eh,Fh: Zeros fill (to end) + else if (v == ULC_ESCAPE_SEQUENCE_STOP) { + do { + *coeff_dst++ = 0; + } while (--sb_size); + } + } + } + } +} + +static inline void ulc_rsp_synthesize(int16_t *coeffs, int16_t *lap, int16_t *dst, int flags) { + rspq_write(ulc_rsp_overlay_id, ULC_RSP_CMD_synthesize, PhysicalAddr(coeffs), PhysicalAddr(lap), PhysicalAddr(dst), flags); +} + +static inline void ulc_rsp_stereo_interleave(int16_t *mid, int16_t *side, int16_t *dst) { + rspq_write(ulc_rsp_overlay_id, ULC_RSP_CMD_stereo_interleave, PhysicalAddr(mid), PhysicalAddr(side), PhysicalAddr(dst)); +} + +static int ulc_decode_block(ulc_state_t *state, int16_t *dst, const uint8_t *src, bool clear_lap, int preroll_index) { + int channels = state->channels; + int16_t *lap_buffer = state->transform_inv_lap; + + int size = 0; + int last_sb_size = 0; + int window_ctrl = ulc_read_nibble(&src, &size); + window_ctrl |= (window_ctrl & 0x8) + ? ulc_read_nibble(&src, &size) << 4 + : 1 << 4; + + for (int c = 0; c < channels; c++) { + // Reset overlap scaling for this channel + last_sb_size = state->last_sb_size; + + // Decode the complete channel before transforming it. The subblocks + // always sum to ULC_BLOCK_SIZE. + struct { + uint16_t offset; + uint16_t size; + uint16_t overlap; + } sb[4]; + int sb_len = 0; + + // Normal coefficients are decoded directly into the final samplebuffer. + // Mono output overwrites them in place, while stereo output is staged + // in the first planar temp bank before interleaving. Each queued + // preroll block needs its own coefficient bank because the RSP might + // not have DMAed an earlier block before the CPU decodes the next one. + int16_t *channel_buffer = (preroll_index >= 0) + ? state->temp_buffer + (preroll_index * channels + c) * ULC_BLOCK_SIZE + : dst + c * ULC_BLOCK_SIZE; + int16_t *channel_output = (channels == 1 || preroll_index >= 0) + ? channel_buffer + : state->temp_buffer + c * ULC_BLOCK_SIZE; + + int coeff_offset = 0; + uint16_t decimation_pattern = ulc_decimation_patterns[window_ctrl >> 4]; + do { + int sb_size = ULC_BLOCK_SIZE >> (decimation_pattern & 0x7); + ulc_decode_coeffs(channel_buffer + coeff_offset, sb_size, &src, &size); + + // Get + update overlap size and limit to that of the last subblock + int overlap = sb_size; + if (decimation_pattern & 0x8) { + overlap >>= (window_ctrl & 0x7); + } + if (overlap > last_sb_size) { + overlap = last_sb_size; + } + last_sb_size = sb_size; + + sb[sb_len].offset = coeff_offset; + sb[sb_len].size = sb_size; + sb[sb_len].overlap = overlap; + sb_len++; + coeff_offset += sb_size; + } while (decimation_pattern >>= 4); + + uint32_t flags = + (channels == 1 ? ULC_RSP_SCALE_SAMPLES : 0) | + (preroll_index >= 0 ? ULC_RSP_DISCARD_OUTPUT : 0) | + (clear_lap ? ULC_RSP_CLEAR_LAP : 0); + + // A full 1024-sample block can remain on the RSP through transform, + // overlap-add, lap-state update, and final output DMA. + if (sb_len == 1) { + uint32_t sb_flags = flags | ULC_RSP_TRANSFORM | ULC_RSP_LAP | (sb[0].overlap << 16) | ULC_BLOCK_SIZE; + ulc_rsp_synthesize(channel_buffer, lap_buffer, channel_output, sb_flags); + } + + // Multiple subblocks need to be transformed and _then_ lapped. + else { + for (int i = 0; i < sb_len; i++) { + uint32_t sb_flags = ULC_RSP_TRANSFORM | sb[i].size; + int16_t *sb_buffer = channel_buffer + sb[i].offset; + ulc_rsp_synthesize(sb_buffer, 0, sb_buffer, sb_flags); + } + for (int i = 0; i < sb_len; i++) { + uint32_t sb_flags = flags | ULC_RSP_LAP | (sb[i].overlap << 16) | sb[i].size; + int16_t *sb_buffer = channel_buffer + sb[i].offset; + int16_t *sb_output = channel_output + sb[i].offset; + ulc_rsp_synthesize(sb_buffer, lap_buffer, sb_output, sb_flags); + flags &= ~ULC_RSP_CLEAR_LAP; + } + } + + // Move to next channel + lap_buffer += ULC_BLOCK_SIZE >> 1; + } + + if (channels == 2 && preroll_index < 0) { + ulc_rsp_stereo_interleave(state->temp_buffer, state->temp_buffer + ULC_BLOCK_SIZE, dst); + } + + // Store the last [sub]block size, and return the number of bits read + state->last_sb_size = last_sb_size; + return size; +} + +static int ulc_skip_block(const ulc_state_t *state, const uint8_t *src) { + int size = 0; + int window_ctrl = ulc_read_nibble(&src, &size); + window_ctrl |= (window_ctrl & 0x8) + ? ulc_read_nibble(&src, &size) << 4 + : 1 << 4; + + for (int c = 0; c < state->channels; c++) { + uint16_t decimation_pattern = ulc_decimation_patterns[window_ctrl >> 4]; + do { + int sb_size = ULC_BLOCK_SIZE >> (decimation_pattern & 0x7); + int32_t v = ulc_read_quantizer(&src, &size); + if (v != ULC_ESCAPE_SEQUENCE_STOP) { + while (sb_size > 0) { + v = ulc_read_nibble(&src, &size); + if (!(0x8103u & (1u << v))) { + sb_size--; + } + else if (v == 0x0) { + v = ulc_read_nibble(&src, &size) + 1; + sb_size -= v; + } + else if (v == 0x1) { + v = ulc_read_nibble(&src, &size); + v = ulc_read_nibble(&src, &size) | (v << 4); + sb_size -= v + 33; + } + else if (v == 0x8) { + v = ulc_read_nibble(&src, &size); + v = ulc_read_nibble(&src, &size) | (v << 4); + v = (ulc_read_nibble(&src, &size) & 1) | (v << 1); + sb_size -= v + 16; + } + else /* if (v == 0xF) */ { + int v = ulc_read_quantizer(&src, &size); + if (v == ULC_ESCAPE_SEQUENCE_STOP_NOISEFILL) { + ulc_read_nibble(&src, &size); + ulc_read_nibble(&src, &size); + ulc_read_nibble(&src, &size); + break; + } + else if (v == ULC_ESCAPE_SEQUENCE_STOP) { + break; + } + } + } + } + } while (decimation_pattern >>= 4); + } + + return size; +} + + + +// ----------------------------------------------------------------------------- +// wav64 interface + +static int wav64_ulc_state_size(int channels) { + int size = sizeof(ulc_state_t) + 63; + size += sizeof(int16_t) * 2 * channels * ULC_BLOCK_SIZE; + size += sizeof(int16_t) * channels * (ULC_BLOCK_SIZE / 2); + return size; +} + +static void waveform_ulc_start(void *ctx, samplebuffer_t *sbuf) { + wav64_t *wav = (wav64_t *)sbuf->wave; + ulc_state_t *state = (ulc_state_t *)sbuf->state; + + assert(sbuf->state_size >= wav64_ulc_state_size(wav->wave.channels)); + + uintptr_t buffer = ROUND_UP((uintptr_t)(state + 1), 64); + state->channels = wav->wave.channels; + state->temp_buffer = (int16_t *)buffer; + state->transform_inv_lap = state->temp_buffer + 2 * wav->wave.channels * ULC_BLOCK_SIZE; + state->last_sb_size = 0; + state->stream_offset = 0; + state->block_index = 0; +} + +static void waveform_ulc_load_block(wav64_t *wav, const wav64_header_ulc_t *ext, const ulc_state_t *state, uint8_t *input) { + lseek(wav->st->current_fd, wav->st->base_offset + state->stream_offset, SEEK_SET); + + // read() can DMA directly into this cached stack buffer. Invalidate it + // first so the CPU decoder does not consume stale cache lines. + data_cache_hit_writeback_invalidate(input, ext->max_block_size); + int size = read(wav->st->current_fd, input, ext->max_block_size); + assertf(size > 0, "wav64: %s: ULC read past end", wav->wave.name); +} + +static void waveform_ulc_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) { + wav64_t *wav = (wav64_t *)sbuf->wave; + wav64_header_ulc_t *ext = wav->st->ext; + ulc_state_t *state = (ulc_state_t *)sbuf->state; + + bool clear_lap = false; + int preroll_blocks = 0; + int blocks_len = DIVIDE_CEIL(wlen, ULC_BLOCK_SIZE); + + int appended = blocks_len * ULC_BLOCK_SIZE; + int16_t *dst = samplebuffer_append(sbuf, appended); + + uint8_t alignas(16) input[ext->max_block_size]; + + if (seeking) { + assertf(wpos % ULC_BLOCK_SIZE == 0, "wav64: %s: unaligned ULC seek: %d", wav->wave.name, wpos); + state->last_sb_size = 0; + + if (wpos == 0) { + state->stream_offset = 0; + state->block_index = 0; + } + else { + uint32_t target_block = CLAMP(wpos / ULC_BLOCK_SIZE, 0, ext->blocks_len - 3); + uint32_t anchor_block = target_block / ULC_SEEK_INTERVAL_BLOCKS * ULC_SEEK_INTERVAL_BLOCKS; + uint32_t anchor_index = anchor_block / ULC_SEEK_INTERVAL_BLOCKS; + + // Read the block offset from the sparse seek table at the end of the file + lseek(wav->st->current_fd, wav->st->base_offset + ext->seek_table_offset + anchor_index * sizeof(uint32_t), SEEK_SET); + uint32_t anchor_offset; + int size = read(wav->st->current_fd, &anchor_offset, sizeof(anchor_offset)); + assertf(size == sizeof(anchor_offset), "wav64: %s: cannot read ULC seek table", wav->wave.name); + + state->stream_offset = anchor_offset; + state->block_index = anchor_block; + + // Walk from the sparse anchor to the preroll block without doing + // coefficient reconstruction or transforms. This happens entirely + // on the cpu. + while (state->block_index < target_block) { + waveform_ulc_load_block(wav, ext, state, input); + int bits = ulc_skip_block(state, input); + state->stream_offset += DIVIDE_CEIL(bits, 8); + state->block_index++; + } + } + + clear_lap = true; + preroll_blocks = ULC_PREROLL_BLOCKS; + blocks_len += ULC_PREROLL_BLOCKS; + } + + // Batch all the decode commands in a single highpri sequence. When the + // mixer calls us from within its own highpri burst, this simply nests. + rspq_highpri_begin(); + + for (int i = 0; i < blocks_len; i++) { + assertf(state->block_index < ext->blocks_len, "wav64: %s: ULC blocks exhausted: %lu/%lu", wav->wave.name, state->block_index, ext->blocks_len); + + waveform_ulc_load_block(wav, ext, state, input); + int preroll_index = i < preroll_blocks ? i : -1; + int bits = ulc_decode_block(state, dst, input, clear_lap, preroll_index); + state->stream_offset += DIVIDE_CEIL(bits, 8); + state->block_index++; + + clear_lap = false; + if (preroll_index < 0) { + dst += ULC_BLOCK_SIZE * wav->wave.channels; + } + } + + rspq_highpri_end(); + + // Trimming the samples past the end of the waveform has to stop on a + // boundary the RSP can write to (see #samplebuffer_align_units), because + // the mixer appends the loop overread right after, in the same fetch. The + // few samples the block already holds beyond the end are kept, which puts + // the loop point up to three samples late, once per lap. + int align = samplebuffer_align_units(sbuf); + int valid = wav->wave.loop_len + ? MIN(appended, ROUND_UP(wav->wave.len - wpos, align)) : appended; + if (appended > valid) { + samplebuffer_undo(sbuf, appended - valid); + rspq_highpri_sync(); + } +} + +void wav64_ulc_init(wav64_t *wav, int state_size) { + wav64_header_ulc_t *ext = wav->st->ext; + assertf(ext->block_size == ULC_BLOCK_SIZE, "wav64: %s: unsupported ULC block size: %d", wav->wave.name, ext->block_size); + assertf(ext->max_block_size > 0, "wav64: %s: invalid ULC maximum block size", wav->wave.name); + assertf(ext->blocks_len >= 3, "wav64: %s: invalid ULC block count: %lu", wav->wave.name, ext->blocks_len); + assertf(wav->wave.channels >= 1 && wav->wave.channels <= 2, "wav64: %s: unsupported ULC channel count: %d", wav->wave.name, wav->wave.channels); + assertf(state_size >= wav64_ulc_state_size(wav->wave.channels), "wav64: %s: ULC state too small; regenerate your asset files", wav->wave.name); + + if (!ulc_rsp_overlay_id) { + rspq_init(); + ulc_rsp_overlay_id = rspq_overlay_register(&rsp_ulc); + } + + wav->wave.start = waveform_ulc_start; + wav->wave.read = waveform_ulc_read; + wav->wave.append_units = ULC_BLOCK_SIZE; + wav->wave.rsp_written = true; +} + +int wav64_ulc_get_bitrate(wav64_t *wav) { + wav64_header_ulc_t *ext = wav->st->ext; + return ext->bitrate_bps; +} + +int wav64_ulc_adjust_seek(wav64_t *wav, int wpos) { + wav64_header_ulc_t *ext = wav->st->ext; + if (ext->seek_table_offset == 0) { + return 0; + } + return wpos / ULC_BLOCK_SIZE * ULC_BLOCK_SIZE; +} diff --git a/src/audio/wav64_ulc_internal.h b/src/audio/wav64_ulc_internal.h new file mode 100644 index 000000000..05e07feac --- /dev/null +++ b/src/audio/wav64_ulc_internal.h @@ -0,0 +1,38 @@ +/** + * @file wav64_ulc_internal.h + * @author Dominic Szablewski + * @brief Support for ulc-compressed WAV64 files + */ + +#ifndef LIBDRAGON_AUDIO_ULC_INTERNAL_H +#define LIBDRAGON_AUDIO_ULC_INTERNAL_H + +#include +#include "wav64.h" + +/** + * @brief Wav64 ULC header extension + * + * This structure is stored in the extended header area right after wav64_header_t. + */ +typedef struct __attribute__((packed)) { + uint16_t block_size; ///< Transform block size (currently always 1024) + uint16_t max_block_size; ///< Largest compressed block, in bytes + uint32_t blocks_len; ///< Number of blocks, including codec delay/flush blocks + uint32_t bitrate_bps; ///< Actual average bitrate of the encoded stream + uint32_t seek_table_offset; ///< First-block-relative offset of the trailing seek table +} wav64_header_ulc_t; + +_Static_assert(sizeof(wav64_header_ulc_t) == 16, "invalid wav64_header_ulc size"); + +/** @brief Initialize ULC decompression on a WAV64 file. */ +void wav64_ulc_init(wav64_t *wav, int state_size); + +/** @brief Return the average bitrate of a ULC-compressed WAV64 file. */ +int wav64_ulc_get_bitrate(wav64_t *wav); + +/** @brief Adjust a requested seek position to a ULC block boundary. */ +int wav64_ulc_adjust_seek(wav64_t *wav, int wpos); + + +#endif diff --git a/src/audio/wav64_vadpcm.c b/src/audio/wav64_vadpcm.c index 04a8520e1..9b49364b4 100644 --- a/src/audio/wav64_vadpcm.c +++ b/src/audio/wav64_vadpcm.c @@ -10,249 +10,480 @@ #include "mixer.h" #include "mixer_internal.h" #include "samplebuffer.h" +#include "dma.h" #include "utils.h" +#include "n64types.h" +#include "profile.h" #include #include +#include +#include +#include -/** @brief Set to 1 to use the reference C decode for VADPCM */ -#define VADPCM_REFERENCE_DECODER 0 - -#if VADPCM_REFERENCE_DECODER -/** @brief VADPCM decoding errors */ -typedef enum { - // No error (success). Equal to 0. - kVADPCMErrNone, - - // Invalid data. - kVADPCMErrInvalidData, - - // Predictor order is too large. - kVADPCMErrLargeOrder, - - // Predictor count is too large. - kVADPCMErrLargePredictorCount, - - // Data uses an unsupported / unknown version of VADPCM. - kVADPCMErrUnknownVersion, - - // Invalid encoding parameters. - kVADPCMErrInvalidParams, -} vadpcm_error; - -// Extend the sign bit of a 4-bit integer. -static int vadpcm_ext4(int x) { - return x > 7 ? x - 16 : x; +/** + * @brief Find a VADPCM skip point index by sample offset. + * + * Skip points are stored sorted by sample offset (enforced by audioconv64). + * + * @param vhead VADPCM header (contains skip_points) + * @param wpos Requested sample offset + * @param nearest If false, require an exact match (returns -1 if not found). + * If true, return the index of the closest skip point. + * @return Index in vhead->skip_points, or -1 if no skip points / not found. + */ + static int wav64_vadpcm_find_skippoint(wav64_header_vadpcm_t *vhead, int wpos, bool nearest) + { + if (vhead->num_skippoints <= 0 || !vhead->skip_points) + return -1; + + // Find first index with offset >= wpos (lower_bound). + int lo = 0, hi = vhead->num_skippoints; // [lo, hi) + while (lo < hi) { + int mid = (lo + hi) >> 1; + int off = vhead->skip_points[mid].offset; + if (off < wpos) lo = mid + 1; + else hi = mid; + } + + if (!nearest) { + // Exact match required. + if (lo < vhead->num_skippoints && vhead->skip_points[lo].offset == wpos) + return lo; + return -1; + } + + // Nearest match (compare neighbors around insertion point). + if (lo == 0) return 0; + if (lo >= vhead->num_skippoints) return vhead->num_skippoints - 1; + + int off_hi = vhead->skip_points[lo].offset; + int off_lo = vhead->skip_points[lo - 1].offset; + int d_lo = wpos - off_lo; + int d_hi = off_hi - wpos; + return (d_hi < d_lo) ? lo : (lo - 1); } -// Clamp an integer to a 16-bit range. -static int vadpcm_clamp16(int x) { - if (x < -0x8000 || 0x7fff < x) { - return (x >> (sizeof(int) * CHAR_BIT - 1)) ^ 0x7fff; - } - return x; -} +static void huffv_decompress(wav64_t *wav, wav64_state_vadpcm_t *vstate, uint8_t *dst, int len, uint8_t *scratch, int slen) { + PROFILE_SCOPE(PS_VADPCM_HUFF) { + wav64_header_vadpcm_t *vhead = (wav64_header_vadpcm_t*)wav->st->ext; -__attribute__((unused)) -static vadpcm_error vadpcm_decode(int predictor_count, int order, - const wav64_vadpcm_vector_t *restrict codebook, - wav64_vadpcm_vector_t *restrict state, - size_t frame_count, int16_t *restrict dest, - const void *restrict src) { - const uint8_t *sptr = src; - for (size_t frame = 0; frame < frame_count; frame++) { - const uint8_t *fin = sptr + 9 * frame; - - // Control byte: scaling & predictor index. - int control = fin[0]; - int scaling = control >> 4; - int predictor_index = control & 15; - if (predictor_index >= predictor_count) { - return kVADPCMErrInvalidData; - } - const wav64_vadpcm_vector_t *predictor = - codebook + order * predictor_index; - - // Decode each of the two vectors within the frame. - for (int vector = 0; vector < 2; vector++) { - int32_t accumulator[8]; - for (int i = 0; i < 8; i++) { - accumulator[i] = 0; - } + unsigned int bitpos = vstate->bitpos; + // debugf("huffv_decompress: bitpos 0x%x.%d, seek to 0x%x\n", bitpos/8, bitpos&7, wav->st->base_offset +bitpos / 8); + lseek(wav->st->current_fd, wav->st->base_offset + bitpos / 8, SEEK_SET); - // Accumulate the part of the predictor from the previous block. - for (int k = 0; k < order; k++) { - int sample = state->v[8 - order + k]; - for (int i = 0; i < 8; i++) { - accumulator[i] += sample * predictor[k].v[i]; - } - } + // Read the compressed data + read(wav->st->current_fd, CachedAddr(scratch), slen); + uint8_t *src = CachedAddr(scratch); - // Decode the ADPCM residual. - int residuals[8]; - for (int i = 0; i < 4; i++) { - int byte = fin[1 + 4 * vector + i]; - residuals[2 * i] = vadpcm_ext4(byte >> 4); - residuals[2 * i + 1] = vadpcm_ext4(byte & 15); - } + // Decompress the data + uint64_t buffer = 0; + int buffer_bits = 0; - // Accumulate the residual and predicted values. - const wav64_vadpcm_vector_t *v = &predictor[order - 1]; - for (int k = 0; k < 8; k++) { - int residual = residuals[k] << scaling; - accumulator[k] += residual << 11; - for (int i = 0; i < 7 - k; i++) { - accumulator[k + 1 + i] += residual * v->v[i]; - } - } + if (bitpos & 7) { + buffer = src[0]; + buffer_bits = 8 - (bitpos & 7); + src++; + } - // Discard fractional part and clamp to 16-bit range. - for (int i = 0; i < 8; i++) { - int sample = vadpcm_clamp16(accumulator[i] >> 11); - dest[16 * frame + 8 * vector + i] = sample; - state->v[i] = sample; + // The Huffman coder works on nibbles, so it only ever wraps classic 9-byte + // frames: sub-nibble packing turns it off (see wav64_vadpcm_init). + assert(len % 9 == 0); + for (int i = 0; i < len; i += 9) { + wav64_vadpcm_hufftable_t *tbl = vhead->huff_tbl; + + for (int j=0; j<9; j++) { + while (buffer_bits < 32) { + buffer <<= 32; + buffer |= *(u_uint32_t*)src; + src += 4; + assertf(src < scratch + slen, "invalid read past end: %p vs %p", src, scratch + slen); + buffer_bits += 32; } + + uint8_t code1 = tbl->codes[(buffer >> (buffer_bits - 8)) & 0xFF]; + int len1 = code1 & 0xF; + int val1 = code1 >> 4; + assert(len1 <= 8); + buffer_bits -= len1; + bitpos += len1; + if (j == 0) tbl++; + + uint8_t code2 = tbl->codes[(buffer >> (buffer_bits - 8)) & 0xFF]; + int len2 = code2 & 0xF; + int val2 = code2 >> 4; + assert(len2 <= 8); + buffer_bits -= len2; + bitpos += len2; + if (j == 0) tbl++; + + *dst++ = (val1 << 4) | val2; } } - return 0; -} -#else -static inline void rsp_vadpcm_decompress(void *input, int16_t *output, bool stereo, int nframes, - wav64_vadpcm_vector_t *state, wav64_vadpcm_vector_t *codebook) -{ - assert(nframes > 0 && nframes <= 256); - rspq_write(__mixer_overlay_id, 0x1, - PhysicalAddr(input), - PhysicalAddr(output) | (nframes-1) << 24, - PhysicalAddr(state) | (stereo ? 1 : 0) << 31, - PhysicalAddr(codebook)); + vstate->bitpos = bitpos; + assertf((void*)src <= CachedAddr(scratch) + slen, "invalid read past end: %p vs %p", src, scratch + slen); + data_cache_hit_invalidate(CachedAddr(scratch), slen); + } } -#endif /* VADPCM_REFERENCE_DECODER */ +/** + * File-frame index of channel `ch`'s frame `frame` in a block-planar VADPCM + * stream (`nframes` total frames per channel). + */ +static int vadpcm_file_index(int frame, int ch, int nframes, int channels) { + if (channels == 1) return frame; + int B = WAV64_VADPCM_BLOCK_FRAMES; + int block = frame / B; + int off = frame % B; + int nblocks = (nframes + B - 1) / B; + int bs = (block == nblocks - 1) ? (nframes - block * B) : B; + return block * B * channels + ch * bs + off; +} -static void waveform_vadpcm_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) { - wav64_t *wav = (wav64_t*)ctx; - wav64_header_vadpcm_t *vhead = (wav64_header_vadpcm_t*)wav->ext; +/** + * @brief Fill samplebuffer(s) with plain (undecoded) VADPCM frames. + * + * wpos/wlen are in frames. Mono writes into `sbuf`. Stereo also writes the + * right plane into `sbuf+1` (mixer allocates adjacent channel buffers). + * Huffman is expanded to plain frames on the CPU. + * + * Stereo always fills through the end of the current file block so Huffman + * can consume L-then-R contiguously and leave bitpos at the next block. + */ +static void waveform_vadpcm_read_compressed(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) { + PROFILE_SCOPE(PS_VADPCM_READ) { + wav64_t *wav = (wav64_t*)sbuf->wave; + wav64_header_vadpcm_t *vhead = (wav64_header_vadpcm_t*)wav->st->ext; + wav64_state_vadpcm_t *vstate = sbuf->state; + assert(sbuf->state_size >= sizeof(wav64_state_vadpcm_t)); + int channels = wav->wave.channels; + int nframes_total = WAV64_VADPCM_FILE_FRAMES(wav->wave.len); + int fbytes = VADPCM_FRAME_BYTES(wav->st->vadpcm.bits); + samplebuffer_t *sbuf_r = (channels == 2) ? sbuf + 1 : NULL; + (void)ctx; if (seeking) { + int sample_pos = wpos * 16; if (wpos == 0) { - memset(&vhead->state, 0, sizeof(vhead->state)); - lseek(wav->current_fd, wav->base_offset, SEEK_SET); + memset(vstate->state, 0, sizeof(vstate->state)); + vstate->bitpos = 0; } else { - assertf(wpos == wav->wave.len - wav->wave.loop_len, - "wav64: seeking to %x not supported (%x %x)\n", wpos, wav->wave.len, wav->wave.loop_len); - memcpy(&vhead->state, &vhead->loop_state, sizeof(vhead->state)); - lseek(wav->current_fd, (wav->wave.len - wav->wave.loop_len) / 16 * 9, SEEK_CUR); + int idx = wav64_vadpcm_find_skippoint(vhead, sample_pos, false); + assertf(idx >= 0, "wav64: %s: invalid VADPCM seeking point: 0x%x", wav->wave.name, sample_pos); + vstate->bitpos = vhead->skip_points[idx].bitpos; + memcpy(vstate->state, vhead->skip_states + idx * channels, + sizeof(vstate->state[0]) * channels); + } + // The right plane has no reader of its own: it only ever receives what + // this function appends, so its ring must follow the left one. A seek + // that restarts the stream arrives with the left ring already flushed, + // and the right one restarts at wpos as well. An overread past the end + // also arrives as a seek (the decoder has to re-seed at the loop + // point), but there the left ring keeps its live window and just + // appends the loop-start frames after it: restarting the right ring + // would leave the two planes at different stream positions. + if (sbuf_r && sbuf->widx == 0) { + samplebuffer_flush(sbuf_r); + sbuf_r->wpos = wpos; + sbuf_r->head = sbuf->head; } } - // Round up wlen to 32 because our RSP decompressor only supports multiples - // of 32 samples (2 frames) because of DMA alignment issues. audioconv64 - // makes sure files are padded to that length, so this is valid also at the - // end of the file. - wlen = ROUND_UP(wlen, 32); - if (wlen == 0) return; + if (wlen <= 0) goto done; - // Maximum number of VADPCM frames that can be decompressed in a single - // RSP call. Keep this in sync with rsp_mixer.S. - enum { MAX_VADPCM_FRAMES = 94 }; - - bool highpri = false; while (wlen > 0) { - // Calculate number of frames to decompress in this iteration - int max_vadpcm_frames = (wav->wave.channels == 1) ? MAX_VADPCM_FRAMES : MAX_VADPCM_FRAMES / 2; - int nframes = MIN(wlen / 16, max_vadpcm_frames); - - // Acquire destination buffer from the sample buffer - int16_t *dest = (int16_t*)samplebuffer_append(sbuf, nframes*16); - - // Calculate source pointer at the end of the destination buffer. - // VADPCM decoding can be safely made in-place, so no auxillary buffer - // is necessary. - int src_bytes = 9 * nframes * wav->wave.channels; - void *src = (void*)dest + ((nframes*16) << SAMPLES_BPS_SHIFT(sbuf)) - src_bytes; - - // Fetch compressed data - // FIXME: remove CachedAddr() when read() supports uncached addresses - int read_bytes = read(wav->current_fd, CachedAddr(src), src_bytes); - assertf(src_bytes == read_bytes, "invalid read past end: %d vs %d", src_bytes, read_bytes); - - #if VADPCM_REFERENCE_DECODER - if (wav->wave.channels == 1) { - vadpcm_error err = vadpcm_decode( - vhead->npredictors, vhead->order, vhead->codebook, vhead->state, - nframes, dest, src); - assertf(err == 0, "VADPCM decoding error: %d\n", err); + int n; + if (channels == 1) { + n = MIN(wlen, SAMPLEBUFFER_MARGIN_UNITS); + } else { + // Fill through end of block (may exceed wlen; allowed by WaveformRead). + int B = WAV64_VADPCM_BLOCK_FRAMES; + int block_end = (wpos / B + 1) * B; + if (block_end > nframes_total) block_end = nframes_total; + n = block_end - wpos; + assert(n > 0 && n <= B); + } + + uint8_t *dest_l = samplebuffer_append(sbuf, n); + uint8_t *dest_r = sbuf_r ? samplebuffer_append(sbuf_r, n) : NULL; + + if (vhead->flags & VADPCM_FLAG_HUFFMAN) { + if (channels == 1) { + int nbytes = n * fbytes; + int scratch_size = ROUND_UP(nbytes * 2, 16); + // data_cache_hit_invalidate requires a 16-byte aligned address; + // alloca only guarantees the platform ABI alignment. + uint8_t *scratch = (uint8_t *)ROUND_UP((uintptr_t)alloca(scratch_size + 15), 16); + huffv_decompress(wav, vstate, dest_l, nbytes, scratch, scratch_size); + } else { + // From bitpos at L_wpos: stream is L_wpos..L_end, R_0..R_end of block. + int B = WAV64_VADPCM_BLOCK_FRAMES; + int block = wpos / B; + int off = wpos % B; + int nblocks = (nframes_total + B - 1) / B; + int bs = (block == nblocks - 1) ? (nframes_total - block * B) : B; + int n_l = bs - off; // L frames still in this block + int n_r = bs; // full R plane of this block + assert(n_l == n); + int span = n_l + n_r; + int nbytes = span * fbytes; + int scratch_size = ROUND_UP(nbytes * 2, 16); + uint8_t *scratch = (uint8_t *)ROUND_UP((uintptr_t)alloca(scratch_size + 15), 16); + uint8_t *spanbuf = (uint8_t *)ROUND_UP((uintptr_t)alloca(nbytes + 15), 16); + huffv_decompress(wav, vstate, spanbuf, nbytes, scratch, scratch_size); + memcpy(dest_l, spanbuf, n * fbytes); + memcpy(dest_r, spanbuf + (n_l + off) * fbytes, n * fbytes); + } } else { - assert(wav->wave.channels == 2); - int16_t uncomp[2][16]; - int16_t *dst = dest; - - for (int i=0; inpredictors, vhead->order, vhead->codebook + 8*j, &vhead->state[j], - 1, uncomp[j], src); - assertf(err == 0, "VADPCM decoding error: %d\n", err); - src += 9; + PROFILE_SCOPE(PS_VADPCM_IO) { + for (int c = 0; c < channels; c++) { + uint8_t *dest = (c == 0) ? dest_l : dest_r; + samplebuffer_t *dstbuf = (c == 0) ? sbuf : sbuf_r; + int fidx = vadpcm_file_index(wpos, c, nframes_total, channels); + int nbytes = n * fbytes; + // Serve the attack window from RDRAM: every note-on starts + // here, and the PI would otherwise serialize a cold DMA per + // channel on the same row. + int attack_n = wav->st->attack_n; + if (c == 0 && wav->st->attack && wpos < attack_n) { + int ncache = MIN(n, attack_n - wpos); + memcpy(dest, (uint8_t*)wav->st->attack + wpos * fbytes, + ncache * fbytes); + if (ncache == n) + continue; + dest += ncache * fbytes; + fidx = vadpcm_file_index(wpos + ncache, c, nframes_total, channels); + nbytes = (n - ncache) * fbytes; } - for (int j=0; j<16; j++) { - *dst++ = uncomp[0][j]; - *dst++ = uncomp[1][j]; + pi_addr_t pi_addr = wav->st->rom_base + wav->st->base_offset + fidx * fbytes; + if (wav->st->rom_base && !(((uint32_t)dest ^ pi_addr) & 1)) { + // A prior chunk of this same WaveformRead may still be in + // flight; wait before replacing the ticket. + samplebuffer_dma_wait(dstbuf); + dstbuf->dma_ticket = dma_read_async(dest, pi_addr, nbytes); + } else { + // This fills the ring through the cache, and a cached write + // carries whole lines: the bytes it shares with the chunk + // before it are read now and written back later, so a DMA + // still filling them would be undone. Frames are 9 bytes, + // so that neighbour is almost always there. + samplebuffer_dma_wait(dstbuf); + lseek(wav->st->current_fd, wav->st->base_offset + fidx * fbytes, SEEK_SET); + int read_bytes = read(wav->st->current_fd, CachedAddr(dest), nbytes); + assertf(nbytes == read_bytes, "invalid read past end: %d vs %d", nbytes, read_bytes); + data_cache_hit_writeback_invalidate(CachedAddr(dest), nbytes); } } + } } - #else - // Switch to highpri as late as possible - if (!highpri) { - rspq_highpri_begin(); - highpri = true; - } - rsp_vadpcm_decompress(src, dest, wav->wave.channels==2, nframes, vhead->state, vhead->codebook); - #endif - wlen -= 16*nframes; - wpos += 16*nframes; + if (sbuf_r) + sbuf_r->wnext = sbuf_r->wpos + sbuf_r->widx; + + wlen -= n; + if (wlen < 0) wlen = 0; + wpos += n; + } +done: ; + } +} + +/** @brief Seek VADPCM predictor state for a resident (preloaded) waveform. */ +static void waveform_vadpcm_seek_state(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) { + (void)ctx; (void)wlen; + if (!seeking) return; + + wav64_t *wav = (wav64_t*)sbuf->wave; + wav64_state_vadpcm_t *vstate = sbuf->state; + wav64_header_vadpcm_t *vhead = (wav64_header_vadpcm_t*)wav->st->ext; + int channels = wav->wave.channels; + int sample_pos = wpos * 16; + if (wpos == 0) { + memset(vstate->state, 0, sizeof(vstate->state[0]) * channels); + } else { + int idx = wav64_vadpcm_find_skippoint(vhead, sample_pos, false); + assertf(idx >= 0, "wav64: %s: invalid VADPCM seeking point: 0x%x", wav->wave.name, sample_pos); + memcpy(vstate->state, vhead->skip_states + idx * channels, + sizeof(vstate->state[0]) * channels); + } +} + +static void waveform_vadpcm_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) { + wav64_t *wav = (wav64_t*)sbuf->wave; + if (wav->wave.mem) + waveform_vadpcm_seek_state(ctx, sbuf, wpos, wlen, seeking); + else + waveform_vadpcm_read_compressed(ctx, sbuf, wpos, wlen, seeking); +} + +/** + * @brief Load the whole VADPCM body into a fully-planar plain-frame buffer. + * + * File layout is block-planar (and possibly Huffman); `dst` receives + * [all L frames][all R frames] for direct MIX_CHANNEL addressing. + */ +void wav64_vadpcm_preload(wav64_t *wav, void *dst) +{ + wav64_header_vadpcm_t *vhead = (wav64_header_vadpcm_t*)wav->st->ext; + int channels = wav->wave.channels; + // The file is read whole, padding included, but only the frames that carry + // samples are laid out for the mixer. + int file_frames = WAV64_VADPCM_FILE_FRAMES(wav->wave.len); + int nframes = WAV64_VADPCM_FRAMES(wav->wave.len); + int fbytes = VADPCM_FRAME_BYTES(wav->st->vadpcm.bits); + int file_bytes = file_frames * fbytes * channels; + uint8_t *scratch = malloc(ROUND_UP(file_bytes, 16)); + assertf(scratch, "Out of memory"); + + lseek(wav->st->current_fd, wav->st->base_offset, SEEK_SET); + if (vhead->flags & VADPCM_FLAG_HUFFMAN) { + wav64_state_vadpcm_t st = {0}; + int scratch2_size = ROUND_UP(file_bytes * 2, 16); + void *scratch2 = memalign(16, scratch2_size); + assertf(scratch2, "Out of memory"); + huffv_decompress(wav, &st, scratch, file_bytes, scratch2, scratch2_size); + free(scratch2); + } else { + int n = read(wav->st->current_fd, scratch, file_bytes); + assertf(n == file_bytes, "wav64: short VADPCM preload read"); } - if (highpri) - rspq_highpri_end(); + uint8_t *out = dst; + for (int c = 0; c < channels; c++) + for (int f = 0; f < nframes; f++) + memcpy(out + (c * nframes + f) * fbytes, + scratch + vadpcm_file_index(f, c, file_frames, channels) * fbytes, fbytes); + free(scratch); +} + +static void wav64_vadpcm_init_huffman(wav64_t *wav) { + wav64_header_vadpcm_t *vhead = (wav64_header_vadpcm_t*)wav->st->ext; + wav64_vadpcm_huffctx_t *ctx = vhead->huff_ctx; + + vhead->huff_tbl = malloc(sizeof(wav64_vadpcm_hufftable_t) * 3); + assertf(vhead->huff_tbl, "Out of memory"); + memset(vhead->huff_tbl, 0, sizeof(wav64_vadpcm_hufftable_t) * 3); + + // Compute huffman tables + for (int i = 0; i < 3; i++) { + for (int j=0; j<16; j++) { + int len = ctx[i].lengths[j/2] >> (4*(~j&1)) & 0xf; + if (len == 0xF) continue; + assert(len <= 8); + assert((ctx[i].values[j] >> len) == 0); + + int shift = 8 - len; + int code = ctx[i].values[j] << shift; + uint8_t value = (j << 4) | len; + for (int k=0; k<(1<huff_tbl[i].codes[code+k] == 0); + vhead->huff_tbl[i].codes[code+k] = value; + } + } - if (wav->wave.loop_len && wpos >= wav->wave.len) { - assert(wav->wave.loop_len == wav->wave.len); - samplebuffer_undo(sbuf, wpos - wav->wave.len); + for (int j=0; j<256; j++) { + assert(vhead->huff_tbl[i].codes[j] != 0); + } } } -void wav64_vadpcm_init(wav64_t *wav) +void wav64_vadpcm_init(wav64_t *wav, int state_size) { - wav64_header_vadpcm_t vhead = {0}; - read(wav->current_fd, &vhead, sizeof(vhead)); - int codebook_size = vhead.npredictors * vhead.order * wav->wave.channels * sizeof(wav64_vadpcm_vector_t); - - void *ext = malloc_uncached(sizeof(vhead) + codebook_size); - memcpy(ext, &vhead, sizeof(vhead)); - // FIXME: remove CachedAddr() when read() supports uncached addresses - read(wav->current_fd, CachedAddr(ext + sizeof(vhead)), codebook_size); - wav->ext = ext; - wav->wave.read = waveform_vadpcm_read; - wav->wave.ctx = wav; + _Static_assert((sizeof(wav64_state_vadpcm_t) % 16) == 0, "wav64: invalid state size for VADPCM"); + assertf(state_size == sizeof(wav64_state_vadpcm_t), + "wav64: invalid state size for VADPCM: %d/%d\n", state_size, sizeof(wav64_state_vadpcm_t)); + + wav64_header_vadpcm_t *vhead = (wav64_header_vadpcm_t*)wav->st->ext; + + // Decode the skip pointer table pointer. The table is stored after the codebook, + // and the exact byte offset is stored in the pointer itself to simplify initialization. + if (vhead->num_skippoints > 0) { + int tbl_off = (int)vhead->skip_points; + int state_off = (int)vhead->skip_states; + vhead->skip_points = (void*)vhead->codebook + tbl_off; + vhead->skip_states = (void*)vhead->codebook + state_off; + data_cache_hit_writeback(vhead->skip_points, sizeof(wav64_vadpcm_skippoint_t) * vhead->num_skippoints); + data_cache_hit_writeback(vhead->skip_states, sizeof(wav64_vadpcm_vector_t) * vhead->num_skippoints * wav->wave.channels); + } + + // Init huffman + assertf(vhead->huff_tbl == NULL, "huff_tbl must be NULL before initialization"); + if (vhead->flags & VADPCM_FLAG_HUFFMAN) { + wav64_vadpcm_init_huffman(wav); + } - // This should never happen as audioconv64 handles this. - assertf(wav->wave.loop_len == 0 || wav->wave.loop_len % 16 == 0, - "wav64: invalid loop length for VADPCM: %d\n", wav->wave.loop_len); + wav->wave.start = NULL; + wav->wave.format = WAVEFORM_FORMAT_VADPCM; + wav->st->vadpcm.codebook = vhead->codebook; + wav->st->vadpcm.bits = VADPCM_RESIDUAL_BITS(vhead->residual_bits); + assertf(wav->st->vadpcm.bits >= 2 && wav->st->vadpcm.bits <= 4, + "wav64: %s: invalid VADPCM residual width %d", wav->wave.name, wav->st->vadpcm.bits); + // Huffman codes nibbles, so it cannot coexist with sub-nibble packing. + assertf(wav->st->vadpcm.bits == 4 || !(vhead->flags & VADPCM_FLAG_HUFFMAN), + "wav64: %s: %d-bit VADPCM cannot be huffman-compressed", wav->wave.name, wav->st->vadpcm.bits); + wav->st->vadpcm.loop_state = NULL; + if (wav->wave.loop_len > 0 && vhead->num_skippoints > 0) { + int loop_end = wav->wave.loop_end ? wav->wave.loop_end : wav->wave.len; + int loop_start = loop_end - wav->wave.loop_len; + int idx = wav64_vadpcm_find_skippoint(vhead, loop_start, false); + if (idx >= 0) + wav->st->vadpcm.loop_state = vhead->skip_states + idx * wav->wave.channels; + } + wav->wave.codec = &wav->st->vadpcm; + wav->wave.read = waveform_vadpcm_read; + // Plain frames stream from ROM with async PI DMA. Huffman frames must be + // decompressed by the CPU, and any other medium is read synchronously. + // A full preload also leaves nothing to overlap: the body lives in RDRAM. + wav->wave.async_read = wav->st->rom_base != 0 && + !(vhead->flags & VADPCM_FLAG_HUFFMAN) && + !(wav->st->flags & WAV64_FLAG_PRELOAD); + + // Attack cache: audioconv64 sizes #attack_frames for samples whose + // note-ons would otherwise serialize cold PI DMAs. 0 means none. + wav->st->attack = NULL; + wav->st->attack_n = 0; + if (wav->wave.async_read && wav->wave.channels == 1 && vhead->attack_frames) { + int fbytes = VADPCM_FRAME_BYTES(wav->st->vadpcm.bits); + int n = MIN(vhead->attack_frames, WAV64_VADPCM_FRAMES(wav->wave.len)); + int nbytes = ROUND_UP(n * fbytes, 16); + void *buf = malloc_uncached(nbytes); + assertf(buf, "wav64: %s: attack cache alloc failed (%d frames)", wav->wave.name, n); + uint32_t pi = wav->st->rom_base + wav->st->base_offset; + data_cache_hit_writeback_invalidate(CachedAddr(buf), nbytes); + dma_read(CachedAddr(buf), pi, nbytes); + wav->st->attack = UncachedAddr(buf); + wav->st->attack_n = n; + } } void wav64_vadpcm_close(wav64_t *wav) { - free_uncached(wav->ext); - wav->ext = NULL; + wav64_header_vadpcm_t *vhead = (wav64_header_vadpcm_t*)wav->st->ext; + if (vhead->huff_tbl) { + free(vhead->huff_tbl); + vhead->huff_tbl = NULL; + } + if (wav->st->attack) { + free_uncached(wav->st->attack); + wav->st->attack = NULL; + wav->st->attack_n = 0; + } } int wav64_vadpcm_get_bitrate(wav64_t *wav) { - return wav->wave.frequency * wav->wave.channels * 72 / 16; + return wav->wave.frequency * wav->wave.channels * + (VADPCM_FRAME_BYTES(wav->st->vadpcm.bits) * 8) / 16; } +int wav64_vadpcm_adjust_seek(wav64_t *wav, int wpos) +{ + wav64_header_vadpcm_t *vhead = (wav64_header_vadpcm_t*)wav->st->ext; + int idx = wav64_vadpcm_find_skippoint(vhead, wpos, true); + + // If no skip points are available, VADPCM seeking is only supported to 0. + if (idx < 0) return 0; + return vhead->skip_points[idx].offset; +} diff --git a/src/audio/wav64_vadpcm_internal.h b/src/audio/wav64_vadpcm_internal.h index de80950df..8da63f2af 100644 --- a/src/audio/wav64_vadpcm_internal.h +++ b/src/audio/wav64_vadpcm_internal.h @@ -5,11 +5,134 @@ #ifndef LIBDRAGON_AUDIO_VADPCM_INTERNAL_H #define LIBDRAGON_AUDIO_VADPCM_INTERNAL_H +#include "../utils.h" + +#define VADPCM_FLAG_HUFFMAN (1 << 0) ///< Huffman-encoded VADPCM +#define VADPCM_FLAG_RESIDENT (1 << 1) ///< Preload whole body into RDRAM at open + +/** + * Stereo VADPCM file layout: blocks of this many frames per channel, alternating + * L then R (mono is a single linear plane). Matches #SAMPLEBUFFER_MARGIN_UNITS + * so one samplebuffer fill is one file block. + */ +#define WAV64_VADPCM_BLOCK_FRAMES 128 + +/** + * @brief Bitstream layout of a VADPCM frame. + * + * A frame always codes 16 samples and starts with one control byte + * (`scaling << 4 | predictor`), followed by the 16 residuals packed at + * #wav64_header_vadpcm_t::residual_bits bits each, most significant bit first: + * + * residual i occupies payload bits [i*bits, i*bits + bits) + * + * where payload bit 0 is the most significant bit of payload byte 0. For the + * classic 4-bit encoding this is exactly the usual nibble packing (residual i + * in nibble i), so 4-bit frames are bit-for-bit unchanged; 3-bit and 2-bit + * frames shrink the payload to 6 and 4 bytes respectively. + * + * The mixer ucode reads the payload as four halfwords, one per group of four + * consecutive residuals: group j lives entirely inside the halfword starting at + * payload byte #VADPCM_GROUP_OFFSET, which is why the packing has to be dense + * and MSB-first. + */ +#define VADPCM_GROUP_OFFSET(bits, j) (((j) * (bits)) >> 1) + +/** @brief A vector of audio samples */ +typedef struct __attribute__((aligned(8))) { + int16_t v[8]; ///< Samples +} wav64_vadpcm_vector_t; + +/** @brief Huffamn decoding context */ +typedef struct { + uint8_t lengths[8]; ///< Length of each symbol (4-bit - 0xF means unused) + uint8_t values[16]; ///< Code used for each symbol +} wav64_vadpcm_huffctx_t; + +/** @brief Huffman decoding tables */ +typedef struct { + uint8_t codes[256]; ///< Symbol+length found in the prefix of each byte +} wav64_vadpcm_hufftable_t; + +/** + * @brief WAV64 VADPCM decoding state (for a single mixer channel) + * + * Notice that the state is accessed in part by RSP (state[2]) and in part by + * the CPU (bitpos) so we need to avoid false-sharing of cachelines. This is + * why we force a 16-byte alignment, so that it spans exactly 2 cachelines. + */ +typedef struct __attribute__((aligned(16))) { + wav64_vadpcm_vector_t state[2]; ///< Current decompression state + int bitpos; ///< Current bit position in the input buffer +} wav64_state_vadpcm_t; + +/** @brief Skip point in the waveform. + * + * This structure encodes the state of the VADPCM decoder at a skip point in the + * waveform. It is used to restore the state of the decoder when skiping to a + * different point in the waveform. + */ +typedef struct { + int offset; ///< Samples offset of the skip point in the waveform + int bitpos; ///< Bit position in the input buffer +} wav64_vadpcm_skippoint_t; + +/** @brief Extended header for a WAV64 file with VADPCM compression. */ +typedef struct __attribute__((packed, aligned(8))) { + int8_t npredictors; ///< Number of predictors + int8_t order; ///< Order of the predictors + uint16_t flags; ///< VADPCM flags + int16_t num_skippoints; ///< Number of allowed skip points + uint8_t residual_bits; ///< Bits per residual (2, 3 or 4; 0 means 4) + uint8_t attack_frames; ///< RDRAM attack cache size in frames (0 = none) + wav64_vadpcm_hufftable_t *huff_tbl; ///< Huffman tables (computed at load time) + wav64_vadpcm_skippoint_t *skip_points; ///< Information on the skip points (located after the codebook) + wav64_vadpcm_vector_t *skip_states; ///< Decompression states at the skip point + wav64_vadpcm_huffctx_t huff_ctx[3]; ///< Huffman contexts + uint32_t padding2; ///< Padding + wav64_vadpcm_vector_t codebook[]; ///< Per-channel codebook (#VADPCM_CODEBOOK_STRIDE bytes each) +} wav64_header_vadpcm_t; + +_Static_assert(sizeof(wav64_header_vadpcm_t) == 96, "invalid wav64_header_vadpcm size"); + +/** + * @brief Residual width of a VADPCM stream. + * + * Files written before native sub-nibble packing leave the field at zero, + * which means the classic 4-bit encoding. + */ +#define VADPCM_RESIDUAL_BITS(bits) ((bits) ? (bits) : 4) + +/** + * @brief Frames the mixer can address in a waveform of `len` samples. + * + * The last frame is partial whenever the length is not a multiple of 16, but + * it still holds samples that have to be played. + */ +#define WAV64_VADPCM_FRAMES(len) DIVIDE_CEIL(len, 16) + +/** + * @brief Frames actually stored in the file for a waveform of `len` samples. + * + * audioconv64 pads the waveform to a multiple of 32 samples before encoding, + * but writes the unpadded length in the header, so the file always holds a + * few frames more than #WAV64_VADPCM_FRAMES. The padding is part of the + * block-planar layout of stereo files, so it must be accounted for when + * addressing frames in the file. + */ +#define WAV64_VADPCM_FILE_FRAMES(len) (DIVIDE_CEIL(len, 32) * 2) + /** @brief Initialize VADPCM decoder for a WAV64 file */ -void wav64_vadpcm_init(wav64_t *wav); +void wav64_vadpcm_init(wav64_t *wav, int state_size); /** @brief Close VADPCM decoder and free resources */ void wav64_vadpcm_close(wav64_t *wav); /** @brief Get the bitrate of the VADPCM stream */ int wav64_vadpcm_get_bitrate(wav64_t *wav); +/** @brief Adjust a requested seek position (samples) to a valid VADPCM skip point (samples). */ +int wav64_vadpcm_adjust_seek(wav64_t *wav, int wpos); + +/** @brief Preload VADPCM body into a fully-planar plain-frame buffer at `dst`. */ +void wav64_vadpcm_preload(wav64_t *wav, void *dst); + #endif diff --git a/src/audio/xm64.c b/src/audio/xm64.c index 9bea48224..e584e743b 100644 --- a/src/audio/xm64.c +++ b/src/audio/xm64.c @@ -6,7 +6,9 @@ */ #include "xm64.h" +#include "wav64.h" #include "mixer.h" +#include "mixer_internal.h" #include "audio.h" #include #include "debug.h" @@ -14,40 +16,68 @@ #include "dragonfs.h" #include "wav64_internal.h" #include "asset_internal.h" +#include "profile.h" +#include "utils.h" #include "libxm/xm.h" #include "libxm/xm_internal.h" #include #include +#include -static void wave_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) { - xm_sample_t *samp = (xm_sample_t*)ctx; - raw_waveform_read_address(sbuf, samp->data8_offset, wpos, wlen, samp->bits >> 4); +static char *xm64_extsampledir = NULL; + +/// @brief Length of the ramp that walks a volume change across a few milliseconds. +/// XM volumes jump on every tick (envelopes, slides, global volume, mute), and +/// a hard step is heard as a click; this is short enough that nothing sounds +/// delayed, and long enough to kill the discontinuity. +#define XM64_VOL_RAMP_SAMPLES 128 + +static void stop(xm_context_t *ctx, xm64player_t *xmp) { + for (int i=0;imodule.num_channels;i++) + mixer_ch_stop(xmp->first_ch+i); + xmp->playing = false; + xmp->stop_requested = false; } static int tick(void *arg) { + int delay; + PROFILE_SCOPE(PS_XM_TICK) { xm64player_t *xmp = (xm64player_t*)arg; xm_context_t *ctx = xmp->ctx; int first_ch = xmp->first_ch; - for (int i=0;imodule.num_channels;i++) { - xm_channel_context_t *ch = &ctx->channels[i]; - ch->sample_position = mixer_ch_get_pos(first_ch+i); + PROFILE_SCOPE(PS_XM_GETPOS) { + for (int i=0;imodule.num_channels;i++) { + xm_channel_context_t *ch = &ctx->channels[i]; + if (mixer_ch_playing(first_ch+i)) + ch->sample_position = mixer_ch_get_pos(first_ch+i); + else + // The mixer auto-stopped the channel: a non-looping sample + // reached its end. Mark it as finished like libxm's own mixer + // would (see xm_next_of_sample), otherwise the playback loop + // below would keep restarting the waveform at the last synced + // (stale) position, which is also an invalid seek point for + // compressed streams. Any new note will reset this via + // xm_trigger_note; seeks reset it explicitly below. + ch->sample_position = -1; + } } // If we're requested to stop playback, do it. - if (xmp->stop_requested || (!xmp->looping && ctx->loop_count > 0)) { - for (int i=0;imodule.num_channels;i++) - mixer_ch_stop(xmp->first_ch+i); - xmp->playing = false; - xmp->stop_requested = false; - // Do not reschedule again - return 0; + if (xmp->stop_requested) { + stop(ctx, xmp); + delay = 0; + goto done; } if (xmp->seek.patidx >= 0) { // Seek was requested. Do it. xm_seek(ctx, xmp->seek.patidx, xmp->seek.row, xmp->seek.tick); xmp->seek.patidx = -1; + for (int i=0;imodule.num_channels;i++) { + xm_channel_context_t *ch = &ctx->channels[i]; + ch->sample_position = 0; + } // Turn off all currently-playing samples, so that we don't risk keep // playing them. for (int i=0;imodule.num_channels;i++) @@ -55,116 +85,135 @@ static int tick(void *arg) { } assert(ctx->remaining_samples_in_tick <= 0); - xm_tick(ctx); + PROFILE_SCOPE(PS_XM_LIBXM) { + xm_tick(ctx); + } + + if (!xmp->looping && ctx->loop_count > 0) { + stop(ctx, xmp); + delay = 0; + goto done; + } float gvol = ctx->global_volume * ctx->amplification; - for (int i=0;imodule.num_channels;i++) { - xm_channel_context_t *ch = &ctx->channels[i]; - if (ch->sample) { - waveform_t *w = ch->sample->wave; - - // Check if this sample is muted. This is an user-level muting - // control exposed via the xm.h API that we respect in case the - // user wants to mute some channels (usually for debugging). - bool muted = ch->muted || ch->instrument->muted; - - // Play the waveform. Notice that the waveform might already - // be playing in this channel, in which case the play - // command only resets its position to 0, and keep the sample - // buffer full, which is what we want. - // The mixer doesn't currently allow for mixer_ch_play() to keep - // the current position, but even if it did, xm_tick() might - // have changed it since last tick, because there is a XM effect - // to force the position in the sample. So it's better to - // set it every time with mixer_ch_set_pos. - mixer_ch_play(first_ch+i, w); - mixer_ch_set_pos(first_ch+i, ch->sample_position); - - // Configure also frequency and volume that might have changed - // since last tick. - mixer_ch_set_freq(first_ch+i, ch->frequency); - mixer_ch_set_vol(first_ch+i, - muted ? 0 : gvol * ch->actual_volume[0], - muted ? 0 : gvol * ch->actual_volume[1]); - } else { - // No sample in this channel: the channel is mute. Just stop it. - mixer_ch_stop(first_ch+i); + PROFILE_SCOPE(PS_XM_SYNC) { + for (int i=0;imodule.num_channels;i++) { + xm_channel_context_t *ch = &ctx->channels[i]; + if (ch->sample && ch->sample_position >= 0) { + wav64_t *w = ch->sample->wave; + + // Check if this sample is muted. This is an user-level muting + // control exposed via the xm.h API that we respect in case the + // user wants to mute some channels (usually for debugging). + bool muted = ch->muted || ch->instrument->muted; + + // Play the waveform, if it was not already playing. We don't handle + // explicit key-on events here since it's a bit complex in XM, so + // we just passively check whether we need to start playing or not. + if (mixer_ch_playing_waveform(first_ch+i) != &w->wave) + wav64_play(w, first_ch+i); + + // Set the position of the sample expected by the playback engine. + mixer_ch_set_pos(first_ch+i, ch->sample_position); + + // Configure also frequency and volume that might have changed + // since last tick. Volume is ramped explicitly: a hard step on + // every tick is the classic XM click, and the mixer no longer + // hides that behind mixer_ch_set_vol. + mixer_ch_set_freq(first_ch+i, ch->frequency); + mixer_ch_set_vol_ramp(first_ch+i, + muted ? 0 : gvol * ch->actual_volume[0], + muted ? 0 : gvol * ch->actual_volume[1], + XM64_VOL_RAMP_SAMPLES); + } else { + mixer_ch_stop(first_ch+i); + } } } // Schedule next tick according to the number of samples in this tick. - int delay = ceilf(ctx->remaining_samples_in_tick); + delay = ceilf(ctx->remaining_samples_in_tick); ctx->remaining_samples_in_tick -= delay; +done: ; + } return delay; } +/** @brief Header of the XM64 file */ +typedef struct __attribute__((packed)) { + char magic[4]; // "XM64" + uint8_t version; // version of the file format + uint32_t metadata_offset; // offset of the metadata + uint32_t metadata_size; // size of the metadata +} xm64_header_t; + void xm64player_open(xm64player_t *player, const char *fn) { memset(player, 0, sizeof(*player)); + player->fd = -1; // No pending seek at the moment, we start from beginning anyway. player->seek.patidx = -1; - player->fh = must_fopen(fn); + // Open the file as a file descriptor + int fd = must_open(fn); + + // Read the header to check if this is a valid XM64 file + xm64_header_t header; + read(fd, &header, sizeof(header)); + if (memcmp(header.magic, "XM64", 4) != 0) { + if (memcmp(header.magic, "Exte", 4) == 0) { + assertf(0, "cannot load XM file: %s\nPlease convert to XM64 with audioconv64", fn); + } + assertf(0, "cannot load XM64 file: %s\nFile corrupted", fn); + } + assertf(header.version == 13, + "cannot load XM64 file: %s\nVersion %d not supported, please convert again with audioconv64", + fn, header.version); + + // Seek to the beginning of the metadata, that are asset-compressed. We need + // to read the metadata in small chunks, so we use asset_fopen() for this. + lseek(fd, header.metadata_offset, SEEK_SET); + FILE *fh = asset_fdopen(fd, NULL); // Load the XM context int sample_rate = audio_get_frequency(); assertf(sample_rate >= 0, "audio_init() and mixer_init() must be called before xm64player_open()"); - int err = xm_context_load(&player->ctx, player->fh, sample_rate); + int err = xm_context_load(&player->ctx, fh, sample_rate); if (err != 0) { if (err == 2) { assertf(0, "error loading XM64 file: %s\nMemory size estimation by audioconv64 was wrong\n", fn); } - - // Check if the file looks like a standard XM, so to provide - // a clear message in that case. - char signature[16] = {0}; - fseek(player->fh, 0, SEEK_SET); - fread(signature, 1, 15, player->fh); - if (strcmp(signature, "Extended Module") == 0) { - assertf(0, "cannot load XM file: %s\nPlease convert to XM64 with audioconv64", fn); - } assertf(0, "error loading XM64 file: %s\nFile corrupted", fn); } - assertf(strncmp(fn, "rom:/", 5) == 0, "xm64player only supports files in ROM (rom:/)"); - uint32_t base_rom_addr = dfs_rom_addr(fn+5); + fclose(fh); - // Count samples - int ninst = xm_get_number_of_instruments(player->ctx); - for (int i=0;inwaves += xm_get_number_of_samples(player->ctx, i+1); + // Reopen as unbuffered file descriptor. This will be used for streaming. + player->fd = must_open(fn); + player->ctx->fd = player->fd; - // Allocate waveforms (one per XM64's "samples" aka waveforms) - player->waves = malloc(sizeof(waveform_t) * player->nwaves); - assert(player->waves); - int nw = 0; - for (int i=0;ictx->external_samples) { + assertf(xm64_extsampledir != NULL, "%s: external samples enabled but no directory set. Call xm64_set_extsampledir() first", fn); + extfn = alloca(strlen(xm64_extsampledir) + 1 + 8 + 1 + 5 + 1); + } + + // Open all embedded wav64 files + for (int i=0; ictx); i++) { xm_instrument_t *inst = &player->ctx->module.instruments[i]; for (int j=0;jnum_samples;j++) { xm_sample_t *samp = &inst->samples[j]; - // Convert offset of samples from relative to absolute - samp->data8_offset += base_rom_addr; - - // Initialize the waveform_t structures with information - // coming from the XM "sample". - samp->wave = &player->waves[nw++]; - memset(samp->wave, 0, sizeof(waveform_t)); - samp->wave->name = strdup(fn); // FIXME: maybe better use a proper name here - samp->wave->bits = samp->bits; - samp->wave->channels = 1; - samp->wave->frequency = sample_rate; // fake, will be changed at every key-on - samp->wave->len = samp->length; - samp->wave->loop_len = samp->loop_type == XM_NO_LOOP ? 0 : samp->loop_length; - // raw_waveform_read does not support looping with odd lengths - // because they break the 2-byte alignment phase which makes it - // impossible to use dma_read. In this case, just shorten the - // loop by 1 sample to define an even-length loop. - if (samp->wave->bits == 8 && samp->wave->loop_len&1) - samp->wave->loop_len -= 1; - samp->wave->read = wave_read; - samp->wave->ctx = samp; + if (!player->ctx->external_samples) { + char filename[128]; + snprintf(filename, sizeof(filename), "%s[%d:%d]", fn, i+1, j); + lseek(player->fd, samp->data8_offset, SEEK_SET); + samp->wave = wav64_loadfd(player->fd, filename, NULL); + } else { + sprintf(extfn, "%s/%08lx.wav64", xm64_extsampledir, samp->data8_offset); + samp->wave = wav64_load(strdup(extfn), NULL); + } } } @@ -184,17 +233,34 @@ void xm64player_play(xm64player_t *player, int first_ch) { assert(first_ch + xm_get_number_of_channels(player->ctx) <= MIXER_MAX_CHANNELS); if (!player->playing) { - // XM64 header contains the optimal size for sample buffers on each - // channel, to minimize memory consumption. To configure it, bump - // the frequency of each channel to an unreasonably high value (we don't - // know how much we need, so shoot high), but then limit the buffer size - // to the optimal value. + // XM64 stores the optimal sample buffer size of each channel, to + // minimize memory consumption. Part of it covers the rounds the mixer + // lets the CPU enqueue before the RSP has mixed them, whose input + // windows must stay untouched in the ring: how many those are depends + // on the audio configuration, so audioconv64 stores that part as a + // rate in bytes per second of playback. + // + // To configure the size, bump the frequency of each channel to an + // unreasonably high value (we don't know how much we need, so shoot + // high), but then limit the buffer size to the optimal value. + int sample_rate = audio_get_frequency(); + int inflight = __mixer_inflight_samples(); for (int i=0; ictx->module.num_channels; i++) { - // If the value is 0, the channel is not used. We don't have a way - // to convey this (0 would be interpreted as "no limit"), so just + uint32_t rate = player->ctx->ctx_stream_buf_rate[i]; + uint32_t min = player->ctx->ctx_stream_buf_min[i]; + // If both are 0, the channel is not used. We don't have a way to + // convey this (0 would be interpreted as "no limit"), so just // avoid calling the limit function altogether. - if (player->ctx->ctx_size_stream_sample_buf[i] != 0) - mixer_ch_set_limits(first_ch+i, 0, 1e9, player->ctx->ctx_size_stream_sample_buf[i]); + if (!rate && !min) + continue; + uint32_t sz = player->ctx->ctx_stream_buf_base[i] + + (uint32_t)((uint64_t)rate * inflight / sample_rate); + uint32_t cap = player->ctx->ctx_stream_buf_cap[i]; + if (cap && sz > cap) sz = cap; + if (sz < min) sz = min; + sz = ROUND_UP(sz, 8); + player->stream_ramsz += sz; + mixer_ch_set_limits(first_ch+i, 0, 1e9, sz); } mixer_add_event(0, tick, player); @@ -244,29 +310,31 @@ void xm64player_set_effect_callback(xm64player_t *player, void (*cb)(void*, uint } void xm64player_close(xm64player_t *player) { - // FIXME: we need to stop playing without racing with the audio thread. - // This is not correct and may crash. - disable_interrupts(); if (player->playing) { mixer_remove_event(tick, player); player->playing = false; } + for (int i=0;ictx->module.num_channels;i++) { mixer_ch_stop(player->first_ch+i); mixer_ch_set_limits(player->first_ch+i, 0, 0, 0); } - enable_interrupts(); - if (player->fh != NULL) { - fclose(player->fh); - player->fh = NULL; + // Close all embedded wav64 files + for (int i=0; ictx->module.num_instruments; i++) { + xm_instrument_t *inst = &player->ctx->module.instruments[i]; + for (int j=0; jnum_samples; j++) { + xm_sample_t *samp = &inst->samples[j]; + if (samp->wave) { + wav64_close(samp->wave); + samp->wave = NULL; + } + } } - if (player->waves) { - for (int i=0;inwaves;i++) - free((void*)player->waves[i].name); - free(player->waves); - player->waves = NULL; + if (player->fd >= 0) { + close(player->fd); + player->fd = -1; } if (player->ctx) { @@ -274,3 +342,9 @@ void xm64player_close(xm64player_t *player) { player->ctx = NULL; } } + +void xm64_set_extsampledir(const char *dir) +{ + free(xm64_extsampledir); + xm64_extsampledir = dir ? strdup(dir) : NULL; +} diff --git a/src/audio/ym64.c b/src/audio/ym64.c index a8822d57c..cce0b7ac1 100644 --- a/src/audio/ym64.c +++ b/src/audio/ym64.c @@ -66,7 +66,7 @@ static void ym_wave_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bo // Now switch to integers. Number of audioframes to process, and number // of (integer) samplers per audioframe. int nframes = lastframe - player->curframe + 1; - int samples_per_frame = (int)f_samples_per_frame; + int samples_per_frame = player->wave.append_units; // Get the pointer to the sample buffer. int16_t *samples = samplebuffer_append(sbuf, nframes*samples_per_frame); @@ -126,6 +126,7 @@ void ym64player_open(ym64player_t *player, const char *fn, ym64player_songinfo_t // Initialize decompressor and re-read the header (this time, it will // be decompressed and we should find a valid YM header). player->decoder = malloc(DECOMPRESS_LZH5_STATE_SIZE + DECOMPRESS_LZH5_DEFAULT_WINDOW_SIZE); + assertf(player->decoder, "Out of memory"); offset = 0; decompress_lzh5_init(player->decoder, player->f, DECOMPRESS_LZH5_DEFAULT_WINDOW_SIZE); _ymread(head, 12); @@ -204,6 +205,8 @@ void ym64player_open(ym64player_t *player, const char *fn, ym64player_songinfo_t .loop_len = len-loop_start, .read = ym_wave_read, .ctx = player, + // Every audioframe is reconstructed as a whole (see #ym_wave_read). + .append_units = (int)(freq / player->playfreq), }; ay8910_reset(&player->ay); diff --git a/src/backtrace.c b/src/backtrace.c index 84049f8f3..7bd9e1678 100644 --- a/src/backtrace.c +++ b/src/backtrace.c @@ -70,6 +70,7 @@ #include "interrupt.h" #include "rompak_internal.h" #include "dlfcn_internal.h" +#include "symtable_internal.h" /** @brief Enable to debug why a backtrace is wrong */ #define BACKTRACE_DEBUG 0 @@ -80,64 +81,6 @@ */ #define FUNCTION_ALIGNMENT 32 -/** - * @brief Symbol table file header - * - * The SYMT file is made of three main tables: - * - * * Address table: this is a sequence of 32-bit integers, each representing an address in the ROM. - * The table is sorted in ascending order to allow for binary search. Moreover, the lowest 2 bits - * of each address can store additional information: If bit 0 is set to 1, the address is the start - * of a function. If bit 1 is set to 1, the address is an inline duplicate. In fact, there might be - * multiple symbols at the same address for inlined functions, so we need one entry in this table - * for each entry; all of them will have the same address, and all but the last one will have bit - * 1 set to 1. - * * Symbol table: this is a sequence of symbol table entries, each representing a symbol. The size - * of this table (in number of entries) is exactly the same as the address table. In fact, each - * address of the address table can be thought of as an external member of this structure; it's - * split externally to allow for efficiency reasons. Each entry stores the function name, - * the source file name and line number, and the binary offset of the symbol within the containing - * function. - * * String table: this table can be thought as a large buffer holding all the strings needed by all - * symbol entries (function names and file names). Each symbol entry stores a string as an offset - * within the symbol table and a length. This allows to reuse the same string (or prefix thereof) - * multiple times. Notice that strings are not null terminated in the string table. - * - * The SYMT file is generated by the n64sym tool during the build process. - */ -typedef struct { - char head[4]; ///< Magic ID "SYMT" - uint32_t version; ///< Version of the symbol table - uint32_t addrtab_off; ///< Offset of the address table in the file - uint32_t addrtab_size; ///< Size of the address table in the file (number of entries) - uint32_t symtab_off; ///< Offset of the symbol table in the file - uint32_t symtab_size; ///< Size of the symbol table in the file (number of entries); always equal to addrtab_size. - uint32_t strtab_off; ///< Offset of the string table in the file - uint32_t strtab_size; ///< Size of the string table in the file (number of entries) -} symtable_header_t; - -/** @brief Symbol table entry **/ -typedef struct { - uint32_t func_sidx; ///< Offset of the function name in the string table - uint32_t file_sidx; ///< Offset of the file name in the string table - uint16_t func_len; ///< Length of the function name - uint16_t file_len; ///< Length of the file name - uint16_t line; ///< Line number (or 0 if this symbol generically refers to a whole function) - uint16_t func_off; ///< Offset of the symbol within its function -} symtable_entry_t; - -/** - * @brief Entry in the address table. - * - * This is an address in RAM, with the lowest 2 bits used to store additional information. - * See the ADDRENTRY_* macros to access the various components. - */ -typedef uint32_t addrtable_entry_t; - -#define ADDRENTRY_ADDR(e) ((e) & ~3) ///< Address (without the flags9) -#define ADDRENTRY_IS_FUNC(e) ((e) & 1) ///< True if the address is the start of a function -#define ADDRENTRY_IS_INLINE(e) ((e) & 2) ///< True if the address is an inline duplicate - #define MIPS_OP_ADDIU_SP(op) (((op) & 0xFFFF0000) == 0x27BD0000) ///< Matches: addiu $sp, $sp, imm #define MIPS_OP_DADDIU_SP(op) (((op) & 0xFFFF0000) == 0x67BD0000) ///< Matches: daddiu $sp, $sp, imm #define MIPS_OP_JR_RA(op) (((op) & 0xFFFFFFFF) == 0x03E00008) ///< Matches: jr $ra @@ -158,15 +101,13 @@ extern uint32_t __text_start[]; extern uint32_t __text_end[]; /** @brief Address of the SYMT symbol table in the rompak. */ -static uint32_t SYMT_ROM = 0xFFFFFFFF; +// static uint32_t SYMT_ROM = 0xFFFFFFFF; // MOVED /** @brief Placeholder used in frames where symbols are not available */ -static const char *UNKNOWN_SYMBOL = "???"; - -/** @brief Base address for addresses in address table */ -static uint32_t addrtable_base = 0; - +// static const char *UNKNOWN_SYMBOL = "???"; // MOVED +/** @brief Module resolver */ +module_lookup_func __bt_lookup_module; /** @brief Check if addr is a valid PC address */ static bool is_valid_address(uint32_t addr) { @@ -175,187 +116,19 @@ static bool is_valid_address(uint32_t addr) return addr >= 0x80000400 && addr < 0x80800000 && (addr & 3) == 0; } -/** @brief Check if addr is inside main executable text section */ -static bool is_main_exe_text_address(uint32_t addr) -{ - // TODO: for now we only handle RAM (cached access). This should be extended to handle - // TLB-mapped addresses for instance. - return addr >= (uint32_t)__text_start && addr < (uint32_t)__text_end; -} - -/** - * @brief Open the SYMT symbol table in the rompak. - * - * If not found, return a null header. - */ -static symtable_header_t symt_open(void *addr) { - if(is_main_exe_text_address((uint32_t)addr)) { - //Open SYMT from rompak - static uint32_t mainexe_symt = 0xFFFFFFFF; - if (mainexe_symt == 0xFFFFFFFF) { - mainexe_symt = rompak_search_ext(".sym"); - if (!mainexe_symt) - debugf("backtrace: no symbol table found in the rompak\n"); - } - addrtable_base = 0; - SYMT_ROM = mainexe_symt; - } else { - dl_module_t *module = NULL; - if(__dl_lookup_module) { - module = __dl_lookup_module(addr); - } - if(module && module->sym_romofs != 0) { - //Read module SYMT - SYMT_ROM = module->sym_romofs; - addrtable_base = (uint32_t)module->prog_base; - } else { - SYMT_ROM = 0; - addrtable_base = 0; - } - } - - if (!SYMT_ROM) { - return (symtable_header_t){0}; - } - - symtable_header_t alignas(8) symt_header; - data_cache_hit_writeback_invalidate(&symt_header, sizeof(symt_header)); - dma_read(&symt_header, SYMT_ROM, sizeof(symtable_header_t)); - - if (symt_header.head[0] != 'S' || symt_header.head[1] != 'Y' || symt_header.head[2] != 'M' || symt_header.head[3] != 'T') { - debugf("backtrace: invalid symbol table found at 0x%08lx\n", SYMT_ROM); - SYMT_ROM = 0; - return (symtable_header_t){0}; - } - if (symt_header.version != 2) { - debugf("backtrace: unsupported symbol table version %ld -- please update your n64sym tool\n", symt_header.version); - SYMT_ROM = 0; - return (symtable_header_t){0}; - } - - return symt_header; -} - -/** - * @brief Return an entry in the address table by index - * - * @param symt SYMT file header - * @param idx Index of the entry to return - * @return addrtable_entry_t Entry of the address table - */ -static addrtable_entry_t symt_addrtab_entry(symtable_header_t *symt, int idx) -{ - assert(idx >= 0 && idx < symt->addrtab_size); - return addrtable_base+io_read(SYMT_ROM + symt->addrtab_off + idx * 4); -} - -/** - * @brief Search the SYMT address table for the given address. - * - * Run a binary search to find the entry in the table. If there is a single exact match, - * the entry is returned. If there are multiple entries with the same address, the first - * entry is returned (this is the case for inlined functions: so some entries following - * the current one will have the same address). If there is no exact match, the entry - * with the biggest address just before the given address is returned. - * - * @param symt SYMT file header - * @param addr Address to search for - * @param idx If not null, will be set to the index of the entry found (or the index just before) - * @return The found entry (or the entry just before) - */ -static addrtable_entry_t symt_addrtab_search(symtable_header_t *symt, uint32_t addr, int *idx) -{ - int min = 0; - int max = symt->addrtab_size - 1; - while (min < max) { - int mid = (min + max) / 2; - addrtable_entry_t entry = symt_addrtab_entry(symt, mid); - if (addr <= ADDRENTRY_ADDR(entry)) - max = mid; - else - min = mid + 1; - } - addrtable_entry_t entry = symt_addrtab_entry(symt, min); - if (min > 0 && ADDRENTRY_ADDR(entry) > addr) - entry = symt_addrtab_entry(symt, --min); - if (idx) *idx = min; - return entry; -} - - -/** - * @brief Fetch a string from the string table - * - * @param symt SYMT file - * @param sidx Index of the first character of the string in the string table - * @param slen Length of the string - * @param buf Destination buffer - * @param size Size of the destination buffer - * @return char* Fetched string within the destination buffer (might not be at offset 0 for alignment reasons) - */ -static char* symt_string(symtable_header_t *symt, int sidx, int slen, char *buf, int size) -{ - // Align 2-byte phase of the RAM buffer with the ROM address. This is required - // for dma_read. - int tweak = (sidx ^ (uint32_t)buf) & 1; - char *func = buf + tweak; size -= tweak; - int n = MIN(slen, size); - - data_cache_hit_writeback_invalidate(buf, size); - dma_read(func, SYMT_ROM + symt->strtab_off + sidx, n); - func[n] = 0; - return func; -} - -/** - * @brief Fetch a symbol table entry from the SYMT file. - * - * @param symt SYMT file - * @param entry Output entry pointer - * @param idx Index of the entry to fetch - */ -static void symt_entry_fetch(symtable_header_t *symt, symtable_entry_t *entry, int idx) -{ - data_cache_hit_writeback_invalidate(entry, sizeof(symtable_entry_t)); - dma_read(entry, SYMT_ROM + symt->symtab_off + idx * sizeof(symtable_entry_t), sizeof(symtable_entry_t)); -} - -// Fetch the function name of an entry -static char* symt_entry_func(symtable_header_t *symt, symtable_entry_t *entry, uint32_t addr, char *buf, int size) -{ - if (addr >= (uint32_t)inthandler && addr < (uint32_t)inthandler_end) { - // Special case exception handlers. This is just to show something slightly - // more readable instead of "notcart+0x0" or similar assembly symbols - snprintf(buf, size, ""); - return buf; - } else { - return symt_string(symt, entry->func_sidx, entry->func_len, buf, size); - } -} - -// Fetch the file name of an entry -static char* symt_entry_file(symtable_header_t *symt, symtable_entry_t *entry, uint32_t addr, char *buf, int size) -{ - return symt_string(symt, entry->file_sidx, entry->file_len, buf, size); -} - char* __symbolize(void *vaddr, char *buf, int size) { symtable_header_t symt = symt_open(vaddr); if (symt.head[0]) { uint32_t addr = (uint32_t)vaddr; - int idx = 0; - addrtable_entry_t a = symt_addrtab_search(&symt, addr, &idx); - while (!ADDRENTRY_IS_FUNC(a)) - a = symt_addrtab_entry(&symt, --idx); - - // Read the symbol name symtable_entry_t alignas(8) entry; - symt_entry_fetch(&symt, &entry, idx); - char *func = symt_entry_func(&symt, &entry, addr, buf, size-12); - char lbuf[12]; - snprintf(lbuf, sizeof(lbuf), "+0x%lx", addr - ADDRENTRY_ADDR(a)); - return strcat(func, lbuf); + + if (symt_find_symbol(&symt, addr, &entry, 1)) { + char *func = symt_get_func_name(&symt, &entry, addr, buf, size-12); + char lbuf[12]; + snprintf(lbuf, sizeof(lbuf), "+0x%lx", entry.func_off); + return strcat(func, lbuf); + } } snprintf(buf, size, "%s", UNKNOWN_SYMBOL); return buf; @@ -593,14 +366,13 @@ static void backtrace_foreach(void (*cb)(void *arg, void *ptr), void *arg, uint3 // address of the function. symtable_header_t symt = symt_open(ra); if (symt.head[0]) { - int idx; - addrtable_entry_t entry = symt_addrtab_search(&symt, (uint32_t)ra, &idx); - while (!ADDRENTRY_IS_FUNC(entry)) - entry = symt_addrtab_entry(&symt, --idx); - func_start = ADDRENTRY_ADDR(entry); - #if BACKTRACE_DEBUG - debugf("Found interrupted function start address: %08lx\n", func_start); - #endif + symtable_entry_t alignas(8) entry; + if (symt_find_symbol(&symt, (uint32_t)ra, &entry, 1)) { + func_start = (uint32_t)ra - entry.func_off; + #if BACKTRACE_DEBUG + debugf("Found interrupted function start address: %08lx\n", func_start); + #endif + } } } break; case BT_LEAF: @@ -662,24 +434,6 @@ int __backtrace_from(void **buffer, int size, uint32_t *pc, uint32_t *sp, uint32 } -static void format_entry(void (*cb)(void *, backtrace_frame_t *), void *cb_arg, - symtable_header_t *symt, int idx, uint32_t addr, uint32_t offset, bool is_func, bool is_inline) -{ - symtable_entry_t alignas(8) entry; - symt_entry_fetch(symt, &entry, idx); - - char alignas(8) file_buf[entry.file_len + 2]; - char alignas(8) func_buf[MAX(entry.func_len + 2, 32)]; - - cb(cb_arg, &(backtrace_frame_t){ - .addr = addr, - .func_offset = offset ? offset : entry.func_off, - .func = symt_entry_func(symt, &entry, addr, func_buf, sizeof(func_buf)), - .source_file = symt_entry_file(symt, &entry, addr, file_buf, sizeof(file_buf)), - .source_line = is_func ? 0 : entry.line, - .is_inline = is_inline, - }); -} bool backtrace_symbols_cb(void **buffer, int size, uint32_t flags, void (*cb)(void *, backtrace_frame_t *), void *cb_arg) @@ -710,21 +464,31 @@ bool backtrace_symbols_cb(void **buffer, int size, uint32_t flags, }); continue; } - int idx; addrtable_entry_t a; - a = symt_addrtab_search(&symt_header, needle, &idx); - - if (ADDRENTRY_ADDR(a) == needle) { - // Found an entry at this address. Go through all inlines for this address. - while (1) { - format_entry(cb, cb_arg, &symt_header, idx, needle, 0, false, ADDRENTRY_IS_INLINE(a)); - if (!ADDRENTRY_IS_INLINE(a)) break; - a = symt_addrtab_entry(&symt_header, ++idx); + + symtable_entry_t alignas(8) entries[8]; + int num_entries = symt_find_symbol(&symt_header, needle, entries, 8); + if (num_entries > 0) { + char alignas(8) file_buf[512]; + char alignas(8) func_buf[512]; + + for (int i=0; ifunc_off, + .func = symt_get_func_name(&symt_header, entry, needle, func_buf, sizeof(func_buf)), + .source_file = symt_get_file_name(&symt_header, entry, needle, file_buf, sizeof(file_buf)), + .source_line = entry->line, + .is_inline = entry->is_inline, + }); } } else { - // Search the containing function - while (!ADDRENTRY_IS_FUNC(a)) - a = symt_addrtab_entry(&symt_header, --idx); - format_entry(cb, cb_arg, &symt_header, idx, needle, needle - ADDRENTRY_ADDR(a), true, false); + // Not found + cb(cb_arg, &(backtrace_frame_t){ + .addr = needle, + .func = UNKNOWN_SYMBOL, .func_offset = 0, + .source_file = UNKNOWN_SYMBOL, .source_line = 0, .is_inline = false + }); } } return true; diff --git a/src/backtrace_internal.h b/src/backtrace_internal.h index 0c59a4157..4e14569e2 100644 --- a/src/backtrace_internal.h +++ b/src/backtrace_internal.h @@ -5,6 +5,11 @@ #ifndef __LIBDRAGON_BACKTRACE_INTERNAL_H #define __LIBDRAGON_BACKTRACE_INTERNAL_H +#include "dlfcn_internal.h" + +/** @brief Module resolver function for backtrace */ +extern module_lookup_func __bt_lookup_module; + /** @brief The "type" of funciton as categorized by the backtrace heuristic (__bt_analyze_func) */ typedef enum { BT_FUNCTION, ///< Regular function with a stack frame @@ -40,7 +45,7 @@ int __backtrace_from(void **buffer, int size, uint32_t *pc, uint32_t *sp, uint32 * @param vaddr Address to symbolize * @param buf Buffer where to store the result * @param size Size of the buffer - * @return char* Pointer to the return string. This is within the provided + * @return Pointer to the return string. This is within the provided * buffer, but not necessarily at the beginning because of DMA * alignment constraints. */ diff --git a/src/bb/bb_rtc.c b/src/bb/bb_rtc.c new file mode 100644 index 000000000..ff27afd07 --- /dev/null +++ b/src/bb/bb_rtc.c @@ -0,0 +1,293 @@ +/** + * @file bb_rtc.c + * @author Giovanni Bajo + * @author Christopher Bonhage + * @brief iQue Player (BB) Real-Time Clock (RTC) driver. + */ + +#include +#include +#include "bb_rtc.h" +#include "debug.h" +#include "dma.h" +#include "n64sys.h" +#include "rtc_internal.h" + +#define PI_BB_GPIO ((volatile uint32_t*)0xA4600060) ///< BB GPIO register + +#define I2C_DATA_OUT (1<<7) ///< Data input/output +#define I2C_CLOCK_OUT (1<<6) ///< Clock input/output +#define I2C_DATA_BIT (1<<3) ///< Data bit +#define I2C_CLOCK_BIT (1<<2) ///< Clock bit + +static uint32_t gpio_cache; + +/** @brief Convert nanoseconds to BBPlayer CPU ticks. */ +#define BB_TICKS_FROM_NS(ns) ((int64_t)(ns) * (144000000 / 2) / 1000000000) + +static inline void wait_ns( unsigned long wait_ns ) +{ + wait_ticks(BB_TICKS_FROM_NS(wait_ns)); +} + +/** @brief Initialize a I2C transaction */ +#define I2C_INIT() ({ \ + gpio_cache = *PI_BB_GPIO & ~(I2C_DATA_OUT | I2C_CLOCK_OUT | I2C_DATA_BIT | I2C_CLOCK_BIT); \ +}) + +/** @brief Write SCL/SDA I2C lines */ +#define I2C_WRITE(clock, data) ({ \ + *PI_BB_GPIO = gpio_cache | (!(data) ? I2C_DATA_OUT : 0) | (!(clock) ? I2C_CLOCK_OUT : 0); \ + wait_ns((clock) ? 600 : 1300); \ +}) + +/** @brief Read SDA I2C line */ +#define I2C_READ(clock) ({ \ + *PI_BB_GPIO = gpio_cache | (!(clock) ? I2C_CLOCK_OUT : 0); \ + (*PI_BB_GPIO & I2C_DATA_BIT) ? 1 : 0; \ +}) + +#include "i2c_internal.h" + +/** @brief RTC slave address on the I2C bus */ +#define RTC_SLAVE_ADDR 0x68 + +/** + * @brief RTC state + * + * This structure contains a dump of the internal state of the RTC chip. + */ +typedef struct bb_rtc_state { + uint8_t secs; ///< Seconds [0-59] + uint8_t mins; ///< Minutes [0-59] + uint8_t hours; ///< Hours [0-23] + uint8_t dow; ///< Day of week [1-7] (Sun-Sat) + uint8_t day; ///< Day of month [1-31] + uint8_t month; ///< Month [1-12] + uint8_t year; ///< Year [0-99] + + bool stop; ///< RTC is stopped + bool oscillator_fail; ///< Oscillator has failed + bool century; ///< Century bit + bool century_enable; ///< Century enable: automatic toggle of century bit + bool output_level; ///< Level of the OUT pin +} bb_rtc_state_t; + +/** + * @brief Read the internal state of the BBPlayer RTC chip + * + * @param state pointer to BBPlayer RTC state struct or NULL + * @param raw pointer to a uint64 to store the raw state or NULL + * + * @retval RTC_ESUCCESS if the operation was successful. + * @retval RTC_EBADCLOCK if an I/O error occurred. + **/ +int bb_rtc_get_state( bb_rtc_state_t *state, uint64_t *raw ) +{ + if( !sys_bbplayer() ) + { + debugf("bb_rtc_get_state: BBPlayer not detected\n"); + return RTC_ENOCLOCK; + } + + uint64_t dword; + uint8_t *bytes = (uint8_t *)&dword; + + if ( !i2c_read_data( RTC_SLAVE_ADDR, 0, sizeof(dword), bytes ) ) + { + debugf("bb_rtc_get_state: failed to read over i2c\n"); + return RTC_EBADCLOCK; + } + + if( state != NULL ) + { + memset( state, 0, sizeof(bb_rtc_state_t) ); + state->secs = bcd_decode( bytes[0] & 0x7F ); + state->mins = bcd_decode( bytes[1] & 0x7F ); + state->hours = bcd_decode( bytes[2] & 0x3F ); + state->dow = bcd_decode( bytes[3] & 0x07 ); + state->day = bcd_decode( bytes[4] & 0x3F ); + state->month = bcd_decode( bytes[5] & 0x1F ); + state->year = bcd_decode( bytes[6] & 0xFF ); + + state->stop = (bytes[0] & 0x80) ? true : false; + state->oscillator_fail = (bytes[1] & 0x80) ? true : false; + state->century = (bytes[2] & 0x40) ? true : false; + state->century_enable = (bytes[2] & 0x80) ? true : false; + state->output_level = (bytes[7] & 0x80) ? true : false; + } + + if( raw != NULL ) + { + *raw = dword; + } + + return RTC_ESUCCESS; +} + +/** + * @brief Write the internal state of the BBPlayer RTC chip + * + * @param state pointer to BBPlayer RTC state struct + * + * @retval RTC_ESUCCESS if the operation was successful. + * @retval RTC_EBADCLOCK if an I/O error occurred. + **/ +int bb_rtc_set_state( bb_rtc_state_t *state ) +{ + if( !sys_bbplayer() ) + { + debugf("bb_rtc_set_state: BBPlayer not detected\n"); + return RTC_ENOCLOCK; + } + + uint8_t bytes[8]; + + bytes[0] = bcd_encode(state->secs); + bytes[1] = bcd_encode(state->mins); + bytes[2] = bcd_encode(state->hours); + bytes[3] = bcd_encode(state->dow); + bytes[4] = bcd_encode(state->day); + bytes[5] = bcd_encode(state->month); + bytes[6] = bcd_encode(state->year); + bytes[7] = 0; + + bytes[0] |= state->stop ? 0x80 : 0x00; + bytes[1] |= state->oscillator_fail ? 0x80 : 0x00; + bytes[2] |= state->century ? 0x40 : 0x00; + bytes[2] |= state->century_enable ? 0x80 : 0x00; + bytes[7] |= state->output_level ? 0x80 : 0x00; + + if( i2c_write_data( RTC_SLAVE_ADDR, 0, sizeof(bytes), bytes ) ) + { + debugf("bb_rtc_set_state: success"); + return RTC_ESUCCESS; + } + else + { + debugf("bb_rtc_set_state: failed to write over i2c\n"); + return RTC_EBADCLOCK; + } +} + +/** + * @brief Enable or disable the BBPlayer RTC century bit + * + * @param enabled true to enable the century bit, false to disable + * + * @return RTC_ESUCCESS if the operation was successful, + * or RTC_EBADCLOCK if an I/O error occurred. + */ +int bb_rtc_set_century_enable( bool enabled ) +{ + bb_rtc_state_t state; + int error = bb_rtc_get_state( &state, NULL ); + if ( error != RTC_ESUCCESS ) + { + debugf("bb_rtc_set_century_enable: failed to read state\n"); + return error; + } + state.century_enable = enabled; + return bb_rtc_set_state(&state); +} + +int bb_rtc_get_time( time_t *out ) +{ + bb_rtc_state_t state; + int error = bb_rtc_get_state( &state, NULL ); + if( error != RTC_ESUCCESS ) + { + debugf("bb_rtc_get_time: failed to read state\n"); + return error; + } + + if( state.oscillator_fail ) + { + debugf("bb_rtc_get_time: oscillator failure reported\n"); + // Oscillator failures are not necessarily fatal. + // It could mean that the battery is dead, or has been replaced. + // TODO: Message this up to RTC subsystem so it can be reported/handled. + } + + // Extremely basic sanity-check on the date and time + if( + state.month == 0 || state.month > 12 || + state.day == 0 || state.day > 31 || + state.hours >= 24 || state.mins >= 60 || state.secs >= 60 + ) + { + debugf("bb_rtc_get_time: invalid date/time\n"); + return RTC_EBADTIME; + } + + // NOTE: Official iQue menu disables the century bit! + bool century = state.century_enable && state.century; + + struct tm rtc_tm; + rtc_tm.tm_sec = state.secs; + rtc_tm.tm_min = state.mins; + rtc_tm.tm_hour = state.hours; + rtc_tm.tm_wday = state.dow; + rtc_tm.tm_mday = state.day; + rtc_tm.tm_mon = state.month - 1; + // BBPlayer was released in 2003; does not support 19XX year + rtc_tm.tm_year = state.year + (century ? 200 : 100); + + debugf("bb_rtc_get_time: parsed time: %04d-%02d-%02d %02d:%02d:%02d\n", + rtc_tm.tm_year + 1900, rtc_tm.tm_mon + 1, rtc_tm.tm_mday, + rtc_tm.tm_hour, rtc_tm.tm_min, rtc_tm.tm_sec); + + *out = mktime( &rtc_tm ); + return RTC_ESUCCESS; +} + +int bb_rtc_set_time( time_t new_time ) +{ + bb_rtc_state_t state; + int error = bb_rtc_get_state( &state, NULL ); + if( error != RTC_ESUCCESS ) + { + debugf("bb_rtc_set_time: failed to read state\n"); + return error; + } + + if( state.oscillator_fail ) + { + debugf("bb_rtc_set_time: resetting oscillator fail bit\n"); + // Oscillator failures are not necessarily fatal. + // It could mean that the battery is dead, or has been replaced. + // If the bit is set again on the next read, it's a real problem. + state.oscillator_fail = false; + } + + // NOTE: Official iQue menu disables the century bit! + time_t max_time = state.century_enable ? BB_RTC_CENTURY_TIMESTAMP_MAX : BB_RTC_TIMESTAMP_MAX; + if( new_time < BB_RTC_TIMESTAMP_MIN || new_time > max_time ) + { + debugf("bb_rtc_set_time: time out of range\n"); + return RTC_EBADTIME; + } + + struct tm * new_tm = gmtime( &new_time ); + + debugf("bb_rtc_set_time: parsed time: %04d-%02d-%02d %02d:%02d:%02d\n", + new_tm->tm_year + 1900, new_tm->tm_mon + 1, new_tm->tm_mday, + new_tm->tm_hour, new_tm->tm_min, new_tm->tm_sec); + + state.stop = false; + state.secs = new_tm->tm_sec; + state.mins = new_tm->tm_min; + state.hours = new_tm->tm_hour; + state.dow = new_tm->tm_wday; + state.day = new_tm->tm_mday; + state.month = new_tm->tm_mon + 1; + // BBPlayer was released in 2003; does not support 19XX year + state.year = new_tm->tm_year % 100; + // NOTE: Official iQue menu disables the century bit! + if( state.century_enable ) + { + state.century = new_tm->tm_year >= 200; + } + + return bb_rtc_set_state( &state ); +} diff --git a/src/bb/bbfs.c b/src/bb/bbfs.c new file mode 100644 index 000000000..7d70a8743 --- /dev/null +++ b/src/bb/bbfs.c @@ -0,0 +1,1203 @@ +/** + * @file bbfs.c + * @author Giovanni Bajo + */ +#include +#include +#include +#include +#include +#include +#include "debug.h" +#include "system.h" +#include "nand.h" +#include "bbfs.h" +#include "utils.h" +#include "../rand_internal.h" + +/** @brief Enable or disable BBFS log tracing. */ +#ifndef BBFS_TRACE +#define BBFS_TRACE 0 +#endif + +/** @brief Print a debug trace message if the level is less than or equal to BBFS_TRACE. */ +#define tracef(lvl, str, ...) ({ if ((lvl) <= BBFS_TRACE) debugf("[bbfs] " str "\n", ##__VA_ARGS__); }) + +/// @cond +#ifndef N64 +#define be16(x) __builtin_bswap16(x) +#define be16i(x) (int16_t)__builtin_bswap16(x) +#define be32(x) __builtin_bswap32(x) +#else +#define be16(x) (x) +#define be16i(x) (x) +#define be32(x) (x) +#endif +/// @endcond + +/** @brief Calculate a random number in range [0..n), assuming RAND_MAX = (1<<31)-1 */ +#define RANDN(n) (((uint64_t)__rand32() * (n)) >> 31) + +/** @brief Create a 32-bit FOURCC code from four characters. */ +#define FOURCC(d, c, b, a) ((a) | ((b) << 8) | ((c) << 16) | ((d) << 24)) +#define FAT_UNUSED 0 ///< FAT entry value indicating an unused block. +#define FAT_TERMINATOR -1 ///< FAT entry value indicating the end of a chain. +#define FAT_BADBLOCK -2 ///< FAT entry value indicating a bad block. +#define FAT_RESERVED -3 ///< FAT entry value indicating a reserved block. + +#define BBFS_MAX_ENTRIES 409 ///< Maximum number of file entries in BBFS +#define BBFS_CHECKSUM 0xCAD7 ///< Magic checksum value for BBFS + +#define BBFS_FLAGS_READING (1<<0) ///< The file is open for reading +#define BBFS_FLAGS_WRITING (1<<1) ///< The file is open for writing +#define BBFS_FLAGS_PAGE_CACHED (1<<2) ///< The current page is cached in the page cache +#define BBFS_FLAGS_BLOCK_SHADOWED (1<<3) ///< The current block is now duplicated into a new block +#define BBFS_FLAGS_LAZY_EXTEND (1<<4) ///< The file has been extended via ftruncate, but the zeros haven't been written yet +#define BBFS_FLAGS_PAGE_CACHE_DIRTY (1<<5) ///< The cached page contains modifications that need to be written back +#define BBFS_FLAGS_CONTIGUOUS (1<<6) ///< The file must be stored as contiguous as possible +#define BBFS_BIGFILE_THRESHOLD (512*1024) ///< Files bigger than this are stored in the beginning of the filesystem + +/** @brief Superblock footer */ +typedef struct __attribute__((packed)) { + uint32_t magic; ///< 'BBFS' or 'BBFL' + uint32_t seqno; ///< Sequence number; bigger is newer + uint16_t link; ///< Link to next superblock (for NANDs larger than 64 MiB) + uint16_t checksum; ///< Checksum (16-bit sum of all 16-bit words) +} bbfs_footer_t; + +/** @brief A file entry in the BBFS filesystem */ +typedef struct { + char name[8]; ///< Filename (0 terminated if shorter than 8 chars) + char ext[3]; ///< Extension (0 terminated if shorter than 3 chars) + uint8_t valid; ///< 1 if entry is valid + int16_t block; ///< First block of the file + uint16_t padding; ///< Size of the padding in the last block (libdragon extension) + uint32_t size; ///< File size in bytes (rounded up to block size) +} bbfs_entry_t; + +/** @brief BBFS superblock */ +typedef struct { + int16_t fat[4096]; ///< Block allocation table + bbfs_entry_t entries[BBFS_MAX_ENTRIES]; ///< File entries + bbfs_footer_t footer; ///< Footer +} bbfs_superblock_t; + +/** @brief An open file in the BBFS filesystem */ +typedef struct { + bbfs_entry_t *entry; ///< File entry + int32_t pos; ///< Current file position + int16_t block; ///< Current block + int16_t* block_prev_link; ///< Pointer to the FAT entry that points to this block + uint32_t page_dirty_mask; ///< Bitmap of dirty pages in current block + int32_t final_size; ///< Final size of the file after a lazy extension (only valid if BBFS_FLAGS_LAZY_EXTEND is set) + int8_t flags; ///< Flags + uint8_t page_cache[NAND_PAGE_SIZE]; ///< Page cache +} bbfs_openfile_t; + +_Static_assert(sizeof(bbfs_superblock_t) == NAND_BLOCK_SIZE, "bbfs_superblock_t size mismatch"); + +/** @brief Running state of the filesystem */ +typedef struct { + uint32_t sb_dirty[2]; ///< Superblock dirty mask + int num_superblocks; ///< Number of superblocks + int total_blocks; ///< Total number of blocks in the filesystem + int small_area_idx; ///< Start index of the area for small files (end of filesystem) + int small_area_free; ///< Number of free blocks in the small area +} bbfs_state_t; + +static bbfs_superblock_t bbfs_superblock[2]; +static bbfs_state_t bbfs_state; + +/** + * @brief Access the FAT entry for a given block index. + * @param bidx Block index. + */ +#define SB_FAT(bidx) bbfs_superblock[(bidx) >> 12].fat[(bidx) & 0xFFF] + +static void bbfs_small_area_update(void); + +static void bbfs_small_area_init(void) +{ + // The small file area is an area at the end of the filesystem where + // we store small files that are more likely to be updated / rewritten / changed. + // Small files are also unlikely to be memory mapped via ATB, which means + // that fragmentation doesn't really matter for them. + // We calculate the small file area dynamically, making as big as necessary + // to always have at least 20% of the area itself free, with a minimum of 1 MiB. + int total_blocks = bbfs_state.total_blocks; + + // Start by counting how many free blocks there are in the last 1 MiB + int area_size = 1024*1024 / NAND_BLOCK_SIZE; + int free_blocks = 0; + for (int i=total_blocks-area_size; i= total_blocks) + block = bbfs_state.small_area_idx; + } + + // No free blocks in the small area; this can only happen if the disk is + // completely full. + return -1; +} + +static void bbfs_state_init(int nblocks) +{ + memset(&bbfs_state, 0, sizeof(bbfs_state)); + bbfs_state.total_blocks = nblocks; + bbfs_state.num_superblocks = nblocks / 4096; + bbfs_small_area_init(); +} + +static uint16_t sb_calc_checksum(bbfs_superblock_t *sb) +{ + uint16_t checksum = 0; + uint16_t *data = (uint16_t *)sb; + for (int j=0; j= 0 && offset < sizeof(bbfs_superblock_t)*2, + "internal error: invalid superblock pointer %p (%p)", data, &bbfs_superblock); + + int sbidx = offset / sizeof(bbfs_superblock_t); + offset = offset % sizeof(bbfs_superblock_t); + + int first_page = offset / NAND_PAGE_SIZE; + int last_page = (offset + len - 1) / NAND_PAGE_SIZE; + for (int i=first_page; i<=last_page; i++) + bbfs_state.sb_dirty[sbidx] |= 1 << i; +} + +/** + * @brief Write a value to a superblock field and record it as dirty. + */ +#define SB_WRITE(a, b) ({ \ + a = b; \ + sb_record_write(&a, sizeof(a)); \ +}) + +static void sb_flush(void) +{ + if (!bbfs_state.sb_dirty[0] && !bbfs_state.sb_dirty[1]) { + tracef(3, "sb_flush: dirty mask is empty, skipping"); + return; + } + + int sb_area = bbfs_state.total_blocks - 16; + int bidx = RANDN(16); + + // Write superblocks in reverse order. We can have 2 superblocks in case + // of a 128 MiB flash, and the first superblock contains a link to the + // second one. Writing the second one first ensures that we know how + // to fill the link in the first one. + for (int sbidx=bbfs_state.num_superblocks-1; sbidx >= 0; sbidx--) { + bbfs_superblock_t *sb = &bbfs_superblock[sbidx]; + + // Update sequence number + SB_WRITE(sb->footer.seqno, be32(be32(sb->footer.seqno) + 1)); + + // Recalculate checksum + sb->footer.checksum = 0; + SB_WRITE(sb->footer.checksum, be16(BBFS_CHECKSUM - sb_calc_checksum(sb))); + + // Select a random superblock entry, and erase it + nand_erase_block(NAND_ADDR_MAKE(sb_area + bidx, 0, 0)); + + // TODO: we could use the dirty mask here to switch between writing new data + // and copying data from the previous superblock index. + tracef(2, "sb_flush: writing superblock %d to block %x", sbidx, sb_area+bidx); + nand_write_pages(NAND_ADDR_MAKE(sb_area + bidx, 0, 0), sizeof(bbfs_superblock_t) / NAND_PAGE_SIZE, sb, true); + bbfs_state.sb_dirty[sbidx] = 0; + + // Fill the link in the previous superblock + if (sbidx > 0) + SB_WRITE(sb[-1].footer.link, be16(sb_area+bidx)); + + bidx = (bidx+1) % 16; + } +} + +static int bbfs_mount(void) +{ + bbfs_footer_t footers[16]; + int nf = 0; + int total_blocks = nand_get_size() / NAND_BLOCK_SIZE; + bool must_be_linked = total_blocks > 4096; + int sb_area = total_blocks - 16; + + for (int i=0; i<16; i++) { + bbfs_footer_t *f = &footers[nf]; + nand_read_data(NAND_ADDR_MAKE(sb_area + i, 0, 0x3FF4), f, 12); + #ifndef N64 + f->magic = __builtin_bswap32(f->magic); + f->seqno = __builtin_bswap32(f->seqno); + f->link = __builtin_bswap16(f->link); + f->checksum = __builtin_bswap16(f->checksum); + #endif + + if (f->magic == FOURCC('B', 'B', 'F', 'S')) { + f->magic = i; + nf++; + } + } + + if (nf == 0) { + return BBFS_ERR_SUPERBLOCK; + } + + // Sort footers by seqno, so that we can use the most recent one + for (int i=1; i= 0 && footers[j].seqno < key.seqno) { + footers[j+1] = footers[j]; + j--; + } + footers[j+1] = key; + } + + // We now want to find the most recent one, making sure the checksum + // is correct. + for (int i=0; i 8 || (dot && strlen(dot+1) > 3)) { + if (invalid_name) *invalid_name = true; + return NULL; + } + int namelen = dot ? dot-name : strlen(name); + + for (int i=0; ivalid && !strncmp(entry->name, name, namelen) && !strncmp(entry->ext, dot ? dot+1 : "", 3)) { + return entry; + } + } + + return NULL; +} + +static void *__bbfs_open(char *name, int flags) +{ + bool invalid_name = false; + bbfs_entry_t *entry = bbfs_find_entry(name, &invalid_name); + if (invalid_name) { + errno = EINVAL; + return NULL; + } + + if (!entry) { + if (!(flags & O_CREAT)) { + errno = ENOENT; + return NULL; + } + + // Search for an empty entry in the superblock where the file + // can be written. + for (int i=0; ivalid) { + memset(entry, 0, sizeof(bbfs_entry_t)); + char *dot = strchr(name, '.'); + strncpy(entry->name, name, dot ? dot-name : 8); + if (dot) strncpy(entry->ext, dot+1, 3); + entry->valid = 1; + entry->block = FAT_TERMINATOR; + sb_record_write(entry, sizeof(bbfs_entry_t)); + break; + } + } + if (!entry->valid) { + errno = ENOSPC; + return NULL; + } + } else { + if ((flags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL)) { + errno = EEXIST; + return NULL; + } + } + + int mode = flags & 7; + bbfs_openfile_t *file = malloc(sizeof(bbfs_openfile_t)); + if (!file) { + errno = ENOMEM; + return NULL; + } + file->entry = entry; + file->pos = 0; + file->block_prev_link = &entry->block; + file->block = be16i(entry->block); + file->flags = + ((mode == O_RDONLY || mode == O_RDWR) ? BBFS_FLAGS_READING : 0) | + ((mode == O_WRONLY || mode == O_RDWR) ? BBFS_FLAGS_WRITING : 0); + + if (flags & O_TRUNC) + SB_WRITE(entry->size, 0); + if (flags & O_APPEND) + file->pos = be32(entry->size) - be16(entry->padding); + return file; +} + +static int __bbfs_page_begin(bbfs_openfile_t *f) +{ + if (!(f->flags & BBFS_FLAGS_PAGE_CACHED)) { + int page_start = f->pos - (f->pos % NAND_PAGE_SIZE); + int block = f->flags & BBFS_FLAGS_BLOCK_SHADOWED ? be16i(*f->block_prev_link) : f->block; + if (nand_read_pages(NAND_ADDR_MAKE(block, 0, page_start % NAND_BLOCK_SIZE), 1, f->page_cache, NULL, true) < 0) { + errno = EIO; + return -1; + } + f->flags |= BBFS_FLAGS_PAGE_CACHED; + f->flags &= ~BBFS_FLAGS_PAGE_CACHE_DIRTY; + } + return 0; +} + +static void __bbfs_page_end(bbfs_openfile_t *f) +{ + if (f->flags & BBFS_FLAGS_PAGE_CACHE_DIRTY) { + assert(f->flags & BBFS_FLAGS_PAGE_CACHED); + int page_start = f->pos - (f->pos % NAND_PAGE_SIZE); + nand_write_pages(NAND_ADDR_MAKE(f->block, 0, page_start % NAND_BLOCK_SIZE), 1, f->page_cache, true); + } + f->flags &= ~(BBFS_FLAGS_PAGE_CACHED|BBFS_FLAGS_PAGE_CACHE_DIRTY); +} + +static int __bbfs_block_read(bbfs_openfile_t *f, uint8_t *ptr, int len) +{ + int read = 0; + while (len > 0) { + int offset = f->pos % NAND_PAGE_SIZE; + int n; + + if (offset == 0 && len >= NAND_PAGE_SIZE) { + n = len - (len % NAND_PAGE_SIZE); + if (nand_read_pages(NAND_ADDR_MAKE(f->block, 0, f->pos % NAND_BLOCK_SIZE), n / NAND_PAGE_SIZE, ptr, NULL, true) < 0) + return -1; + } else { + n = NAND_PAGE_SIZE - offset; + if (n > len) + n = len; + + if (__bbfs_page_begin(f) < 0) + return -1; + memcpy(ptr, f->page_cache + offset, n); + } + + if ((f->pos+n) % NAND_PAGE_SIZE == 0) + __bbfs_page_end(f); + + ptr += n; + len -= n; + f->pos += n; + read += n; + } + return read; +} + +static int __bbfs_read(void *file, uint8_t *ptr, int len) +{ + bbfs_openfile_t *f = file; + int size = be32(f->entry->size) - be16(f->entry->padding); + + if (!(f->flags & BBFS_FLAGS_READING)) { + errno = EBADF; + return -1; + } + if (f->pos >= size) + return 0; + + int toread = size - f->pos; + if (toread > len) + toread = len; + + int read = 0; + while (toread > 0) { + int offset = f->pos % NAND_BLOCK_SIZE; + int n = NAND_BLOCK_SIZE - offset; + if (n > toread) + n = toread; + + if (__bbfs_block_read(f, ptr, n) < 0) + return -1; + ptr += n; + toread -= n; + read += n; + + if (f->pos % NAND_BLOCK_SIZE == 0) { + f->block_prev_link = &SB_FAT(f->block); + f->block = be16i(*f->block_prev_link); + } + } + + return read; +} + +static int __bbfs_allocate_block(int prev_block, bool big_file) +{ + // In general we prefer linear allocation as that is more likely to reduce + // fragmentation. Check if the next block is free + int block = prev_block+1; + if (prev_block != FAT_TERMINATOR && block < bbfs_state.total_blocks && SB_FAT(block) == FAT_UNUSED) + return block; + + // If the file is small, search for a free block in the small area. + if (!big_file) + return bbfs_small_area_alloc(); + + // Search linearly for a free block from the beginning of the filesystem. + // We store there only large files, so the hope is that there is not much + // fragmentation + for (int i=0; iflags & BBFS_FLAGS_BLOCK_SHADOWED) == 0) { + int final_size = f->flags & BBFS_FLAGS_LAZY_EXTEND ? f->final_size : be32(f->entry->size) - be16(f->entry->padding); + bool big_file = (f->flags & BBFS_FLAGS_CONTIGUOUS) || (final_size >= BBFS_BIGFILE_THRESHOLD); + f->block = __bbfs_allocate_block(be16i(*f->block_prev_link), big_file); + if (f->block < 0) { + // No free blocks: filesystem is full + errno = ENOSPC; + return -1; + } + nand_erase_block(NAND_ADDR_MAKE(f->block, 0, 0)); + f->flags |= BBFS_FLAGS_BLOCK_SHADOWED; + } + return 0; +} + +static void __bbfs_write_block_end(bbfs_openfile_t *f) +{ + if (f->flags & BBFS_FLAGS_BLOCK_SHADOWED) { + // We've finished writing to the current block. Update the FAT, + // basically changing it from prev -> old -> next to prev -> new -> next, + // that is replacing the old block with the new one. + int16_t old = be16i(*f->block_prev_link); + if (old != FAT_TERMINATOR) { + int next = be16i(SB_FAT(old)); + SB_WRITE(SB_FAT(f->block), be16i(next)); + SB_WRITE(SB_FAT(old), be16i(FAT_UNUSED)); + SB_WRITE(*f->block_prev_link, be16i(f->block)); + } else { + // This is a totally new block. We don't do anything besides registering it + SB_WRITE(*f->block_prev_link, be16i(f->block)); + SB_WRITE(SB_FAT(f->block), be16i(FAT_TERMINATOR)); + } + f->block_prev_link = &SB_FAT(f->block); + f->block = be16i(*f->block_prev_link); + f->flags &= ~BBFS_FLAGS_BLOCK_SHADOWED; + } +} + +static int __bbfs_block_write(bbfs_openfile_t *f, uint8_t *ptr, int len) +{ + int block = f->block; + int size = be32(f->entry->size) - be16(f->entry->padding); + + // Split the write into page-aligned chunks + int written = 0; + while (len > 0) { + int offset = f->pos % NAND_PAGE_SIZE; + int n; + + if (offset == 0 && len >= NAND_PAGE_SIZE) { + // Fast path: write full pages + assert(!(f->flags & BBFS_FLAGS_PAGE_CACHED)); + n = len - (len % NAND_PAGE_SIZE); + nand_write_pages(NAND_ADDR_MAKE(block, 0, f->pos % NAND_BLOCK_SIZE), n / NAND_PAGE_SIZE, ptr, true); + } else { + n = NAND_PAGE_SIZE - offset; + if (n > len) + n = len; + // Slow path: read the page and modify it + if (__bbfs_page_begin(f) < 0) + return -1; + memcpy(f->page_cache + offset, ptr, n); + // Record it that we will need to write it back later + f->flags |= BBFS_FLAGS_PAGE_CACHE_DIRTY; + } + + // If this write reached the end of the page, finish writing it (flush the cache if any) + if ((f->pos+n) % NAND_PAGE_SIZE == 0) + __bbfs_page_end(f); + + ptr += n; + f->pos += n; + len -= n; + written += n; + } + + // If this write increased the file size, record the new size in the superblock + if (f->pos > size) { + SB_WRITE(f->entry->size, be32(ROUND_UP(f->pos, NAND_BLOCK_SIZE))); + SB_WRITE(f->entry->padding, be16(-f->pos & (NAND_BLOCK_SIZE-1))); + } + + return written; +} + +static int __bbfs_write(void *file, uint8_t *ptr, int len) +{ + bbfs_openfile_t *f = file; + + if (!(f->flags & BBFS_FLAGS_WRITING)) { + errno = EBADF; + return -1; + } + + // Process each block with __bbfs_block_write + int written = 0; + while (len > 0) { + // Shadow the current block if it wasn't already + if (__bbfs_write_block_begin(f) < 0) + return -1; + + int offset = f->pos % NAND_BLOCK_SIZE; + int n = NAND_BLOCK_SIZE - offset; + if (n > len) + n = len; + + int w = __bbfs_block_write(f, ptr, n); + if (w < 0) + return w; + + ptr += w; + len -= w; + written += w; + + // If we reached the end of the block, finish writing it + if (f->pos % NAND_BLOCK_SIZE == 0) { + __bbfs_write_block_end(f); + } + } + + return written; +} + +static int __bbfs_extend(bbfs_openfile_t *f, int until) +{ + // Call this function only when at end of file + assert(f->pos == be32(f->entry->size)); + + // Extend the file + #ifdef N64 + const uint8_t *ZERO = (uint8_t*)0x80802000; + const int ZERO_SIZE = 504*1024; + #else + const int ZERO_SIZE = 504*1024; + static const uint8_t ZERO[ZERO_SIZE] = {0}; + #endif + while (f->pos < until) { + int n = until - f->pos; + if (n > ZERO_SIZE) + n = ZERO_SIZE; + int w = __bbfs_write(f, (uint8_t*)ZERO, n); + if (w < 0) + return w; + } + assert(f->pos == be32(f->entry->size)); + assert(f->pos == until); + return 0; +} + +static void __bbfs_shrink(bbfs_entry_t *entry, int len) +{ + // Search for the block that contains len-1. That's the + // last block that we want to keep. + tracef(2, "shrink: %d -> %d", (int)be32(entry->size) - be16(entry->padding), len); + int16_t* block_ptr = &entry->block; + for (int blen = 0; blen < len; blen += NAND_BLOCK_SIZE) { + assert(be16i(*block_ptr) > 0); + block_ptr = &SB_FAT(be16i(*block_ptr)); + } + if (be16i(*block_ptr) != FAT_TERMINATOR) { + // The current block terminates the chain. Then free all + // other blocks. See + int16_t *next = &SB_FAT(be16i(*block_ptr)); + SB_WRITE(*block_ptr, be16i(FAT_TERMINATOR)); + while (be16i(*next) != FAT_TERMINATOR) { + block_ptr = next; + next = &SB_FAT(be16i(*block_ptr)); + SB_WRITE(*block_ptr, be16i(FAT_UNUSED)); + tracef(3, "shrink: free block %d", (int)(block_ptr - bbfs_superblock[0].fat)); + } + SB_WRITE(*next, be16i(FAT_UNUSED)); + } + + // Truncate the file + SB_WRITE(entry->size, be32(ROUND_UP(len, NAND_BLOCK_SIZE))); + SB_WRITE(entry->padding, be16(-len & (NAND_BLOCK_SIZE-1))); +} + +static int __bbfs_lseek(void *file, int offset, int whence) +{ + bbfs_openfile_t *f = file; + int size = be32(f->entry->size) - be16(f->entry->padding); + int pos = f->pos; + + switch (whence) { + case SEEK_SET: + pos = offset; + break; + case SEEK_CUR: + pos += offset; + break; + case SEEK_END: + pos = size + offset; + break; + } + + if (pos < 0) + pos = 0; + + int clamped_pos = pos; + if (pos > size) + clamped_pos = size; + + bool page_changed = clamped_pos / NAND_PAGE_SIZE != f->pos / NAND_PAGE_SIZE; + bool block_changed = clamped_pos / NAND_BLOCK_SIZE != f->pos / NAND_BLOCK_SIZE; + + if (f->flags & BBFS_FLAGS_WRITING) { + if (page_changed) { + __bbfs_page_end(f); + if (block_changed) + __bbfs_write_block_end(f); + } + } + + // Check if the current block changed + if (block_changed) { + f->block_prev_link = &f->entry->block; + f->block = be16i(*f->block_prev_link); + int newpos = pos; + while (newpos >= NAND_BLOCK_SIZE) { + f->block_prev_link = &SB_FAT(f->block); + f->block = be16i(*f->block_prev_link); + newpos -= NAND_BLOCK_SIZE; + } + } + + if (f->flags & BBFS_FLAGS_LAZY_EXTEND && pos > size) { + // We've been asked to extend the file, and the user is seeking + // beyond the current size. Extend the file now. + if (pos >= f->final_size) + pos = f->final_size; + int err = __bbfs_extend(file, pos); + if (err < 0) + return err; + if (pos == f->final_size) + f->flags &= ~BBFS_FLAGS_LAZY_EXTEND; + } + + f->pos = pos; + return pos; +} + +static int __bbfs_ftruncate(void *file, int len) +{ + bbfs_openfile_t *f = file; + int size = be32(f->entry->size) - be16(f->entry->padding); + + if (!(f->flags & BBFS_FLAGS_WRITING)) { + errno = EBADF; + return -1; + } + + tracef(1, "ftruncate: %d -> %d", size, len); + if (len < size) { + // If we're currently past the new size, move back. Use lseek + // so that everything is flushed / updated correctly. + // TODO: we could optimize this by avoiding writing the current + // shadowed block if any, as it's going to be truncated anyway. + // But at least this version is trivially correct. + if (f->pos > len) + __bbfs_lseek(file, len, SEEK_SET); + // Flush writing the current page/block (if any) + __bbfs_shrink(f->entry, len); + // Even if we were asked before to extend the file, now + // we've been asked to reduce it (against its original size), + // so there's no need to extend it anymore. + f->flags &= ~BBFS_FLAGS_LAZY_EXTEND; + } else if (len > size) { + // Remember that we want to extend the file but don't do + // that yet, as the user might be writing the data anyway. + f->flags |= BBFS_FLAGS_LAZY_EXTEND; + f->final_size = len; + } + + return 0; +} + +static int __bbfs_close(void *file) +{ + bbfs_openfile_t *f = file; + if (f->flags & BBFS_FLAGS_WRITING) { + // Flush writing the current page/block + __bbfs_page_end(f); + __bbfs_write_block_end(f); + // Check if we need to finish extending the file. Calling + // lseek will extend it if necessary. + if (f->flags & BBFS_FLAGS_LAZY_EXTEND) + __bbfs_lseek(file, f->final_size, SEEK_SET); + // Write the superblock if dirty + sb_flush(); + } + + free(file); + return 0; +} + + +static int __bbfs_fstat(void *file, struct stat *st) +{ + bbfs_openfile_t *f = file; + memset(st, 0, sizeof(struct stat)); + st->st_ino = f->entry - bbfs_superblock[0].entries; + st->st_mode = S_IFREG; + st->st_size = be32(f->entry->size) - be16(f->entry->padding); + st->st_blksize = NAND_BLOCK_SIZE; + st->st_blocks = (st->st_size + NAND_BLOCK_SIZE - 1) / NAND_BLOCK_SIZE; + return 0; +} + +static int __bbfs_ioctl(void *file, unsigned long request, void *arg) +{ + bbfs_openfile_t *f = file; + switch (request) { + case IOBBFS_SET_CONTIGUOUS: { + bool contiguous = *(bool*)arg; + if (contiguous) { + f->flags |= BBFS_FLAGS_CONTIGUOUS; + } else { + f->flags &= ~BBFS_FLAGS_CONTIGUOUS; + } + return 0; + } + case IOBBFS_GET_BLOCK: { + int16_t *block = arg; + *block = f->block; + return 0; + } + default: + errno = ENOTTY; + return -1; + } +} + +static int __bbfs_unlink(char *name) +{ + bool invalid_name = false; + bbfs_entry_t *entry = bbfs_find_entry(name, &invalid_name); + if (invalid_name) { + errno = EINVAL; + return -1; + } + + if (!entry) { + errno = ENOENT; + return -1; + } + + // Remove all blocks + __bbfs_shrink(entry, 0); + // Free the entry + SB_WRITE(entry->valid, 0); + // Write the superblock + sb_flush(); + + return 0; +} + +static int __bbfs_findnext(dir_t *dir) +{ + for (int i=dir->d_cookie+1; ivalid) { + dir->d_cookie = i; + dir->d_type = DT_REG; + dir->d_size = be32(entry->size) - be16(entry->padding); + + int j=0; + for (int k=0; k<8; k++) { + if (entry->name[k] == 0) break; + dir->d_name[j++] = entry->name[k]; + } + dir->d_name[j++] = '.'; + for (int k=0; k<3; k++) { + if (entry->ext[k] == 0) break; + dir->d_name[j++] = entry->ext[k]; + } + dir->d_name[j++] = '\0'; + return 0; + } + } + + dir->d_cookie = BBFS_MAX_ENTRIES; + return -1; +} + +static int __bbfs_findfirst(char *name, dir_t *dir) +{ + if (!name || strcmp(name, "/")) { + errno = EINVAL; + return -2; + } + + dir->d_cookie = -1; + return __bbfs_findnext(dir); +} + +static filesystem_t bb_fs = { + .open = __bbfs_open, + .fstat = __bbfs_fstat, + .lseek = __bbfs_lseek, + .read = __bbfs_read, + .write = __bbfs_write, + .ftruncate = __bbfs_ftruncate, + .close = __bbfs_close, + .unlink = __bbfs_unlink, + .ioctl = __bbfs_ioctl, + .findfirst = __bbfs_findfirst, + .findnext = __bbfs_findnext, +}; + +int bbfs_init(void) +{ + nand_init(); + + int err = bbfs_mount(); + if (err < 0) + return err; + + attach_filesystem("bbfs:/", &bb_fs); + return 0; +} + +int16_t* bbfs_get_file_blocks(const char *filename) +{ + bbfs_entry_t* entry = bbfs_find_entry(filename, NULL); + if (!entry) + return NULL; + + int num_blocks = be32(entry->size) / NAND_BLOCK_SIZE; + int16_t *blocks = malloc(sizeof(int16_t) * (num_blocks + 1)); + assertf(blocks, "Out of memory"); + + int block = be16i(entry->block); + for (int i=0; i= bbfs_state.total_blocks) { + tracef(1, "wrong block number %x, filesystem is corrupted", block); + free(blocks); + return NULL; + } + blocks[i] = block; + block = be16i(SB_FAT(block)); + } + + blocks[num_blocks] = -1; + return blocks; +} + +/** + * @brief Internal state using during bbfs_fsck + */ +typedef struct { + int num_errors; ///< number of errors found + uint64_t filename_bloom[8]; ///< bloom filter for filenames in the filesystem + uint8_t used_blocks[4096*2/8]; ///< bitmap of used blocks +} fsck_state_t; + +static uint32_t fsck_hash_filename(const char *str) +{ + uint32_t hash = 0; + for (int j=0; j<11; j++) { + hash += *str++ ^ 0x80; hash += (hash << 10); hash ^= (hash >> 6); + } + hash += (hash << 3); hash ^= (hash >> 11); hash += (hash << 15); + return hash; +} + +static bool fsck_hash_insert(fsck_state_t *state, uint32_t hash) +{ + bool prev = state->filename_bloom[hash >> 29] & (1ull << (hash & 63)); + state->filename_bloom[hash >> 29] |= 1ull << (hash & 63); + return !prev; +} + +static void fsck_filenames(fsck_state_t *state, bool fix_errors) +{ + // Check for duplicated filenames. We use a minimal 512-bit bloom filter to + // avoid quadratic behavior; given that there are a maximum of 409 entries, + // it should hold well. + for (int i=0; ivalid) { + int nlen = strnlen(entry->name, 8); + int elen = strnlen(entry->ext, 3); + + // Check that the filename is well formed. If the filename is shorter + // than 8 characters, all remaining characters must be NULL. + for (int j=nlen; j<8; j++) { + if (entry->name[j] != 0) { + state->num_errors++; + tracef(1, "invalid padding in filename: %.*s.%.*s", nlen, entry->name, elen, entry->ext); + if (fix_errors) for (int k=j; k<8; k++) SB_WRITE(entry->name[k], 0); + break; + } + } + // Same for the extension + for (int j=elen; j<3; j++) { + if (entry->ext[j] != 0) { + state->num_errors++; + tracef(1, "invalid padding in filename: %.*s.%.*s", nlen, entry->name, elen, entry->ext); + if (fix_errors) for (int k=j; k<3; k++) SB_WRITE(entry->ext[k], 0); + break; + } + } + + // Check for duplicated filenames + uint32_t hash = fsck_hash_filename(entry->name); + if (!fsck_hash_insert(state, hash)) { + for (int j=0; jvalid && !strncmp(entry->name, entry2->name, 8) && !strncmp(entry->ext, entry2->ext, 3)) { + state->num_errors++; + tracef(1, "duplicate filename: %.*s.%.*s", nlen, entry->name, elen, entry->ext); + if (fix_errors) SB_WRITE(entry->valid, 0); + break; + } + } + } + + // Check that the size is a multiple of a block + if (be32(entry->size) % NAND_BLOCK_SIZE != 0) { + state->num_errors++; + tracef(1, "file %.*s.%.*s has invalid size %d", nlen, entry->name, elen, entry->ext, (int)be32(entry->size)); + if (fix_errors) SB_WRITE(entry->size, be32(ROUND_UP(be32(entry->size), NAND_BLOCK_SIZE))); + } + + // Check that the padding is within the block size + if (be16(entry->padding) >= NAND_BLOCK_SIZE) { + state->num_errors++; + tracef(1, "file %.*s.%.*s has invalid padding %d", nlen, entry->name, elen, entry->ext, be16(entry->padding)); + if (fix_errors) SB_WRITE(entry->padding, 0); + } + } + } +} + +static void fsck_random_name(fsck_state_t *state, char *name) +{ + // Loop until we find a unique name for this file. We trust the bloom filter + // here; if the bloom filter says the name is unique, we assume it is. + memcpy(name, "FSCK0000XXX", 11); + do { + for (int i=4; i<8; i++) + name[i] = '0' + (rand() % 10); + } while (fsck_hash_insert(state, fsck_hash_filename(name))); +} + +static void fsck_fatchains(fsck_state_t *state, bool fix_errors) +{ + for (int i=0; ivalid) { + int16_t *block_ptr = &entry->block; + int16_t block = be16i(*block_ptr); + int num_blocks = be32(entry->size) / NAND_BLOCK_SIZE; + bool corrupted = false; + + for (int j=0; j= bbfs_state.total_blocks) { + state->num_errors++; + tracef(1, "invalid block number %d in file %.*s.%.*s", block, 8, entry->name, 3, entry->ext); + if (fix_errors) { + SB_WRITE(*block_ptr, be16i(FAT_TERMINATOR)); + corrupted = true; + } + break; + } + state->used_blocks[block / 8] |= 1 << (block % 8); + if (j < num_blocks-1 && SB_FAT(block) == FAT_TERMINATOR) { + state->num_errors++; + tracef(1, "missing block after %d in file %.*s.%.*s", block, 8, entry->name, 3, entry->ext); + if (fix_errors) { + SB_WRITE(*block_ptr, be16i(FAT_TERMINATOR)); + corrupted = true; + } + break; + } + block_ptr = &SB_FAT(block); + block = be16i(*block_ptr); + } + + if (!corrupted && block != FAT_TERMINATOR) { + state->num_errors++; + tracef(1, "extra block %d in file %.*s.%.*s", block, 8, entry->name, 3, entry->ext); + if (fix_errors) { + SB_WRITE(*block_ptr, be16i(FAT_TERMINATOR)); + corrupted = true; + } + } + + if (corrupted) { + fsck_random_name(state, entry->name); + sb_record_write(entry->name, 11); + } + } + } +} + +static void fsck_blocks(fsck_state_t *state, bool fix_errors) +{ + for (int16_t block=0; blockused_blocks[block / 8] & (1 << (block % 8)))) { + state->num_errors++; + tracef(1, "block %d is not marked as free but is not part of any file", block); + if (fix_errors) { + // Go through the chain from here. Mark all blocks as used + int num_blocks = 0; + int16_t cur = block; + do { + state->used_blocks[cur / 8] |= 1 << (cur % 8); + num_blocks++; + cur = be16i(SB_FAT(cur)); + } while (cur != FAT_TERMINATOR); + + // Find a free entry in the superblock and store the block there + for (int j=0; jvalid) { + entry->valid = 1; + entry->block = be16i(block); + entry->size = be32(num_blocks * NAND_BLOCK_SIZE); + entry->padding = 0; + fsck_random_name(state, entry->name); + sb_record_write(entry, sizeof(bbfs_entry_t)); + break; + } + } + } + } + } +} + + +int bbfs_fsck(bool fix_errors) +{ + fsck_state_t state = {0}; + + // Run the various checks + fsck_filenames(&state, fix_errors); + fsck_fatchains(&state, fix_errors); + fsck_blocks (&state, fix_errors); + + // Write the superblock in case we modified it + sb_flush(); + + return state.num_errors; +} diff --git a/src/bb/libdragon.mk b/src/bb/libdragon.mk new file mode 100644 index 000000000..38148125c --- /dev/null +++ b/src/bb/libdragon.mk @@ -0,0 +1,6 @@ + +LIBDRAGON_OBJS += \ + $(BUILD_DIR)/bb/skc.o \ + $(BUILD_DIR)/bb/nand.o \ + $(BUILD_DIR)/bb/bbfs.o \ + $(BUILD_DIR)/bb/bb_rtc.o diff --git a/src/bb/nand.c b/src/bb/nand.c new file mode 100644 index 000000000..671255026 --- /dev/null +++ b/src/bb/nand.c @@ -0,0 +1,486 @@ +/** + * @file nand.c + * @author Giovanni Bajo + */ +#include +#include +#include "n64sys.h" +#include "n64types.h" +#include "debug.h" +#include "dma.h" +#include "nand.h" +#include "utils.h" +#include "interrupt.h" + +#define PI_DRAM_ADDR ((volatile uint32_t*)0xA4600000) ///< PI DRAM address register for NAND operations. +#define PI_CART_ADDR ((volatile uint32_t*)0xA4600004) ///< PI CART address register for NAND operations. +#define PI_BB_ATB_UPPER ((volatile uint32_t*)0xA4600040) ///< PI BB ATB upper register. +#define PI_BB_NAND_CTRL ((volatile uint32_t*)0xA4600048) ///< PI BB NAND control register. +#define PI_BB_NAND_CFG ((volatile uint32_t*)0xA460004C) ///< PI BB NAND configuration register. +#define PI_BB_RD_LEN ((volatile uint32_t*)0xA4600058) ///< PI BB read length register. +#define PI_BB_WR_LEN ((volatile uint32_t*)0xA460005C) ///< PI BB write length register. +#define PI_BB_NAND_ADDR ((volatile uint32_t*)0xA4600070) ///< PI BB NAND address register. + +#define PI_BB_BUFFER_0 ((volatile uint32_t*)0xA4610000) ///< NAND buffer 0 +#define PI_BB_BUFFER_1 ((volatile uint32_t*)0xA4610200) ///< NAND buffer 1 +#define PI_BB_SPARE_0 ((volatile uint32_t*)0xA4610400) ///< NAND spare data 0 +#define PI_BB_SPARE_1 ((volatile uint32_t*)0xA4610410) ///< NAND spare data 1 +#define PI_BB_AES_KEY ((volatile uint32_t*)0xA4610420) ///< AES expanded key +#define PI_BB_AES_IV ((volatile uint32_t*)0xA46104D0) ///< AES initialization vector +#define PI_BB_ATB_LOWER ((volatile uint32_t*)0xA4610500) ///< PI BB ATB lower register. + +#define PI_BB_NAND_CTRL_BUSY (1 << 31) ///< PI BB NAND control busy flag. +#define PI_BB_NAND_CTRL_ECC_ERROR (1 << 10) ///< PI BB NAND control ECC error flag. +#define PI_BB_NAND_CTRL_ECC_CORRECTED (1 << 11) ///< PI BB NAND control ECC corrected flag. + +#define PI_BB_WNAND_CTRL_CMD_SHIFT 16 ///< PI BB write NAND control command shift. +#define PI_BB_WNAND_CTRL_LEN_SHIFT 0 ///< PI BB write NAND control length shift. +#define PI_BB_WNAND_CTRL_MULTICYCLE (1 << 10) ///< PI BB write NAND control multicyle flag. +#define PI_BB_WNAND_CTRL_ECC (1 << 11) ///< PI BB write NAND control ECC flag. +#define PI_BB_WNAND_CTRL_BUF(n) ((n) << 14) ///< PI BB write NAND control buffer select. +#define PI_BB_WNAND_CTRL_INTERRUPT (1 << 30) ///< PI BB write NAND control interrupt flag. +#define PI_BB_WNAND_CTRL_EXECUTE (1 << 31) ///< PI BB write NAND control execute flag. + +#define PI_BB_WATB_UPPER_DMAREAD (1 << 4) ///< This ATB entry will be enabled for DMA. +#define PI_BB_WATB_UPPER_CPUREAD (1 << 5) ///< This ATB entry will be enabled for CPU read. +#define PI_BB_WATB_UPPER_IVSOURCE (1 << 8) ///< PI BB ATB upper IV source flag. + +#define PI_BB_ATB_MAX_ENTRIES 192 ///< Maximum number of ATB entries. + +/** + * @brief NAND hardware commands + * + * These macro contain the command code for the NAND hardware (that can be + * found in the datasheet) plus the configuration for the NAND controller to + * perform the command. + * + * The various bits for the controller are still underspecified. n64brew has + * some guesses, but mostly this was verified with hardware tests. This is why + * the bits don't have a macro name yet. + */ +typedef enum { + NAND_CMD_READ1_H0 = (0x00 << PI_BB_WNAND_CTRL_CMD_SHIFT) | (1<<28) | (1<<27) | (1<<26)| (1<<25) | (1<<24) | (1<<15), + NAND_CMD_READ1_H1 = (0x01 << PI_BB_WNAND_CTRL_CMD_SHIFT) | (1<<28) | (1<<27) | (1<<26)| (1<<25) | (1<<24) | (1<<15), + NAND_CMD_RESET = (0xFF << PI_BB_WNAND_CTRL_CMD_SHIFT), + NAND_CMD_READID = (0x90 << PI_BB_WNAND_CTRL_CMD_SHIFT) | (1<<28) | (1<<24), + NAND_CMD_PAGEPROG_A = (0x80 << PI_BB_WNAND_CTRL_CMD_SHIFT) | PI_BB_WNAND_CTRL_MULTICYCLE | (1<<29) | (1<<27) | (1<<26) | (1<<25) | (1<<24), + NAND_CMD_PAGEPROG_B = (0x10 << PI_BB_WNAND_CTRL_CMD_SHIFT) | (1<<15), + NAND_CMD_COPYBACK_A = (0x00 << PI_BB_WNAND_CTRL_CMD_SHIFT), + NAND_CMD_COPYBACK_B = (0x8A << PI_BB_WNAND_CTRL_CMD_SHIFT), + NAND_CMD_COPYBACK_C = (0x10 << PI_BB_WNAND_CTRL_CMD_SHIFT), + NAND_CMD_ERASE_A = (0x60 << PI_BB_WNAND_CTRL_CMD_SHIFT) | PI_BB_WNAND_CTRL_MULTICYCLE | (1<<27) | (1<<26)| (1<<25), + NAND_CMD_ERASE_B = (0xD0 << PI_BB_WNAND_CTRL_CMD_SHIFT) | (1<<15), + NAND_CMD_READSTATUS = (0x70 << PI_BB_WNAND_CTRL_CMD_SHIFT) +} nand_cmd_t; + +static bool nand_inited; +static uint32_t nand_size; +static int mmap_atb_idx = -1; + +static void dma_bb_read_raw_async(void *ram_buffer, int pi_buffer_offset, int len) +{ + assert(len > 0); + disable_interrupts(); + dma_wait(); + *PI_DRAM_ADDR = PhysicalAddr(ram_buffer); + *PI_CART_ADDR = pi_buffer_offset; + *PI_BB_WR_LEN = len-1; + enable_interrupts(); +} + +static void dma_bb_write_raw_async(void *ram_buffer, int pi_buffer_offset, int len) +{ + assert(len > 0); + disable_interrupts(); + dma_wait(); + *PI_DRAM_ADDR = PhysicalAddr(ram_buffer); + *PI_CART_ADDR = pi_buffer_offset; + *PI_BB_RD_LEN = len-1; + enable_interrupts(); +} + +static void nand_cmd_wait(void) +{ + while (*PI_BB_NAND_CTRL & PI_BB_NAND_CTRL_BUSY) {} +} + +static void nand_cmd_readid(int bufidx) +{ + *PI_BB_NAND_ADDR = 0; + *PI_BB_NAND_CTRL = PI_BB_WNAND_CTRL_EXECUTE | PI_BB_WNAND_CTRL_BUF(bufidx) | + NAND_CMD_READID | (4 << PI_BB_WNAND_CTRL_LEN_SHIFT); + nand_cmd_wait(); +} + +static void nand_cmd_read1_async(int bufidx, uint32_t addr, int len, bool ecc) +{ + nand_cmd_wait(); + assert(len > 0 && len <= 512+16); + *PI_BB_NAND_ADDR = addr; + *PI_BB_NAND_CTRL = PI_BB_WNAND_CTRL_EXECUTE | PI_BB_WNAND_CTRL_BUF(bufidx) | + ((addr & 0x100) ? NAND_CMD_READ1_H1 : NAND_CMD_READ1_H0) | + (ecc ? PI_BB_WNAND_CTRL_ECC : 0) | + (len << PI_BB_WNAND_CTRL_LEN_SHIFT); +} + +static void nand_cmd_read1(int bufidx, uint32_t addr, int len, bool ecc) +{ + nand_cmd_read1_async(bufidx, addr, len, ecc); + nand_cmd_wait(); +} + +static void nand_cmd_pageprog_async(int bufidx, uint32_t addr, bool ecc) +{ + nand_cmd_wait(); + *PI_BB_NAND_ADDR = addr; + *PI_BB_NAND_CTRL = PI_BB_WNAND_CTRL_EXECUTE | PI_BB_WNAND_CTRL_BUF(bufidx) | + (ecc ? PI_BB_WNAND_CTRL_ECC : 0) | + NAND_CMD_PAGEPROG_A | ((NAND_PAGE_SIZE+16) << PI_BB_WNAND_CTRL_LEN_SHIFT); + nand_cmd_wait(); + *PI_BB_NAND_CTRL = PI_BB_WNAND_CTRL_EXECUTE | PI_BB_WNAND_CTRL_BUF(bufidx) | + (ecc ? PI_BB_WNAND_CTRL_ECC : 0) | + NAND_CMD_PAGEPROG_B; +} + +static void nand_cmd_erase(uint32_t addr) +{ + *PI_BB_NAND_ADDR = addr; + *PI_BB_NAND_CTRL = PI_BB_WNAND_CTRL_EXECUTE | NAND_CMD_ERASE_A; + nand_cmd_wait(); + *PI_BB_NAND_CTRL = PI_BB_WNAND_CTRL_EXECUTE | NAND_CMD_ERASE_B; + nand_cmd_wait(); +} + +static void nand_read_id(uint8_t id[4]) +{ + uint8_t aligned_buf[16] __attribute__((aligned(16))); + data_cache_hit_invalidate(aligned_buf, 16); + + const int bufidx = 0; + nand_cmd_readid(bufidx); + + uint32_t id32 = io_read((uint32_t)PI_BB_BUFFER_0 + bufidx*0x200); + id[0] = id32 >> 24; + id[1] = id32 >> 16; + id[2] = id32 >> 8; + id[3] = id32; +} + +static uint8_t io_read8(uint32_t addr) +{ + uint32_t data = io_read(addr & ~3); + return (data >> ((~addr & 3) * 8)) & 0xFF; +} + +void nand_init(void) +{ + assertf(sys_bbplayer(), "NAND is only present on iQue Player"); + if (nand_inited) return; + + uint8_t id[4]; + *PI_BB_NAND_CFG = 0x753E3EFF; + nand_read_id(id); + + uint16_t id16 = (id[0] << 8) | id[1]; + switch (id16) { + default: + debugf("Unknown NAND ID: %04X", id16); + break; + case 0xEC76: // K9F1208U0M + case 0x2076: // NAND512W3A + *PI_BB_NAND_CFG = 0x441F1F3F; + nand_size = 64 * 1024 * 1024; + break; + case 0x9876: // TC58512FT + *PI_BB_NAND_CFG = 0x753E1F3F; + nand_size = 64 * 1024 * 1024; + break; + case 0xEC79: // K9K1G08U0B + *PI_BB_NAND_CFG = 0x441F1F3F; + nand_size = 128 * 1024 * 1024; + break; + } + + nand_inited = true; +} + +int nand_get_size(void) +{ + assertf(nand_inited, "nand_init() must be called first"); + return nand_size; +} + +int nand_read_data(nand_addr_t addr, void *buf, int len) +{ + assertf(nand_inited, "nand_init() must be called first"); + + int bufidx = 0; + uint8_t *buffer = buf; + + while (len > 0) { + int offset = NAND_ADDR_OFFSET(addr); + int read_len = MIN(len, NAND_PAGE_SIZE - offset); + nand_cmd_read1(bufidx, addr, read_len, false); + for (int i=0; i 0, "number of pages must be positive"); + + int bufidx = 0; + uint8_t *buffer = buf; + uint32_t *spare_data = spare; + + // Read the first page from the NAND. Notice that if ECC is requested, it is mandatory to read + // the spares even if we will not return them to the caller, because otherwise the + // controller is unable to perform ECC calculation. + nand_cmd_read1_async(bufidx, addr, NAND_PAGE_SIZE + (spare || ecc ? 16 : 0), ecc); + addr += NAND_PAGE_SIZE; + + for (int i=0; i= 0 && num_blocks_log2 < 16, "invalid ATB entry size: %d", 1<> 30 == 0, "Allowed PI addresses are in range [0 .. 0x3FFFFFFF] (0x%08lX)", pi_address); + assertf(pi_address % NAND_BLOCK_SIZE == 0, "PI address must be block-aligned (0x%08lX)", pi_address); + + if (mmap_atb_idx > 0) { + uint32_t prev_pi_address = PI_BB_ATB_LOWER[mmap_atb_idx-1] * NAND_BLOCK_SIZE; + assertf(pi_address >= prev_pi_address, "PI addresses must be in increasing order (0x%08lX < 0x%08lX)", pi_address, prev_pi_address); + } + + if (flags & NAND_MMAP_ENCRYPTED) { + if (mmap_atb_idx >= PI_BB_ATB_MAX_ENTRIES) + return -1; + atb_write_ivsource(mmap_atb_idx++, pi_address - NAND_BLOCK_SIZE); + } + + while (*blocks != -1) { + // Calculate how many consecutive blocks we can map + int bidx = *blocks; + + int nseq = 1; + while (blocks[nseq] == bidx + nseq) nseq++; + blocks += nseq; + + // Map this sequence as subsequent ATB entries + while (nseq > 0) { + if (mmap_atb_idx >= PI_BB_ATB_MAX_ENTRIES) + return -1; + + // The longest sequence we can map is limited by the PI address + // alignment. For instance, given a PI address of 0x10010000, + // we can only map 0x10000 bytes (4 blocks) in a single ATB entry. + int nseq_log2 = 31 - __builtin_clz(nseq); + int piaddr_align = __builtin_ctz(pi_address / NAND_BLOCK_SIZE); + int n_log2 = MIN(MIN(nseq_log2, piaddr_align), 15); + int n = 1 << n_log2; + + // Write the ATB entry + atb_write(mmap_atb_idx++, pi_address, bidx, n_log2); + + bidx += n; + nseq -= n; + pi_address += n * NAND_BLOCK_SIZE; + } + } + + return 0; +} + +void nand_mmap_end(void) +{ + assertf(mmap_atb_idx != -1, "nand_mmap_begin() was not called"); + + // Fill all ATB entries with increasing addresses + if (mmap_atb_idx < PI_BB_ATB_MAX_ENTRIES) { + uint32_t pi_address = 0; + if (mmap_atb_idx > 0) + pi_address = PI_BB_ATB_LOWER[mmap_atb_idx-1] * NAND_BLOCK_SIZE + NAND_BLOCK_SIZE; + for (; mmap_atb_idx < PI_BB_ATB_MAX_ENTRIES; mmap_atb_idx++) { + atb_write(mmap_atb_idx, pi_address, 0, 0); + pi_address += NAND_BLOCK_SIZE; + } + } + + // ATB requires a read command to be programmed into PI_BB_NAND_CTRL. + *PI_BB_NAND_CTRL = NAND_CMD_READ1_H0; + mmap_atb_idx = -1; +} + +void nand_compute_page_ecc(const uint8_t *buf, uint8_t *ecc) +{ + // This function implements the ECC algorithm used in SmartMedia, and in + // some flash filesystems like YAFFS2. Another C implementation can be found + // in the YAFFS2 source code: + // https://github.com/m-labs/rtems-yaffs2/blob/23be2ac372173ecf701a0ab179c510ca54b678d0/yaffs_ecc.c + + // ECC parity table, calculated via gen-ecc.py. This parity table contains + // various parity bits for each byte of the input data. Bit 1 is always 0, + // and bit 0 is the parity of the while byte. + // The table is vertically symmetric (when viewed as a 16x16 matrix), so + // we store only the first half, and the mirror vertically the accesses for + // the second half. + static const uint8_t ecc_table[128] = { + 0x00, 0x55, 0x59, 0x0c, 0x65, 0x30, 0x3c, 0x69, 0x69, 0x3c, 0x30, 0x65, 0x0c, 0x59, 0x55, 0x00, + 0x95, 0xc0, 0xcc, 0x99, 0xf0, 0xa5, 0xa9, 0xfc, 0xfc, 0xa9, 0xa5, 0xf0, 0x99, 0xcc, 0xc0, 0x95, + 0x99, 0xcc, 0xc0, 0x95, 0xfc, 0xa9, 0xa5, 0xf0, 0xf0, 0xa5, 0xa9, 0xfc, 0x95, 0xc0, 0xcc, 0x99, + 0x0c, 0x59, 0x55, 0x00, 0x69, 0x3c, 0x30, 0x65, 0x65, 0x30, 0x3c, 0x69, 0x00, 0x55, 0x59, 0x0c, + 0xa5, 0xf0, 0xfc, 0xa9, 0xc0, 0x95, 0x99, 0xcc, 0xcc, 0x99, 0x95, 0xc0, 0xa9, 0xfc, 0xf0, 0xa5, + 0x30, 0x65, 0x69, 0x3c, 0x55, 0x00, 0x0c, 0x59, 0x59, 0x0c, 0x00, 0x55, 0x3c, 0x69, 0x65, 0x30, + 0x3c, 0x69, 0x65, 0x30, 0x59, 0x0c, 0x00, 0x55, 0x55, 0x00, 0x0c, 0x59, 0x30, 0x65, 0x69, 0x3c, + 0xa9, 0xfc, 0xf0, 0xa5, 0xcc, 0x99, 0x95, 0xc0, 0xc0, 0x95, 0x99, 0xcc, 0xa5, 0xf0, 0xfc, 0xa9, + }; + + for (int j=0; j<2; j++) { + uint8_t ecc2=0; + uint32_t l0=0, l1=0; + + for (int i=0; i<256; i++) { + // Lookup the parity table (with vertical symmetry for the second half) + uint8_t val = *buf++; + val = ecc_table[val < 128 ? val : val ^ 0xf0]; + // If byte has odd parity, update the line parity + if (val & 1) { l0 ^= i; l1 ^= ~i; val ^= 1; } + // Update column parity + ecc2 ^= val; + } + + // Interleave the bits of the two line parities (l0 and l1) + l0 = (l0 | (l0 << 4)) & 0x0F0F; + l0 = (l0 | (l0 << 2)) & 0x3333; + l0 = (l0 | (l0 << 1)) & 0x5555; + l1 &= 0xFF; + l1 = (l1 | (l1 << 4)) & 0x0F0F; + l1 = (l1 | (l1 << 2)) & 0x3333; + l1 = (l1 | (l1 << 1)) & 0x5555; + l0 = (l1 << 0) | (l0 << 1); + + // Store the inverted line parities and column parity + *ecc++ = ~l0 >> 0; + *ecc++ = ~l0 >> 8; + *ecc++ = ~ecc2; + } +} diff --git a/src/bb/skc.S b/src/bb/skc.S new file mode 100644 index 000000000..7d6b40963 --- /dev/null +++ b/src/bb/skc.S @@ -0,0 +1,38 @@ +############################################################ +# skc.S +# +# Authors: Giovanni Bajo +############################################################ +#include "regs.S" + + .text + .set noreorder + +.macro skc_call id + li v0, \id # Load the system call number in $v0 + lw zero, 0xA4300014 # Force a SK trap. This will cause a NMI to be issued. + nop # NMI is a bit delayed to avoid retriggering on the load + nop + jr ra + nop +.endm + + .global skc_get_id +skc_get_id: + skc_call 0 + + .global skc_launch_setup +skc_launch_setup: + skc_call 1 + + .global skc_launch +skc_launch: + skc_call 2 + + .global skc_exit +skc_exit: + skc_call 13 + + .global skc_keep_alive +skc_keep_alive: + skc_call 14 diff --git a/src/compress/aplib_dec.c b/src/compress/aplib_dec.c index a969b1cfd..17aa629b7 100644 --- a/src/compress/aplib_dec.c +++ b/src/compress/aplib_dec.c @@ -11,6 +11,8 @@ #include "dma.h" #include "dragonfs.h" #include "debug.h" +#include "asset.h" + #include #endif #include "../utils.h" @@ -35,15 +37,9 @@ /** @brief APLib decompressor */ typedef struct { - uint8_t buf[2][128] __attribute__((aligned(16))); ///< Buffer of loaded data - int cur_buf; ///< Current buffer being used - uint8_t *buf_ptr; ///< Pointer to data being processed - uint8_t *buf_end; ///< Pointer to end of current loaded data + __lookahead_buf_t lb; ///< Lookahead reader state uint8_t cc; ///< Current byte being processed int shift; ///< Current bit being processed - int fd; ///< File being read from - uint32_t rom_addr; ///< ROM address being read from (optional) - bool eof; ///< Whether the end of the stream has been reached struct { decompress_ringbuf_t ringbuf; ///< Ring buffer bool first_literal_done; ///< Whether the first literal has been written @@ -55,34 +51,9 @@ typedef struct { _Static_assert(sizeof(aplib_decompressor_t) <= DECOMPRESS_APLIB_STATE_SIZE, "APLib decompressor state too small"); -__attribute__((noinline)) -static void refill(aplib_decompressor_t *d) +static inline uint8_t readbyte(aplib_decompressor_t *d) { - int buf_size; - - d->cur_buf ^= 1; - #ifdef N64 - if (d->rom_addr) { - data_cache_hit_invalidate(d->buf[d->cur_buf^1], sizeof(d->buf[0])); - dma_read_raw_async(d->buf[d->cur_buf^1], d->rom_addr, sizeof(d->buf[0])); - d->rom_addr += sizeof(d->buf[0]); - buf_size = sizeof(d->buf[0]); - } else { - buf_size = read(d->fd, d->buf[d->cur_buf], sizeof(d->buf[0])); - } - #else - buf_size = read(d->fd, d->buf[d->cur_buf], sizeof(d->buf[0])); - #endif - - d->buf_ptr = d->buf[d->cur_buf]; - d->buf_end = d->buf[d->cur_buf] + buf_size; - if (!buf_size) d->eof = true; -} - -static uint8_t readbyte(aplib_decompressor_t *d) -{ - if (unlikely(d->buf_ptr >= d->buf_end)) refill(d); - return *d->buf_ptr++; + return __lookahead_buf_readbyte(&d->lb); } static int readbit(aplib_decompressor_t *d) @@ -105,124 +76,26 @@ static inline int readgamma2(aplib_decompressor_t *d) static void decompress_reset(aplib_decompressor_t *d) { d->shift = -1; - d->eof = false; + __lookahead_buf_reset(&d->lb); d->partial.ringbuf.ringbuf_pos = 0; d->partial.first_literal_done = false; d->partial.nlit = 0; d->partial.match_off = 0; d->partial.match_len = 0; - d->buf_ptr = 0; - d->buf_end = 0; - - #ifdef N64 - if (d->rom_addr) { - data_cache_hit_invalidate(d->buf[d->cur_buf^1], sizeof(d->buf[0])); - dma_read_raw_async(d->buf[d->cur_buf^1], d->rom_addr, sizeof(d->buf[0])); - d->rom_addr += sizeof(d->buf[0]); - } - #endif } static void decompress_init(aplib_decompressor_t *d, int fd, uint32_t rom_addr) { memset(d, 0, sizeof(*d)); - d->fd = fd; - d->rom_addr = rom_addr; + (void)rom_addr; + __lookahead_buf_init(&d->lb, fd); decompress_reset(d); } -static int decompress_full(aplib_decompressor_t *d, uint8_t *out) -{ - uint8_t *out_orig = out; - int nlit = 3; - int match_off = -1; - int match_len = -1; - - *out++ = readbyte(d); - while (!d->eof) { - if (!readbit(d)) { - // 0: literal - *out++ = readbyte(d); - nlit = 3; - // fprintf(stderr, "%x: lit %x\n", out-out_orig-1, out[-1]); - continue; - } - if (!readbit(d)) { - // 10: 8+n bits offset - int off_hi = readgamma2(d) - nlit; - if (off_hi >= 0) { - match_off = (off_hi << 8) | readbyte(d); - match_len = readgamma2(d); - if (match_off < 128 || match_off >= MINMATCH4_OFFSET) - match_len += 2; - else if (match_off >= MINMATCH3_OFFSET) - match_len += 1; - // fprintf(stderr, "%x: offset8 %x %x\n", out-out_orig,match_off, match_len); - } else { - // rep-match - match_len = readgamma2(d); - // fprintf(stderr, "%x: offset8 rep %x\n", out-out_orig, match_len); - } - } else if (!readbit(d)) { - // 110: 7 bits offset + 1 bit length - uint8_t cmd = readbyte(d); - // fprintf(stderr, "%x: offset7 %02x\n", out-out_orig, cmd); - if (cmd == 0) { - // end of stream - // debugf("EOD\n"); - // fprintf(stderr, "EOD\n"); - d->eof = true; - break; - } - - match_off = cmd >> 1; - *out = out[-match_off], out++; - *out = out[-match_off], out++; - if (cmd & 1) - *out = out[-match_off], out++; - nlit = 2; - continue; - } else { - // 111: 4 bits offset - int match_off2 = readbit(d) << 3; - match_off2 |= readbit(d) << 2; - match_off2 |= readbit(d) << 1; - match_off2 |= readbit(d) << 0; - // fprintf(stderr, "%x: offset4 %x\n", out-out_orig, match_off2); - nlit = 3; - if (match_off2) { - *out = out[-match_off2]; - out++; - } else { - *out++ = 0; - } - continue; - } - - if (match_off >= match_len) { - do { - memcpy(out, out - match_off, 8); - out += 8; - match_len -= 8; - } while (match_len > 0); - out += match_len; - } else { - while (match_len-- > 0) { - *out = out[-match_off]; - out++; - } - } - nlit = 2; - } - - // fprintf(stderr, "return %x\n", out - out_orig); - return out - out_orig; -} - __attribute__((used)) static int decompress_aplib_partial(aplib_decompressor_t *d, uint8_t *out, int len) { - if (!len || d->eof) return 0; + if (!len || d->lb.eof) return 0; uint8_t *out_orig = out; uint8_t *out_end = out + len; @@ -268,7 +141,7 @@ static int decompress_aplib_partial(aplib_decompressor_t *d, uint8_t *out, int l uint8_t cmd = readbyte(d); if (cmd == 0) { // end of stream - d->eof = true; + d->lb.eof = true; break; } @@ -325,22 +198,6 @@ ssize_t decompress_aplib_read(void *state, void *buf, size_t len) return decompress_aplib_partial(d, buf, len); } -void* decompress_aplib_full(const char *fn, int fd, size_t cmp_size, size_t size) -{ - uint32_t rom_addr = 0; - #ifdef N64 - if (strncmp(fn, "rom:/", 5) == 0) { - rom_addr = (dfs_rom_addr(fn+5) & 0x1fffffff) + lseek(fd, 0, SEEK_CUR); - } - #endif - void *buf = memalign(ASSET_ALIGNMENT, size + 8); - aplib_decompressor_t d; - decompress_init(&d, fd, rom_addr); - int sz = decompress_full(&d, buf); (void)sz; - buf = realloc(buf, size); - return buf; -} - #ifdef N64 int decompress_aplib_full_inplace(const uint8_t* in, size_t cmp_size, uint8_t *out, size_t size) { diff --git a/src/compress/aplib_dec_fast.S b/src/compress/aplib_dec_fast.S index effdc43ea..1ed41ea01 100644 --- a/src/compress/aplib_dec_fast.S +++ b/src/compress/aplib_dec_fast.S @@ -61,19 +61,19 @@ decompress_aplib_full_fast: b .Lcmd_literal li cc_count, -1 - # Main copy-match loop. This is the basic LZ constrcut, where + # Main copy-match loop. This is the basic LZ construct, where # we copy match_len bytes from match_off bytes before the current # outbuf pointer. Notice that match_off can be less than match_len - # in which case it is mandatory to copy byte by byte for correct - # behavior (memmove() vs memcpy()). + # (overlapping match): the 8-byte loop is still correct as long as + # match_off >= 8, as each chunk only reads bytes already written. # We implement 3 different loops for optimization purposes: - # - 1-byte copy loop: copy 1 byte at a time - # - 8-byte copy loop: copy 8 bytes at a time + # - 1-byte copy loop: copy 1 byte at a time (match_off 2..7) + # - 8-byte copy loop: copy 8 bytes at a time (match_off >= 8) # - memset loop: read one 1 byte (match_off=1), memset 8 bytes at a time # To make the second and third loop more generally useful, we # allow the code to write past the end of the output buffer (up to 8 bytes). .Lmatch: - blt match_off, match_len, .Lmatch1_loop_check + blt match_off, 8, .Lmatch1_loop_check sub $v0, outbuf, match_off .Lmatch8_loop: # 8-byte copy loop ldl $t0, 0($v0) # load 8 bytes diff --git a/src/compress/aplib_dec_internal.h b/src/compress/aplib_dec_internal.h index 538c2fe8a..e9c504d84 100644 --- a/src/compress/aplib_dec_internal.h +++ b/src/compress/aplib_dec_internal.h @@ -5,17 +5,10 @@ #ifndef LIBDRAGON_COMPRESS_APLIB_DEC_INTERNAL_H #define LIBDRAGON_COMPRESS_APLIB_DEC_INTERNAL_H -/** @brief Set to 0 to disable assembly implementation of the full decoder */ -#ifdef N64 -#define DECOMPRESS_APLIB_FULL_USE_ASM 1 -#else -#define DECOMPRESS_APLIB_FULL_USE_ASM 0 -#endif - #include /** @brief Size of APLIB decompressor state */ -#define DECOMPRESS_APLIB_STATE_SIZE 348 +#define DECOMPRESS_APLIB_STATE_SIZE 368 /** @brief Initialize APLIB decompressor */ void decompress_aplib_init(void *state, int fd, int winsize); @@ -24,12 +17,9 @@ ssize_t decompress_aplib_read(void *state, void *buf, size_t len); /** @brief Reset APLIB decompressor state */ void decompress_aplib_reset(void *state); -#if DECOMPRESS_APLIB_FULL_USE_ASM +#ifdef N64 /** @brief Decompress APLIB data in-place using fast assembly implementation */ int decompress_aplib_full_inplace(const uint8_t* in, size_t cmp_size, uint8_t *out, size_t size); -#else -/** @brief Decompress entire APLIB file */ -void* decompress_aplib_full(const char *fn, int fd, size_t cmp_size, size_t size); -#endif +#endif /* N64 */ #endif diff --git a/src/compress/libdragon.mk b/src/compress/libdragon.mk index 2a356c685..a4773463e 100644 --- a/src/compress/libdragon.mk +++ b/src/compress/libdragon.mk @@ -6,4 +6,5 @@ LIBDRAGON_OBJS += \ $(BUILD_DIR)/compress/aplib_dec_fast.o \ $(BUILD_DIR)/compress/aplib_dec.o \ $(BUILD_DIR)/compress/shrinkler_dec_fast.o \ - $(BUILD_DIR)/compress/shrinkler_dec.o \ No newline at end of file + $(BUILD_DIR)/compress/shrinkler_dec.o \ + $(BUILD_DIR)/compress/minishrinkler.o diff --git a/src/compress/lz4_dec.c b/src/compress/lz4_dec.c index 4f7a00efd..6aad23f23 100644 --- a/src/compress/lz4_dec.c +++ b/src/compress/lz4_dec.c @@ -33,44 +33,6 @@ #endif /// @endcond -/** @brief Decode multi-byte length. */ -#define LZ4ULTRA_DECOMPRESSOR_BUILD_LEN(__len) { \ - unsigned int byte; \ - do { \ - if (unlikely(pInBlock >= pInBlockEnd)) return -1; \ - if (dma_race) wait_dma(pInBlock+1); \ - byte = (unsigned int)*pInBlock++; \ - __len += byte; \ - } while (unlikely(byte == 255)); \ -} - -#ifdef N64 -#include "dma.h" -#endif - -__attribute__((used)) -static void wait_dma(const void *pIn) { - #ifdef N64 - static void *ptr; static bool finished = false; - if (pIn == NULL) { - finished = false; - ptr = NULL; - return; - } - if (finished) return; - while (ptr < pIn) { - // Check if DMA is finished - if (!(*PI_STATUS & 1)) { - finished = true; - return; - } - // Read current DMA position. Ignore partial cachelines as they - // would create coherency problems if accessed by the CPU. - ptr = (void*)((*PI_DRAM_ADDR & ~0xF) | 0x80000000); - } - #endif -} - /** * Decompress one data block * @@ -81,96 +43,12 @@ static void wait_dma(const void *pIn) { * * @return size of decompressed data in bytes, or -1 for error */ -int decompress_lz4_full_inplace(const unsigned char *pInBlock, size_t nBlockSize, unsigned char *pOutData, size_t nBlockMaxSize) { #ifdef N64 + int decompress_lz4_full_inplace(const unsigned char *pInBlock, size_t nBlockSize, unsigned char *pOutData, size_t nBlockMaxSize) { extern int decompress_lz4_full_fast(const void *inbuf, int insize, void *outbuf); return decompress_lz4_full_fast(pInBlock, nBlockSize, pOutData); -#else - const unsigned char *pInBlockEnd = pInBlock + nBlockSize; - unsigned char *pCurOutData = pOutData; - const unsigned char *pOutDataEnd = pCurOutData + nBlockMaxSize; - const unsigned char *pOutDataFastEnd = pOutDataEnd - 18; - const bool dma_race = true; - - if (dma_race) wait_dma(NULL); - while (likely(pInBlock < pInBlockEnd)) { - if (dma_race) wait_dma(pInBlock+1); - const unsigned int token = (unsigned int)*pInBlock++; - unsigned int nLiterals = ((token & 0xf0) >> 4); - - if (nLiterals != LITERALS_RUN_LEN && pCurOutData <= pOutDataFastEnd && (pInBlock + 16) <= pInBlockEnd) { - if (dma_race) wait_dma(pInBlock+16); - memcpy(pCurOutData, pInBlock, 16); - } - else { - if (likely(nLiterals == LITERALS_RUN_LEN)) - LZ4ULTRA_DECOMPRESSOR_BUILD_LEN(nLiterals); - - if (unlikely((pInBlock + nLiterals) > pInBlockEnd)) return -1; - if (unlikely((pCurOutData + nLiterals) > pOutDataEnd)) return -2; - - if (dma_race) wait_dma(pInBlock+nLiterals); - memcpy(pCurOutData, pInBlock, nLiterals); - } - - pInBlock += nLiterals; - pCurOutData += nLiterals; - - if (likely((pInBlock + 2) <= pInBlockEnd)) { - unsigned int nMatchOffset; - - if (dma_race) wait_dma(pInBlock+2); - nMatchOffset = (unsigned int)*pInBlock++; - nMatchOffset |= ((unsigned int)*pInBlock++) << 8; - - unsigned int nMatchLen = (token & 0x0f); - - nMatchLen += MIN_MATCH_SIZE; - if (nMatchLen != (MATCH_RUN_LEN + MIN_MATCH_SIZE) && nMatchOffset >= 8 && pCurOutData <= pOutDataFastEnd) { - const unsigned char *pSrc = pCurOutData - nMatchOffset; - - if (unlikely(pSrc < pOutData)) return -3; - - memcpy(pCurOutData, pSrc, 8); - memcpy(pCurOutData + 8, pSrc + 8, 8); - memcpy(pCurOutData + 16, pSrc + 16, 2); - - pCurOutData += nMatchLen; - } - else { - if (likely(nMatchLen == (MATCH_RUN_LEN + MIN_MATCH_SIZE))) - LZ4ULTRA_DECOMPRESSOR_BUILD_LEN(nMatchLen); - - if (unlikely((pCurOutData + nMatchLen) > pOutDataEnd)) return -4; - - const unsigned char *pSrc = pCurOutData - nMatchOffset; - if (unlikely(pSrc < pOutData)) return -1; - - if (nMatchOffset >= 16 && (pCurOutData + nMatchLen) <= pOutDataFastEnd) { - const unsigned char *pCopySrc = pSrc; - unsigned char *pCopyDst = pCurOutData; - const unsigned char *pCopyEndDst = pCurOutData + nMatchLen; - - do { - memcpy(pCopyDst, pCopySrc, 16); - pCopySrc += 16; - pCopyDst += 16; - } while (pCopyDst < pCopyEndDst); - - pCurOutData += nMatchLen; - } - else { - while (nMatchLen--) { - *pCurOutData++ = *pSrc++; - } - } - } - } - } - - return (int)(pCurOutData - pOutData); -#endif } +#endif /** * @brief Fast-access state of the LZ4 algorithm (streaming version). diff --git a/src/compress/lz4_dec_fast.S b/src/compress/lz4_dec_fast.S index d0aef588f..b76f25d6c 100644 --- a/src/compress/lz4_dec_fast.S +++ b/src/compress/lz4_dec_fast.S @@ -81,9 +81,11 @@ decompress_lz4_full_fast: addiu match_len, MINMATCH # add implicit MINMATCH to match length .Lmatch: - blt match_off, match_len, .Lmatch1_loop_check # check if we can do 8-byte copy + blt match_off, 8, .Lmatch1_loop_check # if offset < 8, we can't do 8-byte copies sub $v0, outbuf, match_off # calculate start of match -.Lmatch8_loop: # 8-byte copy loop +.Lmatch8_loop: # 8-byte copy loop (safe for offset >= 8, + # even if match_len > offset: each chunk + # only reads bytes already written) ldl $t0, 0($v0) # load 8 bytes ldr $t0, 7($v0) addiu $v0, 8 diff --git a/src/compress/lz4_dec_internal.h b/src/compress/lz4_dec_internal.h index aaf71adba..f7a2bf06e 100644 --- a/src/compress/lz4_dec_internal.h +++ b/src/compress/lz4_dec_internal.h @@ -56,7 +56,7 @@ * @param src_size Size of the compressed data in bytes * @param dst Pointer to destination buffer (decompressed data) * @param dst_size Size of the destination buffer in bytes - * @return int Number of bytes decompressed, or -1 on error. + * @return Number of bytes decompressed, or -1 on error. */ int decompress_lz4_full_inplace(const uint8_t *src, size_t src_size, uint8_t *dst, size_t dst_size); diff --git a/src/compress/lzh5.c b/src/compress/lzh5.c index fa37e1b1c..6b5cc0e45 100644 --- a/src/compress/lzh5.c +++ b/src/compress/lzh5.c @@ -17,6 +17,8 @@ #include "utils.h" #include "n64sys.h" #include "dragonfs.h" +#include "asset.h" + #else #include #endif @@ -42,6 +44,7 @@ typedef struct { uint8_t *buf_ptr, *buf_end; int buf_size; int cur_buf; + uint64_t ticket[2]; // Bits from the input stream that are waiting to be read. uint64_t bit_buffer; @@ -59,8 +62,9 @@ static void bit_stream_reader_init(BitStreamReader *reader, FILE *fp, uint32_t r #ifdef N64 if (reader->rom_addr) { - data_cache_hit_invalidate(reader->buf[reader->cur_buf^1], sizeof(reader->buf[0])); - dma_read_raw_async(reader->buf[reader->cur_buf^1], reader->rom_addr, sizeof(reader->buf[0])); + int next = reader->cur_buf ^ 1; + data_cache_hit_invalidate(reader->buf[next], sizeof(reader->buf[0])); + reader->ticket[next] = dma_read_raw_async(reader->buf[next], reader->rom_addr, sizeof(reader->buf[0])); reader->rom_addr += sizeof(reader->buf[0]); } #endif @@ -72,8 +76,10 @@ static void refill_bits_fetch(BitStreamReader *reader) #ifdef N64 if (reader->rom_addr) { - data_cache_hit_invalidate(reader->buf[reader->cur_buf^1], sizeof(reader->buf[0])); - dma_read_raw_async(reader->buf[reader->cur_buf^1], reader->rom_addr, sizeof(reader->buf[0])); + dma_wait_finished(reader->ticket[reader->cur_buf]); + int next = reader->cur_buf ^ 1; + data_cache_hit_invalidate(reader->buf[next], sizeof(reader->buf[0])); + reader->ticket[next] = dma_read_raw_async(reader->buf[next], reader->rom_addr, sizeof(reader->buf[0])); reader->rom_addr += sizeof(reader->buf[0]); reader->buf_size = sizeof(reader->buf[0]); #else @@ -557,7 +563,7 @@ int decompress_lzh5_pos(void *state) { void* decompress_lzh5_full(const char *fn, FILE *fp, size_t cmp_size, size_t size) { void *s = memalign(ASSET_ALIGNMENT, size); - assertf(s, "asset_load: out of memory"); + assertf(s, "Out of memory"); uint32_t rom_addr = 0; #ifdef N64 diff --git a/src/compress/lzh5_internal.h b/src/compress/lzh5_internal.h index b2f1eacc3..3b3b35ec5 100644 --- a/src/compress/lzh5_internal.h +++ b/src/compress/lzh5_internal.h @@ -18,7 +18,7 @@ extern "C" { * Note that this can still be allocated on the stack, as the stack size * configured by libdragon is 64KB. */ -#define DECOMPRESS_LZH5_STATE_SIZE (6096+16) +#define DECOMPRESS_LZH5_STATE_SIZE (6128) /** * @brief Default window size for LZH5 decompression. diff --git a/src/compress/minishrinkler.c b/src/compress/minishrinkler.c new file mode 100644 index 000000000..1ac4f1740 --- /dev/null +++ b/src/compress/minishrinkler.c @@ -0,0 +1,833 @@ +/** + * @file minishrinkler.c + * @author Giovanni Bajo + * @brief Minishrinkler compression API implementation + * + * This file contains the core compression logic for Minishrinkler, + * providing a buffer-to-buffer compression API without any file I/O. + * + * This implements the Shrinkler bitstream with a very simplified compression + * algorithm. Since the LZ coder is very basic, the compression ratio is good + * only on small files up to a few KiB (where the range coder itself beats + * standard LZ+Huffman constructs). Once the file size grows beyond that, + * the lack of advanced LZ techniques makes the compression ratio degrade + * quickly, and for large files the compression ratio is much worse than + * standard algorithms like DEFLATE. + */ +#include "minishrinkler_internal.h" +#include "scratch.h" +#include +#include +#include +#if TRACE_SHRINKLER +#include +#endif +#include + +///@cond + +// Compile-time trace control +#define TRACE_SHRINKLER 0 + +#if TRACE_SHRINKLER +static void tracef(const char *fmt, ...) { + static FILE *trace_file = NULL; + if (!trace_file) { + trace_file = fopen("minishrinkler.log", "w"); + } + if (trace_file) { + va_list args; + va_start(args, fmt); + vfprintf(trace_file, fmt, args); + va_end(args); + } +} +#else +#define tracef(fmt, ...) ((void)0) +#endif + +// Configuration - Embedded optimized +#define MAX_FILE_SIZE (1024*1024) // 1MB max for embedded +#define MAX_MATCH_LENGTH 65535 +#define MAX_OFFSET 65535 +#define MIN_MATCH_LENGTH 3 +#define WINDOW_SIZE 65536 + +// Context configuration (must match decompressor) +#define ADJUST_SHIFT 4 +#define NUM_SINGLE_CONTEXTS 1 +#define NUM_CONTEXT_GROUPS 4 +#define CONTEXT_GROUP_SIZE 256 +#define NUM_CONTEXTS (NUM_SINGLE_CONTEXTS + NUM_CONTEXT_GROUPS * CONTEXT_GROUP_SIZE) + +// Context indices +#define CONTEXT_KIND 0 +#define CONTEXT_REPEATED -1 +#define CONTEXT_GROUP_LIT 0 +#define CONTEXT_GROUP_OFFSET 2 +#define CONTEXT_GROUP_LENGTH 3 + +///@endcond + +// Moving window: computed dynamically from work memory; keep max at 64K +// (we will compute window_size and mask at runtime based on available memory) + +/** @brief Range coder context */ +typedef struct { + uint16_t contexts[NUM_CONTEXTS]; ///< Context modeling + uint8_t *output; ///< Output buffer + uint32_t output_size; ///< Output buffer size + uint32_t output_capacity; ///< Output buffer capacity + int32_t dest_bit; ///< Destination bit + uint32_t intervalsize; ///< Interval size + uint32_t intervalmin; ///< Interval minimum + int lit_avg_events; ///< EWMA of literal event cost (init ~9) +} shr_rangecoder_t; + +/** @brief LZ coder state */ +typedef struct { + bool after_first; ///< First symbol was done? + bool prev_was_ref; ///< Previous symbols was a match? + int parity; ///< Parity bit + uint16_t last_offsets[3]; ///< Recent offsets LRU: [0]=last, [1]=prev, [2]=older +} shr_lzstate_t; + +/** @brief Size table for encoding numbers */ +static const uint8_t size_table[128] = { + 0x40,0x3f,0x3f,0x3e,0x3d,0x3c,0x3c,0x3b,0x3a,0x3a,0x39,0x38,0x38,0x37,0x36,0x36,0x35,0x34,0x34,0x33,0x33,0x32,0x31,0x31,0x30,0x30,0x2f,0x2e,0x2e,0x2d,0x2d,0x2c,0x2b,0x2b,0x2a,0x2a,0x29,0x29,0x28,0x27,0x27,0x26,0x26,0x25,0x25,0x24,0x24,0x23,0x23,0x22,0x22,0x21,0x21,0x20,0x20,0x1f,0x1e,0x1e,0x1d,0x1d,0x1d,0x1c,0x1c,0x1b,0x1b,0x1a,0x1a,0x19,0x19,0x18,0x18,0x17,0x17,0x16,0x16,0x15,0x15,0x15,0x14,0x14,0x13,0x13,0x12,0x12,0x11,0x11,0x11,0x10,0x10,0xf,0xf,0xe,0xe,0xe,0xd,0xd,0xc,0xc,0xc,0xb,0xb,0xa,0xa,0x9,0x9,0x9,0x8,0x8,0x8,0x7,0x7,0x6,0x6,0x6,0x5,0x5,0x4,0x4,0x4,0x3,0x3,0x3,0x2,0x2,0x1,0x1,0x1,0x0 +}; + +/** + * @brief Layout structure for working memory bufferr + * + * All the work memory used by the compressor is described by this structure. + * No dynamic allocations beyond the single arena. + * + * The match finder uses a compact set-associative hash table carved from + * the provided work memory. Each bucket keeps a small number (ways) of wrapped + * 16-bit positions. + */ +typedef struct { + shr_rangecoder_t coder; ///< Range coder context + shr_lzstate_t state; ///< LZ coder state + + // Hash table configuration and sliding window + int hash_size; ///< Number of buckets (power of two) + int hash_mask; ///< hash_size - 1 + int ways; ///< Associativity (entries per bucket) + int window_bits; ///< such that window_size == (1 << window_bits) + int window_size; ///< sliding window size (<= 65536) + int window_mask; ///< window_size - 1 + + // Pointers into the single malloc arena (immediately after this struct) + uint16_t *buckets; ///< size = hash_size * ways; each is wrapped pos (0..mask) or 0xFFFF if empty + uint8_t *repl_index; ///< size = hash_size; round-robin replacement index per bucket + + // Rolling estimate of literal event cost (EWMA of encode_literal sizes) + int lit_avg_events; ///< average range-coder events per literal (init ~9) +} shr_work_buffer_t; + +// Utility functions +static int min(int a, int b) { return a < b ? a : b; } + +#if 0 +// Initialize size table +static void init_size_table(shr_work_buffer_t *mem) { + for (int i = 0; i < 128; i++) { + mem->size_table[i] = (int) floor(0.5 + (8.0 - log((double) (128 + i)) / log(2.0)) * (1 << 6)); + } +} +#endif + +// Improved hash function for 3-byte sequences +static unsigned int hash3(const unsigned char *data) { + // Mix into 32-bit and rely on power-of-two mask for index + unsigned int v = (unsigned int)data[0] | ((unsigned int)data[1] << 8) | ((unsigned int)data[2] << 16); + v *= 0x9E3779B1u; // golden ratio multiplier + v ^= v >> 16; + return v; +} + +// Approximate cost of encode_number as used by the coder: number >= 2 +static int estimate_number_cost_int(int number) { + int i = 0; + while ((4 << i) <= number) i++; + // continuation bits (i), stop bit (1), payload bits (i+1) + return i + 1 + (i + 1); +} + +// Update hash table with new position (set-associative, round-robin replacement) +static void update_hash(shr_work_buffer_t *mem, const unsigned char *data, int pos, int data_size) { + if (pos + 2 >= data_size) return; + + unsigned int hash = hash3(&data[pos]) & (unsigned int)mem->hash_mask; + + // Round-robin replacement within the bucket + int way = mem->repl_index[hash] % mem->ways; + mem->repl_index[hash] = (uint8_t)((mem->repl_index[hash] + 1) % mem->ways); + + // Store wrapped position + uint16_t wrapped = (uint16_t)(pos & mem->window_mask); + mem->buckets[hash * mem->ways + way] = wrapped; + + tracef("UPDATE_HASH: pos=%d, hash=%d, way=%d stored_pos=%u (wrapped, mask=0x%04x)\n", + pos, hash, way, wrapped, (unsigned)mem->window_mask); +} + +// Exact copy of original RangeCoder logic (adapted for static allocation) +static void range_coder_init(shr_rangecoder_t *coder, uint8_t *output, int capacity) { + coder->output = output; + coder->output_capacity = capacity; + coder->output_size = 0; + coder->dest_bit = -1; // Start at -1 like original + coder->intervalsize = 0x8000; + coder->intervalmin = 0; + + // Initialize contexts (must match decompressor) + for (int i = 0; i < NUM_CONTEXTS; i++) { + coder->contexts[i] = 0x8000; + } + + // Ensure first byte is initialized + output[0] = 0; + coder->lit_avg_events = 9; +} + +// Forward declaration for tracing (only when tracing is enabled) +#if TRACE_SHRINKLER +static void range_coder_trace_state(shr_rangecoder_t *coder, const char *operation, int context, int bit, int size); +#endif + +// Exact copy of original add_bit function (adapted for static allocation) +static void add_bit(shr_rangecoder_t *coder) { + int pos = coder->dest_bit; + int bytepos; + int bitmask; + + // tracef(" ADD_BIT: start pos=%d\n", pos); + do { + pos--; + if (pos < 0) return; + bytepos = pos >> 3; + bitmask = 0x80 >> (pos & 7); + + // Initialize byte to 0 if not already done + while (bytepos >= (int)coder->output_size) { + coder->output[coder->output_size++] = 0; + } + + assert(bytepos < (int)coder->output_capacity); + coder->output[bytepos] ^= bitmask; + + // tracef(" ADD_BIT: pos=%d bytepos=%d bitmask=0x%02x old_byte=0x%02x new_byte=0x%02x\n", + // pos, bytepos, bitmask, old_byte, new_byte); + } while ((coder->output[bytepos] & bitmask) == 0); +} + +// Exact copy of original rangecoder_code function +static int range_coder_code(shr_rangecoder_t *coder, int context_index, int bit) { + // Handle special context indices + if (context_index < 0) { + // Special contexts like CONTEXT_REPEATED (-1) are handled differently + // For now, just return 0 size for these + return 0; + } + + // Calculate size_before, handling the case where dest_bit is -1 (initial state) + int size_before; + if (coder->dest_bit < 0) { + // Initial state: dest_bit is -1, so size is just the sizetable entry + size_before = size_table[(coder->intervalsize - 0x8000) >> 8]; + } else { + size_before = (coder->dest_bit << 6) + size_table[(coder->intervalsize - 0x8000) >> 8]; // BIT_PRECISION = 6 + } + + // Trace the input state + // range_coder_trace_state(coder, "CODE_START", context_index, bit, size_before); + + unsigned prob = coder->contexts[context_index]; + unsigned threshold = (coder->intervalsize * prob) >> 16; + unsigned new_prob; + + if (!bit) { + // Zero + coder->intervalmin += threshold; + if (coder->intervalmin & 0x10000) { + add_bit(coder); + } + coder->intervalsize = coder->intervalsize - threshold; + new_prob = prob - (prob >> ADJUST_SHIFT); + } else { + // One + coder->intervalsize = threshold; + new_prob = prob + (0xffff >> ADJUST_SHIFT) - (prob >> ADJUST_SHIFT); + } + + coder->contexts[context_index] = new_prob; + + // Renormalization + int renormalizations = 0; + while (coder->intervalsize < 0x8000) { + coder->dest_bit++; + coder->intervalsize <<= 1; + coder->intervalmin <<= 1; + if (coder->intervalmin & 0x10000) { + tracef(" RENORM: carry bit detected, calling add_bit\n"); + add_bit(coder); + } + renormalizations++; + } + coder->intervalmin &= 0xffff; + + if (renormalizations > 0) { + tracef(" RENORM: %d renormalizations, final intervalmin=0x%04x intervalsize=0x%04x dest_bit=%d\n", + renormalizations, coder->intervalmin, coder->intervalsize, coder->dest_bit); + } + + // Calculate size_after, handling the case where dest_bit is -1 (initial state) + int size_after; + if (coder->dest_bit < 0) { + // Initial state: dest_bit is -1, so size is just the sizetable entry + size_after = size_table[(coder->intervalsize - 0x8000) >> 8]; + } else { + size_after = (coder->dest_bit << 6) + size_table[(coder->intervalsize - 0x8000) >> 8]; // BIT_PRECISION = 6 + } + int size_diff = size_after - size_before; + + // Trace the output state + // range_coder_trace_state(coder, "CODE_END", context_index, bit, size_diff); + + return size_diff; +} + +#if TRACE_SHRINKLER +static void range_coder_trace_state(shr_rangecoder_t *coder, const char *operation, int context, int bit, int size) { + (void)coder; // Suppress unused parameter warning + (void)operation; // Suppress unused parameter warning + (void)context; // Suppress unused parameter warning + (void)bit; // Suppress unused parameter warning + (void)size; // Suppress unused parameter warning + // tracef("RANGECODER: %s context=%d bit=%d size=%d intervalmin=0x%04x intervalsize=0x%04x dest_bit=%d\n", + // operation, context, bit, size, coder->intervalmin, coder->intervalsize, coder->dest_bit); +} +#endif + +// Exact copy of original rangecoder_finish function +static void range_coder_finish(shr_rangecoder_t *coder) { + // Trace the finish start + // tracef("RANGECODER: FINISH_START intervalmin=0x%04x intervalsize=0x%04x dest_bit=%d\n", + // coder->intervalmin, coder->intervalsize, coder->dest_bit); + + int intervalmax = coder->intervalmin + coder->intervalsize; + int final_min = 0; + int final_size = 0x10000; + + // tracef("RANGECODER: FINISH_DETAILED_START intervalmin=0x%04x intervalsize=0x%04x dest_bit=%d intervalmax=0x%04x\n", + // coder->intervalmin, coder->intervalsize, coder->dest_bit, intervalmax); + + while (final_min < (int)coder->intervalmin || final_min + final_size >= intervalmax) { + if (final_min + final_size < intervalmax) { + // tracef("RANGECODER: FINISH_ITERATION %d: final_min=0x%04x final_size=0x%04x < intervalmax=0x%04x -> add_bit\n", + // finish_iterations, final_min, final_size, intervalmax); + add_bit(coder); + final_min += final_size; + } else { + // tracef("RANGECODER: FINISH_ITERATION %d: final_min=0x%04x final_size=0x%04x >= intervalmax=0x%04x -> no add_bit\n", + // finish_iterations, final_min, final_size, intervalmax); + } + coder->dest_bit++; + final_size >>= 1; + } + + int required_bytes = ((coder->dest_bit - 1) >> 3) + 1; + while (required_bytes > (int)coder->output_size) { + coder->output[coder->output_size++] = 0; + } + + // Trace the finish end + // tracef("RANGECODER: FINISH_END final dest_bit=%d out_size=%d required_bytes=%d\n", + // coder->dest_bit, coder->output_size, required_bytes); +} + +// LZ Encoder +static void lz_state_init(shr_lzstate_t *state) { + state->after_first = 0; + state->prev_was_ref = 0; + state->parity = 0; + state->last_offsets[0] = 0; + state->last_offsets[1] = 0; + state->last_offsets[2] = 0; +} + +static int encode_literal(shr_rangecoder_t *coder, unsigned char value, shr_lzstate_t *state) { + int parity = state->parity & 1; + int size = 0; + + // Trace literal encoding + tracef("LZ: ENCODE_LITERAL value=0x%02x (%c) parity=%d after_first=%d\n", + value, (value >= 32 && value <= 126) ? value : '.', parity, state->after_first); + + if (state->after_first) { + range_coder_code(coder, 1 + CONTEXT_KIND + (parity << 8), 0); // KIND_LIT + size++; + } + + int context = 1; + for (int i = 7; i >= 0; i--) { + int bit = ((value >> i) & 1); + int actual_context = 1 + ((parity << 8) | context); // Correct context calculation + range_coder_code(coder, actual_context, bit); + size++; + context = (context << 1) | bit; + } + + // Update state + state->after_first = 1; + state->prev_was_ref = 0; + state->parity = state->parity + 1; + // Update EWMA of literal events (alpha=1/8) + // Note: size is in the same "event" units used by the cost model + coder->lit_avg_events = (coder->lit_avg_events * 7 + size) >> 3; + + return size; +} + +// Exact copy of coder_encode_number from cruncher_c/Coder.c (adapted for RangeCoder) +static int encode_number(shr_rangecoder_t *coder, int context_group, int number) { + int base_context = 1 + (context_group << 8); + + // Trace number encoding start + tracef("ENCODE_NUMBER: group=%d number=%d base_context=%d\n", + context_group, number, base_context); + + // Must be >= 2 like original + if (number < 2) { + tracef("ERROR: number < 2 (%d)\n", number); + return 0; + } + + int size = 0; + int context; + int i; + + // First loop: continuation bits (exact copy) + for (i = 0 ; (4 << i) <= number ; i++) { + context = base_context + (i * 2 + 2); + tracef(" CONTINUE_BIT: i=%d context=%d bit=1 (4< %d)\n", + i, context, (4 << i), number); + size += range_coder_code(coder, context, 0); + + // Second loop: actual bits (exact copy) + for (; i >= 0 ; i--) { + int bit = ((number >> i) & 1); + context = base_context + (i * 2 + 1); + tracef(" NUMBER_BIT: i=%d context=%d bit=%d (number>>%d&1)\n", i, context, bit, i); + size += range_coder_code(coder, context, bit); + } + + tracef("ENCODE_NUMBER: COMPLETE size=%d\n", size); + + return size; +} + +static int encode_reference(shr_rangecoder_t *coder, int offset, int length, shr_lzstate_t *state) { + int parity = state->parity & 1; + int size = 0; + + // Trace reference encoding + tracef("=== ENCODE_REFERENCE START ===\n"); + tracef("LZ: ENCODE_REFERENCE offset=%d length=%d parity=%d prev_was_ref=%d last_offset=%d\n", + offset, length, parity, state->prev_was_ref, state->last_offsets[0]); + tracef("KIND_REF: context=%d bit=1\n", 1 + CONTEXT_KIND + (parity << 8)); + + range_coder_code(coder, 1 + CONTEXT_KIND + (parity << 8), 1); // KIND_REF + size++; + + if (!state->prev_was_ref) { + int repeated = offset == state->last_offsets[0]; + tracef("REPEATED: context=%d bit=%d (offset %s last_offset)\n", + 1 + CONTEXT_REPEATED, repeated, repeated ? "==" : "!="); + range_coder_code(coder, 1 + CONTEXT_REPEATED, repeated); + size++; + } + + if (offset != state->last_offsets[0]) { + tracef("ENCODE_OFFSET: offset=%d (encoded as %d)\n", offset, offset + 2); + size += encode_number(coder, CONTEXT_GROUP_OFFSET, offset + 2); + } else { + tracef("SKIP_OFFSET: offset=%d same as last_offset\n", offset); + } + + tracef("ENCODE_LENGTH: length=%d\n", length); + size += encode_number(coder, CONTEXT_GROUP_LENGTH, length); + + // Update state + state->after_first = 1; + state->prev_was_ref = 1; + state->parity = state->parity + length; + // Update LRU offsets: move offset to front + if (offset != state->last_offsets[0]) { + // remove offset if present in [1] or [2] + if (offset == state->last_offsets[1]) { + state->last_offsets[1] = state->last_offsets[0]; + state->last_offsets[0] = (uint16_t)offset; + } else { + state->last_offsets[2] = state->last_offsets[1]; + state->last_offsets[1] = state->last_offsets[0]; + state->last_offsets[0] = (uint16_t)offset; + } + } + assert(offset == state->last_offsets[0] && "reference offset too large"); + + tracef("=== ENCODE_REFERENCE END ===\n"); + tracef("STATE UPDATE: after_first=%d prev_was_ref=%d parity=%d last_offset=%d\n", + state->after_first, state->prev_was_ref, state->parity, state->last_offsets[0]); + tracef("TOTAL SIZE: %d\n\n", size); + + return size; +} + +// Intelligent Match Finder using enhanced hash table +static int find_match(shr_work_buffer_t *mem, const unsigned char *data, int data_size, int pos, + int *best_offset, int *best_length) { + *best_length = 0; + *best_offset = 0; + + if (pos + 2 >= data_size) return 0; + + int max_len = min(MAX_MATCH_LENGTH, data_size - pos); + int window_limit = mem->window_size - 1; + int max_offset = min(pos, min(MAX_OFFSET, window_limit)); + + // Probe recent offsets (cheap LRU of 2) + for (int pi = 1; pi <= 2; ++pi) { + int poff = mem->state.last_offsets[pi]; + if (poff <= 0 || poff > max_offset || poff == mem->state.last_offsets[0]) continue; + int absolute_candidate_pos = pos - poff; + if (absolute_candidate_pos < 0) continue; + // Quick 2-byte check + if (absolute_candidate_pos + 2 <= data_size && pos + 2 <= data_size) { + if (data[pos] != data[absolute_candidate_pos] || data[pos+1] != data[absolute_candidate_pos+1]) continue; + } + int match_len = 0; + while (match_len < max_len && + pos + match_len < data_size && + absolute_candidate_pos + match_len < data_size && + data[pos + match_len] == data[absolute_candidate_pos + match_len]) { + match_len++; + } + if (match_len >= MIN_MATCH_LENGTH) { + int repeated_needed = mem->state.prev_was_ref ? 0 : 1; + int offset_changed = (poff != mem->state.last_offsets[0]); + int base_offset_penalty = estimate_number_cost_int(poff + 2) >> 2; + int ref_cost = 1 + repeated_needed + base_offset_penalty; + if (offset_changed) ref_cost += estimate_number_cost_int(poff + 2); + ref_cost += estimate_number_cost_int(match_len); + int lit_cost = mem->coder.lit_avg_events ? mem->coder.lit_avg_events : 9; + int net_cost = ref_cost - match_len * lit_cost; + if (*best_length == 0 || net_cost < 0 || (net_cost == 0 && match_len > *best_length)) { + *best_length = match_len; + *best_offset = poff; + } + } + } + + // Use hash table to find potential matches + unsigned int hash = hash3(&data[pos]) & (unsigned int)mem->hash_mask; + + int best_quality = 0; + + // Iterate all ways in the bucket in MRU order: last inserted first + int start = mem->repl_index[hash]; + for (int k = 0; k < mem->ways; k++) { + int w = (start - 1 - k); + if (w < 0) w += mem->ways * ((-w) / mem->ways + 1); + w %= mem->ways; + uint16_t wrapped_pos = mem->buckets[hash * mem->ways + w]; + if (wrapped_pos == 0xFFFF) continue; // empty slot + + // Reconstruct absolute candidate position within the current window span + int absolute_candidate_pos = (pos & ~mem->window_mask) | wrapped_pos; + if (absolute_candidate_pos > pos) absolute_candidate_pos -= mem->window_size; + + int offset = pos - absolute_candidate_pos; + if (offset <= 0 || offset > max_offset) continue; + + // Quick pre-check for minimum length + if (absolute_candidate_pos + MIN_MATCH_LENGTH > data_size) continue; + int valid = 1; + for (int i = 0; i < MIN_MATCH_LENGTH; i++) { + if (pos + i >= data_size || data[pos + i] != data[absolute_candidate_pos + i]) { + valid = 0; + break; + } + } + if (!valid) continue; + + // Extend match + int match_len = MIN_MATCH_LENGTH; + while (match_len < max_len && + pos + match_len < data_size && + absolute_candidate_pos + match_len < data_size && + data[pos + match_len] == data[absolute_candidate_pos + match_len]) { + match_len++; + } + + // Dynamic minimal length requirement for far matches + int min_len_req = MIN_MATCH_LENGTH; + if (offset > 1024) min_len_req++; + if (offset > 4096) min_len_req += 2; + + if (match_len >= min_len_req) { + // Estimate reference cost (same units as before) + int repeated_needed = mem->state.prev_was_ref ? 0 : 1; + int offset_changed = (offset != mem->state.last_offsets[0]); + int base_offset_penalty = estimate_number_cost_int(offset + 2) >> 2; // ~25% + int ref_cost = 1; // KIND_REF + ref_cost += repeated_needed; + ref_cost += base_offset_penalty; + if (offset_changed) ref_cost += estimate_number_cost_int(offset + 2); + ref_cost += estimate_number_cost_int(match_len); + + // Credit saved literal cost using EWMA of literal size + int lit_cost = mem->coder.lit_avg_events ? mem->coder.lit_avg_events : 9; + int saved = match_len * lit_cost; + int net_cost = ref_cost - saved; + + int is_better = 0; + if (*best_length == 0) { + is_better = 1; + } else { + // Prefer lower net cost; break ties with longer length, then closer offset + if (best_quality == 0 || net_cost < best_quality) { + is_better = 1; + } else if (net_cost == best_quality) { + if (match_len > *best_length) { + is_better = 1; + } else if (match_len == *best_length && offset < *best_offset) { + is_better = 1; + } + } + } + if (is_better) { + *best_length = match_len; + *best_offset = offset; + best_quality = net_cost; // store best net cost here + } + } + } + + return *best_length >= MIN_MATCH_LENGTH; +} + +// Main compression function +static int compress_data(const unsigned char *input, int input_size, + unsigned char *output, int output_capacity, + size_t work_memory_size) { + int pos = 0; + + // Derive match-finder sizes from provided work memory (single malloc arena) + // We use a set-associative table with "ways" entries per bucket and + // a round-robin replacement index per bucket (1 byte). + int ways = (work_memory_size >= 4096) ? 4 : 2; // increase associativity when memory allows + if (work_memory_size <= sizeof(shr_work_buffer_t)) { + return -4; // Not enough memory even for control structure + } + + size_t available = work_memory_size - sizeof(shr_work_buffer_t); + + // Compute the maximum number of buckets we can afford in the available arena: + // each bucket costs (ways * 2 bytes) + 1 byte for replacement index + size_t cost_per_bucket = (size_t)(ways * 2 + 1); + size_t max_buckets = available / cost_per_bucket; + if (max_buckets == 0) { + return -4; // Not enough memory for even a single bucket + } + + // Choose hash_size as largest power-of-two <= max_buckets for fast masking + int hash_size = 1; + while (((size_t)hash_size << 1) <= max_buckets) hash_size <<= 1; + + // Determine window size based on total storable entries (hash_size * ways) + // Pick a power-of-two window_size <= min(65536, 2 * hash_size * ways) with a minimum of 256 + size_t target_entries = (size_t)hash_size * (size_t)ways; + size_t target_window = target_entries * 2; // tighter coupling with actual indexable entries + if (target_window < 256) target_window = 256; + if (target_window > 65536) target_window = 65536; + // Round down to power of two + size_t window_size = 1; + while ((window_size << 1) <= target_window) window_size <<= 1; + int window_bits = 0; + size_t tmp_ws = window_size; + while ((tmp_ws >>= 1) != 0) window_bits++; + int window_mask = (int)(window_size - 1); + + // Compute actual arena size and allocate + size_t buckets_bytes = (size_t)hash_size * (size_t)ways * sizeof(uint16_t); + size_t repl_bytes = (size_t)hash_size * sizeof(uint8_t); + + // Guard in case rounding left us slightly over the available arena; adjust down + if (buckets_bytes + repl_bytes > available) { + size_t usable_buckets = available / cost_per_bucket; + if (usable_buckets == 0) return -4; + // round down to power of two + hash_size = 1; + while (((size_t)hash_size << 1) <= usable_buckets) hash_size <<= 1; + buckets_bytes = (size_t)hash_size * (size_t)ways * sizeof(uint16_t); + repl_bytes = (size_t)hash_size * sizeof(uint8_t); + } + + size_t total_size = work_memory_size; // allocate exactly what caller provides + + shr_work_buffer_t *mem = scratch_malloc(total_size); + if (!mem) { + fprintf(stderr, "Error: Memory allocation failed\n"); + return -4; // Memory allocation failed + } + // Zero the entire arena for deterministic behavior + memset(mem, 0, work_memory_size); + uint8_t *arena = (uint8_t *)(mem + 1); + mem->buckets = (uint16_t *)arena; + mem->repl_index = (uint8_t *)(arena + buckets_bytes); + + // Initialize match finder configuration + mem->hash_size = hash_size; + mem->ways = ways; + mem->hash_mask = hash_size - 1; + mem->window_bits = window_bits; + mem->window_size = (int)window_size; + mem->window_mask = window_mask; + + // Initialize tables: 0xFFFF means empty slot + for (int i = 0; i < mem->hash_size * mem->ways; i++) mem->buckets[i] = 0xFFFF; + memset(mem->repl_index, 0, (size_t)mem->hash_size); + + // Initialize + range_coder_init(&mem->coder, output, output_capacity); + lz_state_init(&mem->state); + + // Enable tracing + tracef("=== MINISHRINKLER TRACE ===\n"); + tracef("Input size: %d bytes\n", input_size); + tracef("Input data: "); + for (int i = 0; i < min(input_size, 32); i++) { + tracef("%02x ", input[i]); + } + if (input_size > 32) tracef("..."); + tracef("\n\n"); + + // Compression with lazy matching + while (pos < input_size) { + // Update hash table + update_hash(mem, input, pos, input_size); + + int best_offset, best_length; + + if (find_match(mem, input, input_size, pos, &best_offset, &best_length)) { + // Lazy matching: look ahead for better matches + if (best_length >= 4 && pos + 1 < input_size) { + int next_offset, next_length; + + // Update hash for next position + update_hash(mem, input, pos + 1, input_size); + + if (find_match(mem, input, input_size, pos + 1, &next_offset, &next_length)) { + // Compare current match vs next position match + int current_cost = 2 + (best_length >= 8 ? 1 : 0) + (best_offset >= 256 ? 1 : 0); + int next_cost = 2 + (next_length >= 8 ? 1 : 0) + (next_offset >= 256 ? 1 : 0); + + // If next match is significantly better, encode literal and continue + if (next_length > best_length + 1 || + (next_length == best_length + 1 && next_cost <= current_cost)) { + tracef("POS %d: LAZY LITERAL 0x%02x (waiting for better match)\n", pos, input[pos]); + encode_literal(&mem->coder, input[pos], &mem->state); + pos++; + continue; + } + } + } + + // Encode reference + tracef("POS %d: MATCH offset=%d length=%d\n", pos, best_offset, best_length); + encode_reference(&mem->coder, best_offset, best_length, &mem->state); + pos += best_length; + } else { + // Encode literal + tracef("POS %d: LITERAL 0x%02x (%c)\n", pos, input[pos], + (input[pos] >= 32 && input[pos] <= 126) ? input[pos] : '.'); + encode_literal(&mem->coder, input[pos], &mem->state); + pos++; + } + } + + // Encode end marker (offset 0) + tracef("END: ENCODE_END_MARKER\n"); + int parity = mem->state.parity & 1; + range_coder_code(&mem->coder, 1 + CONTEXT_KIND + (parity << 8), 1); // KIND_REF + range_coder_code(&mem->coder, 1 + CONTEXT_REPEATED, 0); // Not repeated + encode_number(&mem->coder, CONTEXT_GROUP_OFFSET, 2); // Offset 0 + 2 = 2 = end + + // Finalize + range_coder_finish(&mem->coder); + + tracef("=== COMPRESSION COMPLETE ===\n"); + tracef("Final output size: %d bytes\n", mem->coder.output_size); + + // Save output size before freeing memory + int output_size = mem->coder.output_size; + + // Free embedded memory + scratch_free(mem); + + return output_size; +} + +/** + * @brief Get the maximum compressed size for given input size + */ +size_t minishrinkler_get_max_compressed_size(size_t input_size) { + // Worst case: no compression + range coder overhead + // Each byte might need up to 9 bits in worst case + return (input_size * 9 + 7) / 8 + 64; // Add some safety margin +} + +/** + * @brief Compress data from input buffer to output buffer + */ +int minishrinkler_compress( + const uint8_t *input_data, + size_t input_size, + uint8_t *output_buffer, + size_t output_capacity, + size_t work_memory_size +) { + // Validate input parameters + if (!input_data || !output_buffer || input_size == 0 || output_capacity == 0) { + return -2; // Invalid parameters + } + + // Check if output buffer is large enough + size_t max_compressed_size = minishrinkler_get_max_compressed_size(input_size); + if (output_capacity < max_compressed_size) { + return -1; // Output buffer too small + } + + // Check input size limit + if (input_size > MAX_FILE_SIZE) { + return -3; // Input too large + } + + // Call the original compression function + int result = compress_data((const unsigned char*)input_data, (int)input_size, + (unsigned char*)output_buffer, (int)output_capacity, + work_memory_size); + + return result; +} diff --git a/src/compress/minishrinkler_internal.h b/src/compress/minishrinkler_internal.h new file mode 100644 index 000000000..646ea72b6 --- /dev/null +++ b/src/compress/minishrinkler_internal.h @@ -0,0 +1,101 @@ +/** + * @file minishrinkler_internal.h + * @author Giovanni Bajo + * @brief Minishrinkler compression API - Simplified Shrinkler compressor + * + * This header provides a simple buffer-to-buffer compression API for the + * Minishrinkler compressor, a simplified version of the Shrinkler algorithm. + * + * Features: + * * A single dynamic memory allocations for all the work memory + * * Compatible with official Shrinkler decompressor (same bitstream) + * * LZ77 compression with hash-based match finding + * * Range coder with context modeling + * * Generates raw compressed data (no header) + * + * Usage: + * @code + * #include "minishrinkler_internal.h" + * + * uint8_t input_buffer[1024]; + * uint8_t output_buffer[2048]; // Should be larger than input + * + * int compressed_size = minishrinkler_compress( + * input_buffer, 1024, // input data and size + * output_buffer, 2048, // output buffer and capacity + * 4608 // work memory size for hash table + * ); + * + * if (compressed_size > 0) { + * // Success: compressed_size contains the actual compressed size + * } else { + * // Error: output buffer too small or other error + * } + * @endcode + */ + +#ifndef MINISHRINKLER_H +#define MINISHRINKLER_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Compress data from input buffer to output buffer + * + * This function compresses the input data using the Minishrinkler algorithm + * and writes the compressed data to the output buffer. The compressed data + * is compatible with the official Shrinkler decompressor. + * + * @param input_data Pointer to the input data to compress + * @param input_size Size of the input data in bytes + * @param output_buffer Pointer to the output buffer for compressed data + * @param output_capacity Maximum capacity of the output buffer in bytes + * @param work_memory_size Size of work memory to allocate for hash table in bytes + * + * @return On success: number of bytes written to output_buffer (compressed size) + * @return On failure: negative value indicating error + * -1: output buffer too small + * -2: invalid input parameters + * -3: compression failed + * + * @note The output buffer should be at least as large as the input data + * to handle worst-case scenarios where compression doesn't help. + * For typical data, the compressed size will be smaller than input. + * + * @note The compressed output is raw data without any header, compatible + * with the official Shrinkler decompressor using the -d option. + * + * @note This function does not perform any file I/O operations. + */ +int minishrinkler_compress( + const uint8_t *input_data, + size_t input_size, + uint8_t *output_buffer, + size_t output_capacity, + size_t work_memory_size +); + +/** + * @brief Get the maximum compressed size for given input size + * + * This function returns the maximum possible compressed size for a given + * input size. This is useful for allocating output buffers. + * + * @param input_size Size of the input data in bytes + * @return Maximum compressed size in bytes + * + * @note The actual compressed size will typically be much smaller. + * This is just the worst-case scenario. + */ +size_t minishrinkler_get_max_compressed_size(size_t input_size); + +#ifdef __cplusplus +} +#endif + +#endif /* MINISHRINKLER_H */ diff --git a/src/compress/ringbuf.c b/src/compress/ringbuf.c index 0fed680b0..dc6493738 100644 --- a/src/compress/ringbuf.c +++ b/src/compress/ringbuf.c @@ -5,6 +5,14 @@ #include "ringbuf_internal.h" #include "../utils.h" #include +#include +#include + +#ifdef N64 +#include "n64sys.h" +#include "dma.h" +#include "dragonfs.h" +#endif void __ringbuf_init(decompress_ringbuf_t *ringbuf, uint8_t *buf, int size) { @@ -71,3 +79,68 @@ void __ringbuf_copy(decompress_ringbuf_t *ringbuf, int copy_offset, uint8_t *dst ringbuf->ringbuf_pos &= ringbuf->ringbuf_size - 1; } } + +// ---------------------------------------------------------------------------- +// Lookahead (double-buffer) reader +// ---------------------------------------------------------------------------- + +__attribute__((noinline)) +void __lookahead_buf_refill(__lookahead_buf_t *lb) +{ + int buf_size; + + lb->cur ^= 1; + + #ifdef N64 + if (lb->rom_base) { + dma_wait_finished(lb->ticket[lb->cur]); + int next = lb->cur ^ 1; + data_cache_hit_invalidate(lb->buf[next], sizeof(lb->buf[0])); + lb->ticket[next] = dma_read_raw_async(lb->buf[next], lb->rom_addr, sizeof(lb->buf[0])); + lb->rom_addr += sizeof(lb->buf[0]); + buf_size = sizeof(lb->buf[0]); + } else { + buf_size = read(lb->fd, lb->buf[lb->cur], sizeof(lb->buf[0])); + } + #else + buf_size = read(lb->fd, lb->buf[lb->cur], sizeof(lb->buf[0])); + #endif + + lb->ptr = lb->buf[lb->cur]; + lb->end = lb->buf[lb->cur] + buf_size; + if (!buf_size) lb->eof = true; +} + +void __lookahead_buf_init(__lookahead_buf_t *lb, int fd) +{ + memset(lb, 0, sizeof(*lb)); + lb->fd = fd; + + #ifdef N64 + pi_addr_t rom_base = 0; + if (ioctl(fd, IODFS_GET_ROM_BASE, &rom_base) >= 0) { + lb->rom_base = rom_base; + } + #endif + + __lookahead_buf_reset(lb); +} + +void __lookahead_buf_reset(__lookahead_buf_t *lb) +{ + lb->eof = false; + lb->cur = 0; + lb->ptr = lb->end = lb->buf[0]; + lb->rom_addr = lb->rom_base ? (lb->rom_base + (uint32_t)lseek(lb->fd, 0, SEEK_CUR)) : 0; + + #ifdef N64 + if (lb->rom_base) { + // Prefetch the first buffer (the logic expects the first refill() to flip + // cur and start prefetching the other buffer). + int next = lb->cur ^ 1; + data_cache_hit_invalidate(lb->buf[next], sizeof(lb->buf[0])); + lb->ticket[next] = dma_read_raw_async(lb->buf[next], lb->rom_addr, sizeof(lb->buf[0])); + lb->rom_addr += sizeof(lb->buf[0]); + } + #endif +} diff --git a/src/compress/ringbuf_internal.h b/src/compress/ringbuf_internal.h index 00304d5b2..d2b11c4db 100644 --- a/src/compress/ringbuf_internal.h +++ b/src/compress/ringbuf_internal.h @@ -6,6 +6,9 @@ #define LIBDRAGON_COMPRESS_RINGBUF_INTERNAL_H #include +#include +#include "../utils.h" +#include "n64types.h" /** * @brief A ring buffer used for streaming decompression. @@ -66,4 +69,39 @@ void __ringbuf_write(decompress_ringbuf_t *ringbuf, uint8_t *src, int count); */ void __ringbuf_copy(decompress_ringbuf_t *ringbuf, int copy_offset, uint8_t *dst, int count); +/** + * @brief A double-buffered lookahead reader used to overlap I/O with decoding. + * + * It keeps two fixed-size buffers and, on N64 when reading from ROM (rom_base != 0), + * it prefetches the "next" buffer via DMA while the caller is consuming the current one. + */ +typedef struct { + uint8_t buf[2][128] __attribute__((aligned(16))); ///< Double buffer storage + uint8_t cur; ///< Current buffer index (0/1) + uint8_t *ptr; ///< Read pointer in current buffer + uint8_t *end; ///< End pointer in current buffer + int fd; ///< File descriptor (used when rom_base == 0) + pi_addr_t rom_base; ///< Base ROM address (0 if not reading from ROM) + pi_addr_t rom_addr; ///< Current ROM address (advanced as we prefetch) + uint64_t ticket[2]; ///< DMA ticket for each prefetch buffer (ROM mode only) + bool eof; ///< True if EOF reached (fd mode only) +} __lookahead_buf_t; + +/** @brief Initialize a lookahead buffer */ +void __lookahead_buf_init(__lookahead_buf_t *lb, int fd); + +/** @brief Reset lookahead buffer (after caller rewound underlying fd/ROM pointer) */ +void __lookahead_buf_reset(__lookahead_buf_t *lb); + +/** @brief Refill lookahead buffer when needed */ +void __lookahead_buf_refill(__lookahead_buf_t *lb); + +/** @brief Read one byte from lookahead buffer, refilling when needed */ +static inline uint8_t __lookahead_buf_readbyte(__lookahead_buf_t *lb) +{ + if (UNLIKELY(lb->ptr >= lb->end)) + __lookahead_buf_refill(lb); + return *lb->ptr++; +} + #endif diff --git a/src/compress/shrinkler_dec.c b/src/compress/shrinkler_dec.c index 7fcfad3de..4b6f9b2ef 100644 --- a/src/compress/shrinkler_dec.c +++ b/src/compress/shrinkler_dec.c @@ -8,11 +8,12 @@ #include #include #include -#ifdef N64 -#include "debug.h" -#else +#include -#endif +#include "shrinkler_dec_internal.h" +#include "ringbuf_internal.h" +#include "../utils.h" +#include "debug.h" /// @cond #if defined(__GNUC__) || defined(__clang__) @@ -44,164 +45,250 @@ #define CONTEXT_GROUP_OFFSET 2 ///< Context group for offsets #define CONTEXT_GROUP_LENGTH 3 ///< Context group for lengths -/** @brief Decompressor state (for assembly) */ -typedef struct { - uint16_t contexts[NUM_CONTEXTS]; ///< Probability contexts -} shrinkler_asm_state_t; +#ifdef N64 +/** @brief Full decompression of a Shrinkler stream into a buffer */ +int decompress_shrinkler_full_inplace(const uint8_t* in, size_t cmp_size, uint8_t *out, size_t size) +{ + extern int decompress_shrinkler_full_fast(const uint8_t* in, int insize, uint8_t *out); + return decompress_shrinkler_full_fast(in, cmp_size, out); +} +#endif + +// ---------------------------------------------------------------------------- +// Streaming (asset_fopen) implementation +// ---------------------------------------------------------------------------- -/** @brief Decompressor state (for C) */ +/** @brief Shrinkler streaming decompressor state */ typedef struct { - uint16_t contexts[NUM_CONTEXTS]; ///< Probability contexts - unsigned intervalsize; ///< Current interval size - uint64_t intervalvalue; ///< Current interval value - - uint8_t *src; ///< Pointer to the input data - int bits_left; ///< Number of bits left in the interval -} shrinkler_ctx_t; - -static void shr_decode_init(shrinkler_ctx_t *ctx, uint8_t *src) { - for (int i=0; icontexts[i] = 0x8000; - - ctx->intervalsize = 1; - ctx->intervalvalue = 0; - ctx->src = src; - ctx->bits_left = 0; - - // Adjust for 64-bit values - for (int i=0;i<4;i++) - ctx->intervalvalue = (ctx->intervalvalue << 8) | *ctx->src++; - ctx->intervalvalue <<= 31; - ctx->bits_left = 1; - ctx->intervalsize = 0x8000; + uint8_t buf[128] __attribute__((aligned(8))); ///< File buffer + int fd; ///< File descriptor to read from + int buf_idx; ///< Current index in the file buffer + int buf_size; ///< Size of the file buffer + bool file_eof; ///< True if we reached EOF on the compressed stream + + // Arithmetic decoder state + uint16_t contexts[NUM_CONTEXTS]; ///< Probability contexts + uint32_t intervalsize; ///< Current interval size + uint64_t intervalvalue; ///< Current interval value + int bits_left; ///< Bits left until next 32-bit refill + bool decoder_inited; ///< True after reading the initial 32 bits + + // LZ/high-level decoder state + bool stream_eof; ///< True if end-of-stream token has been decoded + bool ref; ///< Next symbol is a match if true, literal if false + bool prev_was_ref; ///< Previous symbol was a match + int match_off; ///< Current match offset (for repeated matches) + int match_len; ///< Remaining match length to copy + uint32_t out_pos; ///< Bytes produced so far (for parity) + + decompress_ringbuf_t ringbuf; ///< Ring buffer (window) +} shrinkler_dec_state_t; + +_Static_assert(sizeof(shrinkler_dec_state_t) <= DECOMPRESS_SHRINKLER_STATE_SIZE, + "Shrinkler streaming decompressor state too small"); + +static void shr_refill(shrinkler_dec_state_t *s) +{ + s->buf_size = read(s->fd, s->buf, sizeof(s->buf)); + s->buf_idx = 0; + s->file_eof = (s->buf_size == 0); +} + +static inline uint8_t shr_readbyte(shrinkler_dec_state_t *s) +{ + if (unlikely(s->buf_idx >= s->buf_size)) { + shr_refill(s); + /* + * Shrinkler streams are typically padded and the arithmetic decoder + * is allowed to read past the end of the compressed input. + * When reading from a file descriptor, we emulate this by returning + * zero bytes once EOF is reached, instead of terminating early. + */ + if (unlikely(s->file_eof)) return 0; + } + return s->buf[s->buf_idx++]; +} + +static inline uint32_t shr_read_u32be(shrinkler_dec_state_t *s) +{ + uint32_t v = 0; + v = (v << 8) | shr_readbyte(s); + v = (v << 8) | shr_readbyte(s); + v = (v << 8) | shr_readbyte(s); + v = (v << 8) | shr_readbyte(s); + return v; } -static inline int shr_decode_bit(shrinkler_ctx_t *ctx, int context_index) { - while ((ctx->intervalsize < 0x8000)) { - if (unlikely(ctx->bits_left == 0)) { - ctx->intervalvalue |= read32be(ctx->src); - ctx->src += 4; - ctx->bits_left = 32; +static void shr_stream_decode_init(shrinkler_dec_state_t *s) +{ + for (int i = 0; i < NUM_CONTEXTS; i++) + s->contexts[i] = 0x8000; + + s->intervalsize = 1; + s->intervalvalue = 0; + s->bits_left = 0; + + // Load initial 32 bits and position them like the reference decoder + uint32_t w = shr_read_u32be(s); + s->intervalvalue = ((uint64_t)w) << 31; + s->bits_left = 1; + s->intervalsize = 0x8000; + s->decoder_inited = true; +} + +static inline int shr_stream_decode_bit(shrinkler_dec_state_t *s, int context_index) +{ + while (s->intervalsize < 0x8000) { + if (unlikely(s->bits_left == 0)) { + s->intervalvalue |= (uint64_t)shr_read_u32be(s); + s->bits_left = 32; } - ctx->bits_left -= 1; - ctx->intervalsize <<= 1; - ctx->intervalvalue <<= 1; + s->bits_left -= 1; + s->intervalsize <<= 1; + s->intervalvalue <<= 1; } - unsigned prob = ctx->contexts[context_index]; - unsigned intervalvalue = ctx->intervalvalue >> 48; - unsigned threshold = (ctx->intervalsize * prob) >> 16; + unsigned prob = s->contexts[context_index]; + unsigned intervalvalue = (unsigned)(s->intervalvalue >> 48); + unsigned threshold = (s->intervalsize * prob) >> 16; if (intervalvalue >= threshold) { // Zero - ctx->intervalvalue -= (uint64_t)threshold << 48; - ctx->intervalsize -= threshold; - ctx->contexts[context_index] = prob - (prob >> ADJUST_SHIFT); + s->intervalvalue -= (uint64_t)threshold << 48; + s->intervalsize -= threshold; + s->contexts[context_index] = prob - (prob >> ADJUST_SHIFT); return 0; } else { // One - ctx->intervalsize = threshold; - ctx->contexts[context_index] = prob + (0xffff >> ADJUST_SHIFT) - (prob >> ADJUST_SHIFT); + s->intervalsize = threshold; + s->contexts[context_index] = prob + (0xffff >> ADJUST_SHIFT) - (prob >> ADJUST_SHIFT); return 1; } } -static inline int shr_decode_number(shrinkler_ctx_t *ctx, int base_context) { +static inline int shr_stream_decode_number(shrinkler_dec_state_t *s, int base_context) +{ int context; int i; for (i = 0 ;; i++) { context = base_context + (i * 2 + 2); - if (shr_decode_bit(ctx, context) == 0) break; + if (shr_stream_decode_bit(s, context) == 0) break; } int number = 1; for (; i >= 0 ; i--) { context = base_context + (i * 2 + 1); - int bit = shr_decode_bit(ctx, context); + int bit = shr_stream_decode_bit(s, context); number = (number << 1) | bit; } - return number; } -static inline int lzDecode(shrinkler_ctx_t *ctx, int context) { - return shr_decode_bit(ctx, NUM_SINGLE_CONTEXTS + context); +static inline int shr_lzDecode(shrinkler_dec_state_t *s, int context) +{ + return shr_stream_decode_bit(s, NUM_SINGLE_CONTEXTS + context); } -static inline int lzDecodeNumber(shrinkler_ctx_t *ctx, int context_group) { - return shr_decode_number(ctx, NUM_SINGLE_CONTEXTS + (context_group << 8)); +static inline int shr_lzDecodeNumber(shrinkler_dec_state_t *s, int context_group) +{ + return shr_stream_decode_number(s, NUM_SINGLE_CONTEXTS + (context_group << 8)); } -static int shr_unpack(uint8_t *dst, uint8_t *src) +void decompress_shrinkler_init(void *state, int fd, int winsize) { - const int parity_mask = 1; + shrinkler_dec_state_t *s = (shrinkler_dec_state_t*)state; + memset(s, 0, sizeof(*s)); + s->fd = fd; + __ringbuf_init(&s->ringbuf, (uint8_t*)state + sizeof(shrinkler_dec_state_t), winsize); + decompress_shrinkler_reset(state); +} - uint8_t *dst_start = dst; - shrinkler_ctx_t ctx; - shr_decode_init(&ctx, src); +void decompress_shrinkler_reset(void *state) +{ + shrinkler_dec_state_t *s = (shrinkler_dec_state_t*)state; + s->buf_idx = 0; + s->buf_size = 0; + s->file_eof = false; - bool ref = false; - bool prev_was_ref = false; - int offset = 0; + s->decoder_inited = false; + s->stream_eof = false; + s->ref = false; + s->prev_was_ref = false; + s->match_off = 0; + s->match_len = 0; + s->out_pos = 0; + s->ringbuf.ringbuf_pos = 0; +} - while (1) { - if (ref) { - bool repeated = false; - if (!prev_was_ref) { - repeated = lzDecode(&ctx, CONTEXT_REPEATED); +ssize_t decompress_shrinkler_read(void *state, void *buf, size_t len) +{ + shrinkler_dec_state_t *s = (shrinkler_dec_state_t*)state; + uint8_t *out = (uint8_t*)buf; + uint8_t *out_orig = out; + + if (len == 0 || s->stream_eof) return 0; + + if (unlikely(!s->decoder_inited)) { + // Lazy-init: read the initial 32-bit seed only when the stream is first consumed + if (unlikely(s->buf_idx >= s->buf_size)) + shr_refill(s); + if (unlikely(s->file_eof)) { s->stream_eof = true; return 0; } + shr_stream_decode_init(s); + } + + while (!s->stream_eof && len > 0) { + // If we're in the middle of copying a match, continue copying bytes first. + if (s->match_len > 0) { + int n = MIN((size_t)s->match_len, len); + __ringbuf_copy(&s->ringbuf, s->match_off, out, n); + out += n; + len -= n; + s->match_len -= n; + s->out_pos += n; + if (s->match_len > 0) + break; // output buffer full + + // Match finished: decode the next kind bit + int parity = s->out_pos & 1; + s->ref = shr_lzDecode(s, CONTEXT_KIND + (parity << 8)); + continue; + } + + if (!s->ref) { + // Literal + int parity = s->out_pos & 1; + int context = 1; + for (int i = 7; i >= 0; i--) { + int bit = shr_lzDecode(s, (parity << 8) | context); + context = (context << 1) | bit; } + uint8_t lit = (uint8_t)context; + *out++ = lit; + __ringbuf_writebyte(&s->ringbuf, lit); + len -= 1; + s->out_pos += 1; + s->prev_was_ref = false; + + parity = s->out_pos & 1; + s->ref = shr_lzDecode(s, CONTEXT_KIND + (parity << 8)); + } else { + // Match + bool repeated = false; + if (!s->prev_was_ref) + repeated = shr_lzDecode(s, CONTEXT_REPEATED); if (!repeated) { - offset = lzDecodeNumber(&ctx, CONTEXT_GROUP_OFFSET) - 2; - if (offset == 0) + s->match_off = shr_lzDecodeNumber(s, CONTEXT_GROUP_OFFSET) - 2; + if (s->match_off == 0) { + s->stream_eof = true; break; + } } - int length = lzDecodeNumber(&ctx, CONTEXT_GROUP_LENGTH); - prev_was_ref = true; - if (offset > 8) - while (length >= 8) { - memcpy(dst, dst - offset, 8); - dst += 8; - length -= 8; - } - while (length--) { - *dst = dst[-offset]; - dst++; - } - } else { - int parity = (dst - dst_start) & parity_mask; - int context = 1; - for (int i = 7 ; i >= 0 ; i--) { - int bit = lzDecode(&ctx, (parity << 8) | context); - context = (context << 1) | bit; - } - uint8_t lit = context; - *dst++ = lit; - prev_was_ref = false; + s->match_len = shr_lzDecodeNumber(s, CONTEXT_GROUP_LENGTH); + s->prev_was_ref = true; + + // Copy as much as we can in this call (loop continues to match_len branch) } - int parity = (dst - dst_start) & parity_mask; - ref = lzDecode(&ctx, CONTEXT_KIND + (parity << 8)); } - - return dst - dst_start; -} -void* decompress_shrinkler_full(const char *fn, int fd, size_t cmp_size, size_t size) -{ - void *in = malloc(cmp_size); - read(fd, in, cmp_size); - - void *out = malloc(size); - if (!out) return 0; - int dec_size = shr_unpack(out, in); (void)dec_size; - assertf(dec_size == size, "Shrinkler size:%d exp:%d", dec_size, size); - free(in); - return out; + return out - out_orig; } - -#ifdef N64 -int decompress_shrinkler_full_inplace(const uint8_t* in, size_t cmp_size, uint8_t *out, size_t size) -{ - extern int decompress_shrinkler_full_fast(const uint8_t* in, int insize, uint8_t *out); - return decompress_shrinkler_full_fast(in, cmp_size, out); -} -#endif diff --git a/src/compress/shrinkler_dec_fast.S b/src/compress/shrinkler_dec_fast.S index 23033e566..d7edf95be 100644 --- a/src/compress/shrinkler_dec_fast.S +++ b/src/compress/shrinkler_dec_fast.S @@ -128,9 +128,11 @@ decompress_shrinkler_full_fast: sll $t0, 9 xor parity, $t0 li prev_match, 1<<31 - blt match_off, match_len, .Lmatch1_loop_check # check if we can do 8-byte copy + blt match_off, 8, .Lmatch1_loop_check # if offset < 8, we can't do 8-byte copies sub $v0, outbuf, match_off # calculate start of match -.Lmatch8_loop: # 8-byte copy loop +.Lmatch8_loop: # 8-byte copy loop (safe for offset >= 8, + # even if match_len > offset: each chunk + # only reads bytes already written) ldl $t0, 0($v0) # load 8 bytes ldr $t0, 7($v0) addiu $v0, 8 diff --git a/src/compress/shrinkler_dec_internal.h b/src/compress/shrinkler_dec_internal.h index a20838593..d6c7c8a44 100644 --- a/src/compress/shrinkler_dec_internal.h +++ b/src/compress/shrinkler_dec_internal.h @@ -5,32 +5,27 @@ #ifndef LIBDRAGON_COMPRESS_SHRINKLER_DEC_INTERNAL_H #define LIBDRAGON_COMPRESS_SHRINKLER_DEC_INTERNAL_H -/** @brief Set to 0 to disable assembly implementation of the full decoder */ -#ifdef N64 -#define DECOMPRESS_SHRINKLER_FULL_USE_ASM 1 -#else -#define DECOMPRESS_SHRINKLER_FULL_USE_ASM 0 -#endif +// Keep this header self-contained (used both by libdragon and host tools) +#include +#include +#include +#include /** * @brief Size of the Shrinkler decompressor state structure, in bytes. */ -#define DECOMPRESS_SHRINKLER_STATE_SIZE 512 +#define DECOMPRESS_SHRINKLER_STATE_SIZE 2304 -#if DECOMPRESS_SHRINKLER_FULL_USE_ASM +/** @brief Initialize Shrinkler streaming decompressor */ +void decompress_shrinkler_init(void *state, int fd, int winsize); +/** @brief Read decompressed data from Shrinkler streaming decompressor */ +ssize_t decompress_shrinkler_read(void *state, void *buf, size_t len); +/** @brief Reset Shrinkler streaming decompressor state */ +void decompress_shrinkler_reset(void *state); + +#ifdef N64 int decompress_shrinkler_full_inplace(const uint8_t* in, size_t cmp_size, uint8_t *out, size_t size); -#else -/** - * @brief Decompress a full Shrinkler-compressed file into a buffer. - * - * @param fn Filename of the file to decompress - * @param fd File descriptor to read compressed data from. - * @param cmp_size Size of the compressed data. - * @param size Size of the decompressed data. - * @return Newly-allocated buffer with decompressed contents - */ -void* decompress_shrinkler_full(const char *fn, int fd, size_t cmp_size, size_t size); -#endif +#endif /* N64 */ #endif diff --git a/src/console.c b/src/console.c index d5ad4d76d..b4a5247b5 100644 --- a/src/console.c +++ b/src/console.c @@ -15,8 +15,11 @@ #include #include "system.h" #include "cop0.h" +#include "vi.h" +#include "display.h" #include "console.h" #include "graphics.h" +#include "debug.h" /* Prototypes */ static void __console_render(void); @@ -138,9 +141,10 @@ void console_init() { /* In case they initialized the display already */ display_close(); - display_init( RESOLUTION_640x240, DEPTH_16_BPP, 2, GAMMA_NONE, FILTERS_RESAMPLE ); + display_init( RESOLUTION_640x240, DEPTH_16_BPP, 2, GAMMA_NONE, FILTERS_DISABLED ); render_buffer = malloc(CONSOLE_SIZE); + assertf(render_buffer, "Out of memory"); console_set_render_mode(RENDER_AUTOMATIC); console_clear(); @@ -225,17 +229,14 @@ static void __console_render(void) } end:; + display_show(dc); + /* If the interrupts are disabled, the console wouldn't show to the screen. * Since the console is only used for development and emergency context, * it is better to force display irrespective of vblank. */ uint32_t c0_status = C0_STATUS(); if ((c0_status & C0_STATUS_IE) == 0 || ((c0_status & (C0_STATUS_EXL|C0_STATUS_ERL)) != 0)) - { - extern void display_show_force(display_context_t dc); - display_show_force(dc); - } - else - display_show(dc); + vi_wait_vblank(); } void console_render() diff --git a/src/coroutine.c b/src/coroutine.c new file mode 100644 index 000000000..1de1bbc08 --- /dev/null +++ b/src/coroutine.c @@ -0,0 +1,130 @@ +/** + * @file coroutine.c + * @author Max Bebök + * @author Giovanni Bajo + * @brief Cooperative coroutines implementation + * @ingroup lowlevel + */ + +#include "coroutine.h" +#include "utils.h" +#include "n64sys.h" +#include "debug.h" +#include +#include +#include +#include +#include + +/** @brief Coroutine */ +typedef struct coroutine_s { + ucontext_t ctx; ///< Context for the coroutine + void (*fn)(void *); ///< Function to execute in the coroutine + void *arg; ///< Argument to pass to the function + void *stack; ///< Stack for the coroutine + size_t stack_size; ///< Size of the stack + bool finished; ///< Whether the coroutine has finished + uint64_t wakeup_ticks; ///< Time to wake up the coroutine + struct coroutine_s *caller; ///< Caller of the coroutine +} coroutine_t; + +static coroutine_t *coro_current = NULL; +static ucontext_t sched_ctx; + +static void coro_entry(uintptr_t coro_ptr) +{ + coroutine_t *co = (coroutine_t *)coro_ptr; + co->fn(co->arg); + co->finished = true; + + // Yield back to the caller forever to avoid returning on a dead stack. + for (;;) { + coro_yield(); + } +} + +coroutine_t *coro_create(void (*fn)(void *), void *arg, size_t stack_size) +{ + if (!fn || stack_size == 0) return NULL; + + coroutine_t *co = (coroutine_t *)aligned_alloc(8, sizeof(*co)); + if (!co) return NULL; + + memset(co, 0, sizeof(*co)); + + co->stack = malloc(stack_size); + if (!co->stack) { + free(co); + return NULL; + } + + co->fn = fn; + co->arg = arg; + co->stack_size = stack_size; + + co->ctx.uc_stack.ss_sp = co->stack; + co->ctx.uc_stack.ss_size = stack_size; + co->ctx.uc_link = NULL; + makecontext(&co->ctx, (void (*) (void))coro_entry, 1, (uintptr_t)co); + return co; +} + +void coro_resume(coroutine_t *co) +{ + if (!co || co->finished) return; + + // sleep + if(get_ticks() < co->wakeup_ticks)return; + co->wakeup_ticks = 0; + + coroutine_t *prev = coro_current; + co->caller = prev; + coro_current = co; + + if (prev) { + swapcontext(&prev->ctx, &co->ctx); + } else { + swapcontext(&sched_ctx, &co->ctx); + } + + coro_current = prev; +} + +void coro_sleep(uint64_t ticks) +{ + assert(coro_current); + coro_current->wakeup_ticks = get_ticks() + ticks; + coro_yield(); +} + +void coro_yield(void) +{ + if (!coro_current) return; + + coroutine_t *cur = coro_current; + coroutine_t *caller = cur->caller; + coro_current = caller; + + if (caller) { + swapcontext(&cur->ctx, &caller->ctx); + } else { + swapcontext(&cur->ctx, &sched_ctx); + } +} + +void coro_destroy(coroutine_t *co) +{ + if (!co) return; + free(co->stack); + free(co); +} + +bool coro_finished(coroutine_t *co) +{ + return co->finished; +} + +coroutine_t* coro_get_current(void) +{ + return coro_current; +} diff --git a/src/dd/dd.c b/src/dd/dd.c new file mode 100644 index 000000000..de66c4494 --- /dev/null +++ b/src/dd/dd.c @@ -0,0 +1,199 @@ +/** + * @file dd.c + * @author Giovanni Bajo + * @author Christopher Bonhage + */ +#include +#include +#include +#include "dd.h" +#include "debug.h" +#include "interrupt.h" +#include "dma.h" +#include "n64sys.h" +#include "rtc_internal.h" + +#define DD_ASIC_STATUS_MECHA_IRQ_LINE (1<<9) ///< DD ASIC status: Mecha IRQ line +#define DD_ASIC_STATUS_BM_IRQ_LINE (1<<10) ///< DD ASIC status: BM IRQ line + +#define DD_ASIC_WCTRL_MECHA_IRQ_CLEAR (1<<8) ///< DD ASIC write control: Mecha IRQ clear + +/** @brief True if a 64DD drive is found. */ +bool dd_found = false; + +static volatile int mecha_irq_count = 0; +static volatile int bm_irq_count = 0; + +void dd_write(uint32_t address, uint16_t value) { + io_write(address, value << 16); +} + +uint16_t dd_read(uint32_t address) { + return io_read(address) >> 16; +} + +static void dd_handler(void) +{ + uint16_t status = dd_read(DD_ASIC_STATUS); + + if (status & DD_ASIC_STATUS_MECHA_IRQ_LINE) { + // Acknowledge interrupt and record it was generated + mecha_irq_count++; + dd_write(DD_ASIC_WCTRL, DD_ASIC_WCTRL_MECHA_IRQ_CLEAR); + } + + if (status & DD_ASIC_STATUS_BM_IRQ_LINE) { + // This interrupt is auto-acknowledged when we read the ASIC status, + // so just record it was generated + bm_irq_count++; + } +} + +uint16_t dd_command(dd_cmd_t cmd) { + int irq_count = mecha_irq_count; + dd_write(DD_ASIC_WCMD, cmd); + + while (mecha_irq_count == irq_count) {} + return dd_read(DD_ASIC_DATA); +} + +/** @brief Initialize the DD (global constructor run before main). */ +__attribute__((constructor)) +void dd_init(void) +{ + // iQue doesn't like PI accesses outside of ROM + if (sys_bbplayer()) return; + + uint32_t magic = 0x36344444; // "64DD" + dd_found = io_read(0x06000020) == magic; + if (!dd_found) return; + + // Install the cart interrupt handler immediately, because the DD will generate + // interrupts as soon as you interact with it, and we need to acknowledge + // them to avoid stalling the CPU. + set_CART_interrupt(1); + register_CART_handler(dd_handler); +} + +int dd_rtc_get_time( time_t *out ) +{ + if( !dd_found ) + { + return RTC_ENOCLOCK; + } + + // NOTE: the order of these commands is important, because reading MINSEC + // is what actually triggers the handshake with the RTC. The other two + // reads just fetch cached values. + uint16_t ms = dd_command(DD_CMD_RTC_GET_MINSEC); + uint16_t dh = dd_command(DD_CMD_RTC_GET_DAYHOUR); + uint16_t ym = dd_command(DD_CMD_RTC_GET_YEARMONTH); + + bool clock_error = ms >> 15; + uint8_t sec_enc = ms & 0xFF; + uint8_t min_enc = (ms >> 8) & 0x7F; + uint8_t hour_enc = dh & 0xFF; + uint8_t day_enc = dh >> 8; + uint8_t month_enc = ym & 0xFF; + uint8_t year_enc = ym >> 8; + + debugf("dd_rtc_get_time: raw ms:%04x dh:%04x ym:%04x\n", ms, dh, ym); + + if( clock_error ) + { + debugf("dd_rtc_get_time: clock error\n"); + return RTC_EBADCLOCK; + } + + int sec = bcd_decode( sec_enc ); + int min = bcd_decode( min_enc ); + int hour = bcd_decode( hour_enc ); + int day = bcd_decode( day_enc ); + int month = bcd_decode( month_enc ); + int year = bcd_decode( year_enc ); + + // Extremely basic sanity-check on the date and time + if( + month == 0 || month > 12 || + day == 0 || day > 31 || + hour >= 24 || min >= 60 || sec >= 60 + ) + { + debugf("dd_rtc_get_time: invalid date/time\n"); + return RTC_EBADTIME; + } + + struct tm rtc_time = (struct tm){ + .tm_sec = sec, + .tm_min = min, + .tm_hour = hour, + .tm_mday = day, + .tm_mon = month - 1, + // By convention, 2-digit year is interpreted as 20YY if it's 96 or later + .tm_year = year + (year >= 96 ? 0 : 100), + }; + + debugf("dd_rtc_get_time: parsed time: %04d-%02d-%02d %02d:%02d:%02d\n", + rtc_time.tm_year + 1900, rtc_time.tm_mon + 1, rtc_time.tm_mday, + rtc_time.tm_hour, rtc_time.tm_min, rtc_time.tm_sec); + + *out = mktime( &rtc_time ); + return RTC_ESUCCESS; +} + +int dd_rtc_set_time( time_t new_time ) +{ + if( !dd_found ) + { + return RTC_ENOCLOCK; + } + + if( new_time < DD_RTC_TIMESTAMP_MIN || new_time > DD_RTC_TIMESTAMP_MAX ) + { + debugf("dd_rtc_set_time: time out of range\n"); + return RTC_EBADTIME; + } + + struct tm * rtc_time = gmtime( &new_time ); + + debugf("dd_rtc_set_time: parsed time: %04d-%02d-%02d %02d:%02d:%02d\n", + rtc_time->tm_year + 1900, rtc_time->tm_mon + 1, rtc_time->tm_mday, + rtc_time->tm_hour, rtc_time->tm_min, rtc_time->tm_sec); + + uint8_t year = bcd_encode( rtc_time->tm_year % 100 ); + uint8_t month = bcd_encode( rtc_time->tm_mon + 1 ); + uint8_t day = bcd_encode( rtc_time->tm_mday ); + uint8_t hour = bcd_encode( rtc_time->tm_hour ); + uint8_t min = bcd_encode( rtc_time->tm_min ); + uint8_t sec = bcd_encode( rtc_time->tm_sec ); + + uint16_t ym_write = year << 8 | month; + uint16_t dh_write = day << 8 | hour; + uint16_t ms_write = min << 8 | sec; + + debugf("dd_rtc_set_time: raw ms:%04x dh:%04x ym:%04x\n", ms_write, dh_write, ym_write); + + dd_write( DD_ASIC_DATA, ym_write ); + uint16_t ym_read = dd_command( DD_CMD_RTC_SET_YEARMONTH ); + + dd_write( DD_ASIC_DATA, dh_write ); + uint16_t dh_read = dd_command( DD_CMD_RTC_SET_DAYHOUR ); + + dd_write( DD_ASIC_DATA, ms_write ); + uint16_t ms_read = dd_command( DD_CMD_RTC_SET_MINSEC ); + + debugf("dd_rtc_set_time: result ms:%04x dh:%04x ym:%04x\n", ms_read, dh_read, ym_read); + + bool clock_error = ms_read >> 15; + if( clock_error ) + { + return RTC_EBADCLOCK; + } + + if( ym_write != ym_read || dh_write != dh_read || ms_write != ms_read ) + { + return RTC_EBADTIME; + } + + return RTC_ESUCCESS; +} diff --git a/src/dd/libdragon.mk b/src/dd/libdragon.mk new file mode 100644 index 000000000..7faa7b41e --- /dev/null +++ b/src/dd/libdragon.mk @@ -0,0 +1,3 @@ + +LIBDRAGON_OBJS += \ + $(BUILD_DIR)/dd/dd.o diff --git a/src/debug.c b/src/debug.c index 64b767d50..9f82e24cd 100644 --- a/src/debug.c +++ b/src/debug.c @@ -10,10 +10,10 @@ #include #include #include -#include #include #include "console.h" #include "debug.h" +#include "emux.h" #include "regsinternal.h" #include "system.h" #include "n64types.h" @@ -25,9 +25,10 @@ #include "libcart/cart.h" #include "interrupt.h" #include "backtrace.h" +#include "kernel/kernel_internal.h" #include "exception_internal.h" +#include "fat.h" #include "fatfs/ff.h" -#include "fatfs/ffconf.h" #include "fatfs/diskio.h" /** @@ -71,7 +72,7 @@ static FILE *sdlog_file = NULL; /** @brief prefix used to address SD filesystem */ static char sdfs_prefix[16]; -static char sdfs_logic_drive[3] = { 0 }; +static int sdfs_vol_id = -1; /** @brief debug writer functions (USB, SD, IS64) */ static void (*debug_writer[3])(const uint8_t *buf, int size) = { 0 }; @@ -149,6 +150,11 @@ static void isviewer_write(const uint8_t *data, int len) } } +static void emux_write(const uint8_t *data, int len) +{ + emux_logn((const char*)data, len); +} + static void usblog_write(const uint8_t *data, int len) { usb_write(DATATYPE_TEXT, data, len); @@ -170,94 +176,10 @@ static void sdlog_write(const uint8_t *data, int len) /********************************************************************* * FAT backend *********************************************************************/ -/** @cond */ -static FATFS sd_fat; -#define FAT_VOLUME_SD 0 - -typedef struct -{ - DSTATUS (*disk_initialize)(void); - DSTATUS (*disk_status)(void); - DRESULT (*disk_read)(BYTE* buff, LBA_t sector, UINT count); - DRESULT (*disk_read_sdram)(BYTE* buff, LBA_t sector, UINT count); - DRESULT (*disk_write)(const BYTE* buff, LBA_t sector, UINT count); - DRESULT (*disk_ioctl)(BYTE cmd, void* buff); -} fat_disk_t; - -static fat_disk_t fat_disks[FF_VOLUMES] = {0}; - -DSTATUS disk_initialize(BYTE pdrv) -{ - if (fat_disks[pdrv].disk_initialize) - return fat_disks[pdrv].disk_initialize(); - return STA_NOINIT; -} +static int fat_disk_status_default(void) { return 0; } -DSTATUS disk_status(BYTE pdrv) -{ - if (fat_disks[pdrv].disk_status) - return fat_disks[pdrv].disk_status(); - return STA_NOINIT; -} - -DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count) -{ - _Static_assert(FF_MIN_SS == 512, "this function assumes sector size == 512"); - _Static_assert(FF_MAX_SS == 512, "this function assumes sector size == 512"); - if (fat_disks[pdrv].disk_read && PhysicalAddr(buff) < 0x00800000) - return fat_disks[pdrv].disk_read(buff, sector, count); - if (fat_disks[pdrv].disk_read_sdram && io_accessible(PhysicalAddr(buff))) - return fat_disks[pdrv].disk_read_sdram(buff, sector, count); - return RES_PARERR; -} - -DRESULT disk_write(BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count) -{ - _Static_assert(FF_MIN_SS == 512, "this function assumes sector size == 512"); - _Static_assert(FF_MAX_SS == 512, "this function assumes sector size == 512"); - if (fat_disks[pdrv].disk_write) - return fat_disks[pdrv].disk_write(buff, sector, count); - return RES_PARERR; -} - -DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void* buff) -{ - if (fat_disks[pdrv].disk_ioctl) - return fat_disks[pdrv].disk_ioctl(cmd, buff); - return RES_PARERR; -} - -DWORD get_fattime(void) -{ - time_t t = time(NULL); - if (t == -1) { - return (DWORD)( - (FF_NORTC_YEAR - 1980) << 25 | - FF_NORTC_MON << 21 | - FF_NORTC_MDAY << 16 - ); - } - struct tm tm = *localtime(&t); - return (DWORD)( - (tm.tm_year - 80) << 25 | - (tm.tm_mon + 1) << 21 | - tm.tm_mday << 16 | - tm.tm_hour << 11 | - tm.tm_min << 5 | - (tm.tm_sec >> 1) - ); -} - -/** @endcond */ - -/********************************************************************* - * Helpers - *********************************************************************/ - -static DSTATUS fat_disk_status_default(void) { return 0; } - -static DRESULT fat_disk_ioctl_default(BYTE cmd, void* buff) +static int fat_disk_ioctl_default(uint8_t cmd, void* buff) { switch (cmd) { @@ -266,241 +188,47 @@ static DRESULT fat_disk_ioctl_default(BYTE cmd, void* buff) } } -static DSTATUS fat_disk_initialize_sd(void) +static int fat_disk_initialize_sd(void) { return cart_card_init() ? STA_NOINIT : 0; } -static DRESULT fat_disk_read_sd(BYTE* buff, LBA_t sector, UINT count) -{ - return cart_card_rd_dram(buff, sector, count) ? RES_ERROR : RES_OK; -} - -static DRESULT fat_disk_read_sdram_sd(BYTE* buff, LBA_t sector, UINT count) -{ - return cart_card_rd_cart(PhysicalAddr(buff), sector, count) ? RES_ERROR : RES_OK; -} - -static DRESULT fat_disk_write_sd(const BYTE* buff, LBA_t sector, UINT count) -{ - return cart_card_wr_dram(buff, sector, count) ? RES_ERROR : RES_OK; -} - -static fat_disk_t fat_disk_sd = -{ - fat_disk_initialize_sd, - fat_disk_status_default, - fat_disk_read_sd, - fat_disk_read_sdram_sd, - fat_disk_write_sd, - fat_disk_ioctl_default -}; - -/********************************************************************* - * FAT newlib wrappers - *********************************************************************/ - -/** Maximum number of FAT files that can be concurrently opened */ -#define MAX_FAT_FILES 4 -static FIL fat_files[MAX_FAT_FILES] = {0}; -static DIR find_dir; - -static void __fresult_set_errno(FRESULT err) -{ - assertf(err != FR_INT_ERR, "FatFS assertion error"); - switch (err) { - case FR_OK: return; - case FR_DISK_ERR: errno = EIO; return; - case FR_NOT_READY: errno = EBUSY; return; - case FR_NO_FILE: errno = ENOENT; return; - case FR_NO_PATH: errno = ENOENT; return; - case FR_INVALID_NAME: errno = EINVAL; return; - case FR_DENIED: errno = EACCES; return; - case FR_EXIST: errno = EEXIST; return; - case FR_INVALID_OBJECT: errno = EINVAL; return; - case FR_WRITE_PROTECTED: errno = EROFS; return; - case FR_INVALID_DRIVE: errno = ENODEV; return; - case FR_NOT_ENABLED: errno = ENODEV; return; - case FR_NO_FILESYSTEM: errno = ENODEV; return; - case FR_MKFS_ABORTED: errno = EIO; return; - case FR_TIMEOUT: errno = ETIMEDOUT; return; - case FR_LOCKED: errno = EBUSY; return; - case FR_NOT_ENOUGH_CORE: errno = ENOMEM; return; - case FR_TOO_MANY_OPEN_FILES: errno = EMFILE; return; - case FR_INVALID_PARAMETER: errno = EINVAL; return; - default: errno = EIO; return; - } -} - -static void *__fat_open(char *name, int flags) +static int fat_disk_read_sd(uint8_t* buff, int64_t sector, int count) { - int i; - for (i=0;ist_size = size; - if (attr & AM_RDO) - st->st_mode |= 0444; - else - st->st_mode |= 0666; - if (attr & AM_DIR) - st->st_mode |= S_IFDIR; - else - st->st_mode |= S_IFREG; -} - -static int __fat_fstat(void *file, struct stat *st) -{ - FIL *f = file; - __fat_stat_fill(f_size(f), f->obj.attr, st); - return 0; -} - -static int __fat_read(void *file, uint8_t *ptr, int len) -{ - UINT read; - FRESULT res = f_read(file, ptr, len, &read); - if (res != FR_OK) { - __fresult_set_errno(res); - return -1; - } - return read; -} - -static int __fat_write(void *file, uint8_t *ptr, int len) -{ - UINT written; - FRESULT res = f_write(file, ptr, len, &written); - if (res != FR_OK) { - __fresult_set_errno(res); - return -1; - } - return written; -} + _Static_assert(FF_MIN_SS == 512, "this function assumes sector size == 512"); + _Static_assert(FF_MAX_SS == 512, "this function assumes sector size == 512"); + assertf((uint32_t)sector == sector, "unsupported access to SD card > 2 TiB"); -static int __fat_close(void *file) -{ - FRESULT res = f_close(file); - if (res != FR_OK) { - __fresult_set_errno(res); - return -1; - } - return 0; -} + // Check whether the user is requesting a read into RDRAM, or into + // the cartridge's SDRAM (that is, a PI-accessible address). We try + // to be generic here and not hardcode specific addresses; we assume + // libcart will know better than us where the SDRAM is located. + if (PhysicalAddr(buff) < 0x00800000) + return cart_card_rd_dram(buff, sector, count) ? RES_ERROR : RES_OK; + if (io_accessible(PhysicalAddr(buff))) + return cart_card_rd_cart(PhysicalAddr(buff), sector, count) ? RES_ERROR : RES_OK; -static int __fat_lseek(void *file, int offset, int whence) -{ - FRESULT res; - FIL *f = file; - switch (whence) - { - case SEEK_SET: res = f_lseek(f, offset); break; - case SEEK_CUR: res = f_lseek(f, f_tell(f) + offset); break; - case SEEK_END: res = f_lseek(f, f_size(f) + offset); break; - default: return -1; - } - if (res != FR_OK) { - __fresult_set_errno(res); - return -1; - } - return f_tell(f); + return RES_PARERR; } -static int __fat_unlink(char *name) -{ - FRESULT res = f_unlink(name); - if (res != FR_OK) { - __fresult_set_errno(res); - return -1; - } - return 0; -} -static int __fat_findnext(dir_t *dir) +static int fat_disk_write_sd(const uint8_t* buff, int64_t sector, int count) { - FILINFO info; - FRESULT res = f_readdir(&find_dir, &info); - if (res != FR_OK) { - __fresult_set_errno(res); - return -1; - } - - // Check if we reached the end of the directory - if (info.fname[0] == 0) { - res = f_closedir(&find_dir); - if (res != FR_OK) { - __fresult_set_errno(res); - return -1; - } - return -1; - } - - strlcpy(dir->d_name, info.fname, sizeof(dir->d_name)); - if (info.fattrib & AM_DIR) - dir->d_type = DT_DIR; - else - dir->d_type = DT_REG; - return 0; + _Static_assert(FF_MIN_SS == 512, "this function assumes sector size == 512"); + _Static_assert(FF_MAX_SS == 512, "this function assumes sector size == 512"); + assertf((uint32_t)sector == sector, "unsupported access to SD card > 2 TiB"); + return cart_card_wr_dram(buff, sector, count) ? RES_ERROR : RES_OK; } -static int __fat_findfirst(char *name, dir_t *dir) +static fat_disk_t fat_disk_sd = { - FRESULT res = f_opendir(&find_dir, name); - if (res != FR_OK) { - return -1; - } - return __fat_findnext(dir); -} - -static filesystem_t fat_fs = { - .open = __fat_open, - .fstat = __fat_fstat, - .lseek = __fat_lseek, - .read = __fat_read, - .write = __fat_write, - .close = __fat_close, - .unlink = __fat_unlink, - .findfirst = __fat_findfirst, - .findnext = __fat_findnext, + .disk_initialize = fat_disk_initialize_sd, + .disk_status = fat_disk_status_default, + .disk_read = fat_disk_read_sd, + .disk_write = fat_disk_write_sd, + .disk_ioctl = fat_disk_ioctl_default, }; - - /** Initialize the SD stack just once */ static bool sd_initialize_once(void) { static bool once = false; @@ -535,6 +263,7 @@ static bool usb_initialize_once(void) { return ok; } +/** Emit debug output */ static int __stderr_write(char *buf, unsigned int len) { for (int i=0; i= 0) { - sdfs_logic_drive[0] = '0' + npart; - sdfs_logic_drive[1] = ':'; - sdfs_logic_drive[2] = '\0'; - } else { - sdfs_logic_drive[0] = '\0'; - } - - FRESULT res = f_mount(&sd_fat, sdfs_logic_drive, 1); - if (res != FR_OK) - { - debugf("Cannot mount SD FAT filesystem: %d\n", res); + strlcpy(sdfs_prefix, prefix, sizeof(sdfs_prefix)); + sdfs_vol_id = fat_mount(sdfs_prefix, &fat_disk_sd, FAT_MOUNT_DEFERRED); + if (sdfs_vol_id < 0) return false; - } - strlcpy(sdfs_prefix, prefix, sizeof(sdfs_prefix)); - attach_filesystem(sdfs_prefix, &fat_fs); enabled_features |= DEBUG_FEATURE_FILE_SD; return true; } void debug_close_sdfs(void) { - if (enabled_features & DEBUG_FEATURE_FILE_SD) + if ((enabled_features & DEBUG_FEATURE_FILE_SD) && sdfs_vol_id >= 0) { - detach_filesystem(sdfs_prefix); - f_mount(NULL, sdfs_logic_drive, 0); + if (fat_unmount(sdfs_vol_id) == 0) { + sdfs_vol_id = -1; + enabled_features &= ~DEBUG_FEATURE_FILE_SD; + } + } +} + +static void debugfv(const char *msg, va_list args) +{ + // Debug output. The goal is to call __stderr_write() with the final output + // string. Normally, we can do this simply by calling vfprintf(stderr, ...). + // + // Under interrupts, when the kernel is used, though, we need to avoid + // using stdio functions as they could deadlock on FILE mutexes. The best + // would be to have a fprintf variant that calls a custom function for each + // output character, but that doesn't exist in newlib. So we resort to a + // fixed-size vsnprintf buffer. + if (__kernel && exception_is_running()) { + char buf[256] __attribute__((uninitialized)); + int n = vsnprintf(buf, sizeof(buf), msg, args); + if (n > 0) __stderr_write(buf, n < sizeof(buf) ? n : sizeof(buf)); + return; } + + vfprintf(stderr, msg, args); +} + +void debugf(const char *msg, ...) +{ + va_list args; + va_start(args, msg); + debugfv(msg, args); } void debug_assert_func_f(const char *file, int line, const char *func, const char *failedexpr, const char *msg, ...) @@ -642,7 +395,7 @@ void debug_assert_func_f(const char *file, int line, const char *func, const cha // As first step, immediately print the assertion on stderr. This is // very likely to succeed as it should not cause any further allocations // and we would display the assertion immediately on logs. - fprintf(stderr, + debugf( "ASSERTION FAILED: %s\n" "file \"%s\", line %d%s%s\n", failedexpr, file, line, @@ -653,13 +406,13 @@ void debug_assert_func_f(const char *file, int line, const char *func, const cha va_list args; va_start(args, msg); - vfprintf(stderr, msg, args); + debugfv(msg, args); va_end(args); - fprintf(stderr, "\n"); + debugf("\n"); } - fprintf(stderr, "\n"); + debugf("\n"); va_list args; va_start(args, msg); diff --git a/include/dfsinternal.h b/src/dfs_internal.h similarity index 81% rename from include/dfsinternal.h rename to src/dfs_internal.h index 68b178569..e0fcfdb2f 100644 --- a/include/dfsinternal.h +++ b/src/dfs_internal.h @@ -1,7 +1,8 @@ /** - * @file dfsinternal.h + * @file dfs_internal.h * @author Jennifer Taylor - * @author gamemasterplc + * @author Giovanni Bajo + * @author Liam Coleman * @brief Internal DFS Definitions * @ingroup dfs */ @@ -18,12 +19,11 @@ /** @brief The special ID value in #directory_entry::next_entry defining the root sector */ #define ROOT_NEXT_ENTRY 0xDEADBEEF /** @brief Special path value in #directory_entry::path defining the root sector */ -#define ROOT_PATH "DragonFS 2.0" +#define ROOT_PATH "DragonFS 2.1" + /** @brief The size of a sector */ -#define SECTOR_SIZE 256 -/** @brief The size of a sector payload */ -#define SECTOR_PAYLOAD 252 +#define MAX_DIRENT_SIZE 256 /** @brief Prime number used for hash lookups */ #define DFS_LOOKUP_PRIME 31 @@ -35,13 +35,14 @@ struct directory_entry uint32_t next_entry; /** @brief File size and flags. See #FLAGS_FILE, #FLAGS_DIR and #FLAGS_EOF */ uint32_t flags; - /** @brief The file or directory name */ - char path[MAX_FILENAME_LEN+1]; /** @brief Offset to start sector of the file */ uint32_t file_pointer; -} __attribute__((__packed__)); + /** @brief The file or directory name */ + char path[]; +}; -_Static_assert(sizeof(struct directory_entry) == SECTOR_SIZE, "invalid directory_entry size"); +/** @brief Size of the ID dirent entry (beginning of the filesystem) */ +#define ID_DIRENT_SIZE (((sizeof(directory_entry_t) + strlen(ROOT_PATH) + 1) + 1) / 2 * 2) /** @brief Type definition */ typedef struct directory_entry directory_entry_t; @@ -53,8 +54,8 @@ typedef struct dfs_open_file_s uint32_t size; /** @brief The offset of the current location in the file */ uint32_t loc; - /** @brief The offset within the filesystem where the file is stored */ - uint32_t cart_start_loc; + /** @brief The PI address where the file is stored */ + pi_addr_t cart_start_loc; } dfs_open_file_t; /** @brief Data for a single file in dfs_lookup_t */ diff --git a/src/display.c b/src/display.c index 9d20e5f78..61b375c1d 100644 --- a/src/display.c +++ b/src/display.c @@ -9,295 +9,546 @@ #include #include #include +#include #include "regsinternal.h" +#include "system_internal.h" #include "n64sys.h" -#include "vi.h" #include "display.h" #include "interrupt.h" #include "utils.h" #include "debug.h" #include "surface.h" #include "rsp.h" +#include "kirq.h" +#include "accounting_internal.h" /** @brief Maximum number of video backbuffers */ #define NUM_BUFFERS 32 /** @brief Number of past frames used to evaluate FPS */ #define FPS_WINDOW 32 +/** @brief How many times per second we should update the FPS value */ +#define FPS_UPDATE_FREQ 4 static surface_t *surfaces; +/** @brief Currently allocated Z-buffer (allocated for __alloc_width x __alloc_height) */ +static surface_t surf_zbuf; +/** @brief View of zbuf with current getter dimensions (__width x __height); returned by display_get_zbuf */ +static surface_t surf_zbuf_view; +/** @brief Record whether the Z-buffer was allocated via sbrk_top */ +static bool zbuf_sbrk_top = false; +/** @brief True if the vblank handler is installed */ +static bool handler_installed = false; /** @brief Currently active bit depth */ static uint32_t __bitdepth; /** @brief Currently active video width (calculated) */ static uint32_t __width; /** @brief Currently active video height (calculated) */ static uint32_t __height; -/** @brief Number of active buffers */ -static uint32_t __buffers = NUM_BUFFERS; -/** @brief Pointer to uncached 16-bit aligned version of buffers */ -static void *__safe_buffer[NUM_BUFFERS]; +/** @brief Currently active video interlace mode */ +static interlace_mode_t __interlace_mode = INTERLACE_OFF; +/** @brief Number of active buffers (returned by display_get_num_buffers); also used for the assignable ring in display_try_get. */ +static int __num_buffers = 0; +/** @brief Allocated framebuffer dimensions (from display_init; used for display_change validation and zbuf) */ +static uint32_t __alloc_width = 0; +static uint32_t __alloc_height = 0; +static uint32_t __alloc_bitdepth = 0; +static uint32_t __alloc_buffers = 0; +/** @brief Stored params for apply_display_vi_config when a display_change() is pending */ +static resolution_t pending_res; +static bitdepth_t pending_bit; +static gamma_t pending_gamma; +static filter_options_t pending_filters; +static interlace_mode_t pending_interlace_mode; +/** @brief Pending display_change countdown: -1 means no pending config, >=0 means frames left before apply */ +static int pending_vi_frames_left = -1; /** @brief Currently displayed buffer */ static int now_showing = -1; +/** @brief FIFO queue of buffer indices in the order they were obtained via display_get/display_try_get */ +static uint8_t display_queue[NUM_BUFFERS]; +/** @brief Queue head (next index that should be shown, once ready) */ +static uint8_t display_queue_head = 0; +/** @brief Queue tail (where next obtained index is appended) */ +static uint8_t display_queue_tail = 0; +/** @brief Number of queued buffers that must be shown in FIFO order */ +static uint8_t display_queue_count = 0; /** @brief Bitmask of surfaces that are currently being drawn */ static uint32_t drawing_mask = 0; /** @brief Bitmask of surfaces that are ready to be shown */ static volatile uint32_t ready_mask = 0; -/** @brief Window of absolute times at which previous frames were shown */ -static uint32_t frame_times[FPS_WINDOW]; -/** @brief Current index into the frame times window */ -static int frame_times_index = 0; -/** @brief Current duration of the frame window (time elapsed for FPS_WINDOW frames) */ -static uint32_t frame_times_duration; - -/** @brief Get the next buffer index (with wraparound) */ +/** @brief Actual display refresh rate */ +static float refresh_rate; +/** @brief Actual display refresh period */ +static float refresh_period; +/** @brief Currently set FPS limit */ +static float fps_limit; +/** @brief Currently calculated delta time estimation */ +static volatile float delta_time; +/** @brief Snapshot of frame rate for display purposes (avoid changing it too fast) */ +static volatile float frame_rate_snapshot; +/** @brief Factor between TV refresh rate and requested virtual refresh rate (#display_set_fps_limit) */ +static float frame_skip; +/** @brief Minimum refresh period as requested by #display_set_fps_limit */ +static float min_refresh_period; +/** @brief Rounded minimum refresh period as requested by #display_set_fps_limit */ +static float min_refresh_period_rounded; +/** @brief True if we are applying the workaround for the VI bug on 320x16-bit unfiltered mode */ +static bool vi_bug_workaround = false; + +static void apply_display_vi_config(resolution_t res, bitdepth_t bit, gamma_t gamma, filter_options_t filters); + +/** @brief State for the Kalman filter */ +typedef struct { + float P; ///< Process noise covariance + float Q; ///< Measurement noise covariance + float R; ///< Estimation error covariance + float p_estimate; ///> Last estimated value +} kalman_state_t; + +/** @brief State for kalman filter used for FPS estimation */ +static kalman_state_t k_fps; +/** @brief State for kalman filter used for delta-time estimation */ +static kalman_state_t k_delta; + +/** @brief Initalize Kalman's filter */ +static void kalman_init(kalman_state_t *s, float x, float Q) +{ + s->P = 1.0f; + s->Q = Q; + s->R = 1.0f; + s->p_estimate = x; +} + +/** @brief Run kalman's filter */ +static float kalman(kalman_state_t *s, float x) +{ + float p_pred = s->p_estimate; + float P_pred = s->P + s->Q; + + float K = P_pred / (P_pred + s->R); + s->p_estimate = p_pred + K * (x - p_pred); + s->P = (1 - K) * P_pred; + + return s->p_estimate; +} + +/** @brief Get the next buffer index (with wraparound) for the configured ring; used by display_try_get. */ static inline int buffer_next(int idx) { idx += 1; - if (idx == __buffers) + if (idx >= __num_buffers) idx = 0; return idx; } +/** + * @brief Check if we should use this vblank interrupt or not, depending on fps limit + * + * FPS limit is implemented simply by pretending the hardware is slower at generating + * video interrupts, which in turn means skipping an interrupt every now and then + * to keep the frame rate within the desired limits. + */ +static bool fps_limit_ok(void) +{ + static float frame_skip_accum = 0.0f; + frame_skip_accum += frame_skip; + if (frame_skip_accum < 0.0f) return false; + frame_skip_accum -= 1.0f; + return true; +} + +/** + * @brief Update FPS estimation. + * + * This function is on every "virtual" vblank (that is, only on vblank interrupts + * which are not ignored by #fps_limit_ok). It updates the estimation of the + * frame rate using a Kalman filter, based on the number of frames that were + * actually displayed. + * + * @param newframe True if a new frame was displayed in this vblank, false otherwise + */ +static void update_fps(bool newframe) +{ + static int last_frame_counter = 0; + ++last_frame_counter; + if (!newframe) return; + + // Calculate updated delta_time and frame_rate. Technically one is just the + // reciprocal of the other, but we prefer a more reactive kalman filter (Q=1) + // for delta_time, and a more stable one (Q=0.01) for frame_rate for display purposes. + delta_time = kalman(&k_delta, last_frame_counter) * min_refresh_period; + float kk_fps = kalman(&k_fps, last_frame_counter); + + // Take a few snapshots of the framerate for display purposes. + static uint32_t last_update = 0; + uint32_t now = TICKS_READ(); + if (TICKS_DISTANCE(last_update, now) > TICKS_PER_SECOND / FPS_UPDATE_FREQ) { + last_update = now; + frame_rate_snapshot = 1.0f / (kk_fps * min_refresh_period_rounded); + // Update the refresh rate in case it changed (eg: switch PAL50/PAL60) + refresh_rate = vi_get_refresh_rate(); + refresh_period = 1.0f / refresh_rate; + display_set_fps_limit(fps_limit); + } + + last_frame_counter = 0; +} + /** * @brief Interrupt handler for vertical blank * * If there is another frame to display, display the frame */ -static void __display_callback() +static void __display_callback(void *arg) { + // If a reset has occured and this is almost the last VI interrupt + // before RESET_TIME_LENGTH grace period, stop all work and exit + uint32_t next_time = TICKS_FROM_MS(refresh_period*1000); + if(exception_reset_time() + next_time*3 >= RESET_TIME_LENGTH) die(); + /* Least significant bit of the current line register indicates if the currently displayed field is odd or even. */ bool field = (*VI_V_CURRENT) & 1; - bool interlaced = (*VI_CTRL) & (VI_CTRL_SERRATE); - - /* Check if the next buffer is ready to be displayed, otherwise just - leave up the current frame */ - int next = buffer_next(now_showing); - if (ready_mask & (1 << next)) { - now_showing = next; - ready_mask &= ~(1 << next); - } - vi_write_dram_register(__safe_buffer[now_showing] + (interlaced && !field ? __width * __bitdepth : 0)); - - // FIXME: PAL-M on old boards like NUS-CPU-02 requires changing V_BURST every field, otherwise - // the image seems garbled at the top. It is probably a bug in old revisions of the VI chip, - // since the problem doesn't exist on newer boards. - if (get_tv_type() == TV_MPAL && interlaced) { - if (field == 0) { - *VI_V_BURST = 0x000b0202; - } else { - *VI_V_BURST = 0x000e0204; + bool apply_pending_vi_config = false; + + /* Show frames in strict FIFO order (order of display_get/display_try_get). + If full interlace mode is selected then don't update the buffer until two fields were displayed. */ + if (!(__interlace_mode == INTERLACE_FULL && field) && fps_limit_ok()) { + bool newframe = false; + if (display_queue_count > 0) { + int next = display_queue[display_queue_head]; + if (ready_mask & (1U << next)) { + if (pending_vi_frames_left >= 0) { + if (pending_vi_frames_left > 0) { + pending_vi_frames_left--; + } else { + apply_pending_vi_config = true; + pending_vi_frames_left = -1; + } + } + now_showing = next; + ready_mask &= ~(1U << next); + display_queue_head = (display_queue_head + 1) % NUM_BUFFERS; + display_queue_count--; + newframe = true; + } } + update_fps(newframe); } -} - -void display_init( resolution_t res, bitdepth_t bit, uint32_t num_buffers, gamma_t gamma, filter_options_t filters ) -{ - uint32_t tv_type = get_tv_type(); - uint32_t control = !sys_bbplayer()? VI_PIXEL_ADVANCE_DEFAULT : VI_PIXEL_ADVANCE_BBPLAYER; - - /* Can't have the video interrupt happening here */ - disable_interrupts(); - - /* Minimum is two buffers. */ - __buffers = MAX(1, MIN(NUM_BUFFERS, num_buffers)); - - if( res.interlaced ) - { - /* Serrate on to stop vertical jitter */ - control |= VI_CTRL_SERRATE; - } - - /* Figure out control register based on input given */ - switch( bit ) - { - case DEPTH_16_BPP: - control |= VI_CTRL_TYPE_16_BPP; - break; - case DEPTH_32_BPP: - control |= VI_CTRL_TYPE_32_BPP; - break; + vi_write_begin(); + if (apply_pending_vi_config) { + __interlace_mode = pending_interlace_mode; + apply_display_vi_config(pending_res, pending_bit, pending_gamma, pending_filters); } + vi_show(&surfaces[now_showing]); + if ( vi_bug_workaround ) vi_write(VI_X_SCALE, 0x201); + vi_write_end(); +} - switch( gamma ) - { - case GAMMA_NONE: - /* Nothing to set here */ - break; - case GAMMA_CORRECT: - control |= VI_GAMMA_ENABLE; - break; - case GAMMA_CORRECT_DITHER: - control |= VI_GAMMA_ENABLE | VI_GAMMA_DITHER_ENABLE; - break; - } +/** + * @brief Apply VI configuration (interlace, gamma, filters, borders, vi_bug_workaround). + * Caller must hold vi_write_begin() / vi_write_end() around this. + */ +static void apply_display_vi_config(resolution_t res, bitdepth_t bit, gamma_t gamma, filter_options_t filters) +{ + vi_set_interlaced(res.interlaced != INTERLACE_OFF); + vi_set_gamma((vi_gamma_t)gamma); - switch( filters ) + switch (filters) { - /* Libdragon uses preconfigured modes for enabling certain - combinations of VI filters due to a large number of wrong/invalid configurations - with very strict conditions, and to simplify the options for the user. - Like for example antialiasing requiring resampling; dedithering not working with - resampling, unless always fetching; always enabling divot filter under AA etc. - The cases below provide all possible configurations that are deemed useful. */ + /* Libdragon uses preconfigured modes for enabling certain + combinations of VI filters due to a large number of wrong/invalid configurations + with very strict conditions, and to simplify the options for the user. + Like for example antialiasing requiring resampling; dedithering not working with + resampling, unless always fetching; always enabling divot filter under AA etc. + The cases below provide all possible configurations that are deemed useful. */ case FILTERS_DISABLED: - /* Disabling resampling (AA_MODE = 0x3) on 16bpp hits a hardware bug on NTSC + /* Disabling resampling (AA_MODE = 0x3) on 16bpp hits a hardware bug on NTSC consoles when the horizontal resolution is 320 or lower (see issue #66). It would work on PAL consoles, but we think users are better served by prohibiting it altogether. - For people that absolutely need this on PAL consoles, it can - be enabled with *(volatile uint32_t*)0xA4400000 |= 0x300 just - after the display_init call. */ - if ( bit == DEPTH_16_BPP ) - { - assertf(res.width > 320, + For the very common case of width=320 exactly, we can do a workaround, + which is setting a slightly higher VI XSCALE (0x201 instead of 0x200) + which workarounds the bug without any artifact. See below where the + fix is applied. + + For people that absolutely need this on PAL consoles, call display_init() + with FILTERS_RESAMPLE, and then call vi_set_aa_mode(VI_AA_MODE_NONE); */ + if (bit == DEPTH_16_BPP) { + assertf(res.width >= 320, "FILTERS_DISABLED is not supported by the hardware for widths <= 320.\n" "Please use FILTERS_RESAMPLE instead."); } - - /* Set AA off flag */ - control |= VI_AA_MODE_NONE; - + vi_set_aa_mode(VI_AA_MODE_NONE); + vi_set_divot(false); + vi_set_dedither(false); break; case FILTERS_RESAMPLE: /* Set AA on resample */ - control |= VI_AA_MODE_RESAMPLE; + vi_set_aa_mode(VI_AA_MODE_RESAMPLE); /* Dither filter should not be enabled with this AA mode as it will cause ugly vertical streaks */ + vi_set_divot(false); + vi_set_dedither(false); break; case FILTERS_DEDITHER: - /* Set AA off flag and dedither on - (only on 16bpp mode, act as FILTERS_DISABLED on 32bpp) */ - if ( bit == DEPTH_16_BPP ) { - // Assert on width (see FILTERS_DISABLED) + /* Set AA off flag and dedither on + (only on 16bpp mode, act as FILTERS_DISABLED on 32bpp) */ + if (bit == DEPTH_16_BPP) { + /* Assert on width (see FILTERS_DISABLED) */ assertf(res.width > 320, "FILTERS_DEDITHER is not supported by the hardware for widths <= 320.\n" "Please use FILTERS_RESAMPLE instead."); - control |= VI_AA_MODE_NONE | VI_DEDITHER_FILTER_ENABLE; - } - else control |= VI_AA_MODE_NONE; - + vi_set_aa_mode(VI_AA_MODE_NONE); + vi_set_divot(false); + vi_set_dedither(true); + } else { + vi_set_aa_mode(VI_AA_MODE_NONE); + vi_set_divot(false); + vi_set_dedither(false); + } break; case FILTERS_RESAMPLE_ANTIALIAS: - /* Set AA on resample and fetch as well as divot on */ - control |= VI_AA_MODE_RESAMPLE_FETCH_NEEDED | VI_DIVOT_ENABLE; - + /* Set AA on resample and fetch as well as divot on. + * FETCH_ALWAYS seems to change the way VI operates on the bus + * to work better when there is bandwidth saturation; so + * even if the RDRAM bus is very busy, the VI will still get the + * data it needs. + * + * Note that FETCH_ALWAYS appears to be broken in 32bpp modes, so + * we cannot use it there; this means that it'll be much easier + * to get image corruption for VI bandwidth saturation in 32bpp modes. + */ + if (bit == DEPTH_16_BPP) + vi_set_aa_mode(VI_AA_MODE_RESAMPLE_FETCH_ALWAYS); + else + vi_set_aa_mode(VI_AA_MODE_RESAMPLE_FETCH_NEEDED); + vi_set_divot(true); + vi_set_dedither(false); break; case FILTERS_RESAMPLE_ANTIALIAS_DEDITHER: - /* Set AA on resample always and fetch as well as dedither on - (only on 16bpp mode, act as FILTERS_RESAMPLE_ANTIALIAS on 32bpp) */ - - /* Enable dither filter in 16bpp mode to give gradients - a slightly smoother look */ - if ( bit == DEPTH_16_BPP ) - control |= VI_AA_MODE_RESAMPLE_FETCH_ALWAYS | VI_DEDITHER_FILTER_ENABLE | VI_DIVOT_ENABLE; - else control |= VI_AA_MODE_RESAMPLE_FETCH_NEEDED | VI_DIVOT_ENABLE; + /* Set AA on resample always and fetch as well as dedither on + (only on 16bpp mode, act as FILTERS_RESAMPLE_ANTIALIAS on 32bpp) */ + if (bit == DEPTH_16_BPP) { + vi_set_aa_mode(VI_AA_MODE_RESAMPLE_FETCH_ALWAYS); + vi_set_dedither(true); + vi_set_divot(true); + } else { + vi_set_aa_mode(VI_AA_MODE_RESAMPLE_FETCH_NEEDED); + vi_set_dedither(false); + vi_set_divot(true); + } break; } + float aspect_ratio = res.aspect_ratio ? res.aspect_ratio : 4.0f / 3.0f; + vi_set_borders(vi_calc_borders(aspect_ratio, res.overscan_margin)); + + /* Workaround for VI bug */ + vi_bug_workaround = (res.width == 320 && bit == DEPTH_16_BPP && filters == FILTERS_DISABLED); +} + +void display_init( resolution_t res, bitdepth_t bit, uint32_t num_buffers, gamma_t gamma, filter_options_t filters ) +{ + assertf(__num_buffers == 0, "display_init() called while the display is already initialized.\nPlease close the current display with display_close() first."); + /* Calculate width and scale registers */ assertf(res.width > 0, "nonpositive width"); assertf(res.height > 0, "nonpositive height"); assertf(res.width <= 800, "invalid width"); assertf(res.height <= 720, "heights > 720 are buggy on hardware"); - if( bit == DEPTH_16_BPP ) - { + if (bit == DEPTH_16_BPP) assertf(res.width % 4 == 0, "width must be divisible by 4 for 16-bit depth"); - } - else if ( bit == DEPTH_32_BPP ) - { + else if (bit == DEPTH_32_BPP) assertf(res.width % 2 == 0, "width must be divisible by 2 for 32-bit depth"); - } + + vi_init(); + vi_write_begin(); + + /* Reset VI configuration to default, before proceeding to configure the + current display mode. */ + vi_reset(); + + /* Minimum is at least one buffer. */ + uint32_t nb = MAX(1, MIN(NUM_BUFFERS, num_buffers)); + __num_buffers = (int)nb; + __alloc_buffers = nb; /* Set up the display */ __width = res.width; __height = res.height; - __bitdepth = ( bit == DEPTH_16_BPP ) ? 2 : 4; + __bitdepth = (bit == DEPTH_16_BPP) ? 2 : 4; + __interlace_mode = res.interlaced; + __alloc_width = res.width; + __alloc_height = res.height; + __alloc_bitdepth = __bitdepth; + + /* Set up pending config variables to affect display.h getters */ + pending_res = res; + pending_bit = bit; + pending_gamma = gamma; + pending_filters = filters; + pending_interlace_mode = res.interlaced; + pending_vi_frames_left = -1; - surfaces = malloc(sizeof(surface_t) * __buffers); + apply_display_vi_config(res, bit, gamma, filters); + + surfaces = malloc(sizeof(surface_t) * __num_buffers); + assertf(surfaces, "Out of memory"); /* Initialize buffers and set parameters */ - for( int i = 0; i < __buffers; i++ ) + tex_format_t format = bit == DEPTH_16_BPP ? FMT_RGBA16 : FMT_RGBA32; + for (int i = 0; i < __num_buffers; i++) { /* Set parameters necessary for drawing */ /* Grab a location to render to */ - tex_format_t format = bit == DEPTH_16_BPP ? FMT_RGBA16 : FMT_RGBA32; surfaces[i] = surface_alloc(format, __width, __height); - __safe_buffer[i] = surfaces[i].buffer; - assert(__safe_buffer[i] != NULL); + assertf(surfaces[i].buffer, "Out of memory"); /* Baseline is blank */ - memset( __safe_buffer[i], 0, __width * __height * __bitdepth ); + sys_hw_memset(surfaces[i].buffer, 0, __width * __height * __bitdepth); } /* Set the first buffer as the displaying buffer */ now_showing = 0; drawing_mask = 0; ready_mask = 0; + display_queue_head = 0; + display_queue_tail = 0; + display_queue_count = 0; + vi_show(&surfaces[0]); + if (vi_bug_workaround) { + /* VI hits a rendering bug when HSTART < 128 && 16-bpp && X_SCALE <= 0x200, + and resampling is disabled (see vi.c for this). HSTART < 128 is the + default border configuration on NTSC. Since X_SCALE=0x200 means + width=320 which happens to be the most common resolution, let's apply + a simple workaround. + A X_SCALE of 0x201 will behave exactly like 0x200 would if it worked, + and introduce zero rendering artifacts (without resampling, that is). */ + vi_write(VI_X_SCALE, 0x201); + } - /* Show our screen normally. If display is already active, do that during vblank - to avoid confusing the VI chip with in-frame modifications. */ - if ( vi_is_active() ) { vi_wait_for_vblank(); } + /* Calculate actual refresh rate for this configuration */ + refresh_rate = vi_get_refresh_rate(); + refresh_period = 1.0f / refresh_rate; + frame_rate_snapshot = refresh_rate; + display_set_fps_limit(0); + kalman_init(&k_fps, 1.0f, 0.01f); + kalman_init(&k_delta, 1.0f, 1.0f); - /* Set basic preset */ - vi_write_config(&vi_config_presets[res.interlaced][tv_type]); + vi_write_end(); - vi_write_safe(VI_ORIGIN, PhysicalAddr(__safe_buffer[0])); - vi_write_safe(VI_WIDTH, res.width); - vi_write_safe(VI_X_SCALE, VI_X_SCALE_SET(res.width)); - vi_write_safe(VI_Y_SCALE, VI_Y_SCALE_SET(res.height)); - vi_write_safe(VI_CTRL, control); + vi_install_vblank_handler(__display_callback, NULL); + handler_installed = true; +} - enable_interrupts(); +void display_change(resolution_t res, bitdepth_t bit, uint32_t num_buffers, gamma_t gamma, filter_options_t filters) +{ + assertf(__alloc_buffers != 0, "display_change() called with display not initialized."); + + assertf(res.width > 0, "nonpositive width"); + assertf(res.height > 0, "nonpositive height"); + assertf(res.width <= 800, "invalid width"); + assertf(res.height <= 720, "heights > 720 are buggy on hardware"); + if (bit == DEPTH_16_BPP) + assertf(res.width % 4 == 0, "width must be divisible by 4 for 16-bit depth"); + else if (bit == DEPTH_32_BPP) + assertf(res.width % 2 == 0, "width must be divisible by 2 for 32-bit depth"); + + assertf(num_buffers <= __alloc_buffers, + "display_change() num_buffers (%u) exceeds originally allocated (%u).", (unsigned)num_buffers, (unsigned)__alloc_buffers); + + uint32_t new_bpp = (bit == DEPTH_16_BPP) ? 2 : 4; + uint32_t new_size = (uint32_t)res.width * (uint32_t)res.height * new_bpp; + uint32_t alloc_size = __alloc_width * __alloc_height * __alloc_bitdepth; + assertf(new_size <= alloc_size, + "display_change() framebuffer size (%ux%u %s) exceeds allocated size.", (unsigned)res.width, (unsigned)res.height, + bit == DEPTH_16_BPP ? "16bpp" : "32bpp"); - /* Set which line to call back on in order to flip screens */ - register_VI_handler( __display_callback ); - set_VI_interrupt( 1, VI_V_CURRENT_VBLANK ); + uint32_t nb = MAX(1, MIN(NUM_BUFFERS, num_buffers)); + + disable_interrupts(); + __num_buffers = (int)nb; + pending_res = res; + pending_bit = bit; + pending_gamma = gamma; + pending_filters = filters; + pending_interlace_mode = res.interlaced; + pending_vi_frames_left = display_queue_count; + + /* Update getters immediately so they return the new configuration. */ + __width = (uint32_t)res.width; + __height = (uint32_t)res.height; + __bitdepth = new_bpp; + enable_interrupts(); } void display_close() { - /* Can't have the video interrupt happening here */ - disable_interrupts(); + // Contrary to most other subsystems, for display we want to handle + // correctly failed display_init() calls followed by + // display_close(), because this is a common pattern during eg + // exception handling. So we need to be a bit more careful than usual + // to make sure to deinitialize piece-wise. - set_VI_interrupt( 0, 0 ); - unregister_VI_handler( __display_callback ); + if ( handler_installed ) + { + vi_uninstall_vblank_handler(__display_callback, NULL); + handler_installed = false; + } now_showing = -1; drawing_mask = 0; ready_mask = 0; + display_queue_head = 0; + display_queue_tail = 0; + display_queue_count = 0; + pending_vi_frames_left = -1; - __width = 0; - __height = 0; - - /* If display is active, wait for vblank before touching the registers */ - if( vi_is_active() ) { vi_wait_for_vblank(); } + if (surf_zbuf.buffer) + { + surface_free(&surf_zbuf); + memset(&surf_zbuf_view, 0, sizeof(surf_zbuf_view)); + if (zbuf_sbrk_top) { + sbrk_top(-(int)(__alloc_width * __alloc_height * 2)); + zbuf_sbrk_top = false; + } + } - vi_set_blank_image(); - vi_write_dram_register( 0 ); + // Blank the image and wait until it actually happens, before + // freeing the buffers. + vi_show(NULL); + vi_wait_vblank(); - if( surfaces ) + if (surfaces) { - for( int i = 0; i < __buffers; i++ ) + for (uint32_t i = 0; i < __alloc_buffers; i++) { /* Free framebuffer memory */ surface_free(&surfaces[i]); - __safe_buffer[i] = NULL; } free(surfaces); surfaces = NULL; } - enable_interrupts(); + __width = 0; + __height = 0; + __num_buffers = 0; + __alloc_width = 0; + __alloc_height = 0; + __alloc_bitdepth = 0; + __alloc_buffers = 0; } surface_t* display_try_get(void) { surface_t* retval = NULL; - int next; + int next, start; + + assertf(__num_buffers != 0, "Display not initialized."); /* Can't have the video interrupt happening here */ disable_interrupts(); @@ -309,15 +560,28 @@ surface_t* display_try_get(void) Notice that the loop is always executed once, so it also works in the case of a single display buffer, though it at least wait for that buffer to be shown. */ - next = buffer_next(now_showing); + start = now_showing; + if (start < 0 || start >= __num_buffers) + start = __num_buffers - 1; + + next = buffer_next(start); do { - if (((drawing_mask | ready_mask) & (1 << next)) == 0) { + if (((drawing_mask | ready_mask) & (1U << next)) == 0) { + void *buf = surfaces[next].buffer; + tex_format_t fmt = __bitdepth == 2 ? FMT_RGBA16 : FMT_RGBA32; + uint16_t stride = (uint16_t)(__width * __bitdepth); + surfaces[next] = surface_make(buf, fmt, (uint16_t)__width, (uint16_t)__height, stride); + surfaces[next].flags |= SURFACE_FLAGS_OWNEDBUFFER; retval = &surfaces[next]; - drawing_mask |= 1 << next; + drawing_mask |= 1U << next; + assertf(display_queue_count < NUM_BUFFERS, "Display queue overflow"); + display_queue[display_queue_tail] = (uint8_t)next; + display_queue_tail = (display_queue_tail + 1) % NUM_BUFFERS; + display_queue_count++; break; } next = buffer_next(next); - } while (next != now_showing); + } while (next != start); enable_interrupts(); @@ -331,92 +595,126 @@ surface_t* display_get(void) // it is common for display to become ready again after RSP+RDP // have finished processing the previous frame's commands. surface_t* disp; - RSP_WAIT_LOOP(200) { + + assertf(__num_buffers != 0, "Display not initialized."); + + kirq_wait_t kirq = kirq_begin_wait_vi(); + ACCT_SCOPE(ACCT_CAT_DISPLAY) RSP_WAIT_LOOP(200) { if ((disp = display_try_get())) { break; } + kirq_wait(&kirq); } return disp; } +surface_t* display_get_zbuf(void) +{ + if (surf_zbuf.buffer == NULL) { + /* Try to allocate the Z-Buffer from the top of the heap (near the stack). + This basically puts it in the last memory bank, hopefully separating it + from framebuffers, which provides a nice speed gain. */ + uint32_t alloc_size = __alloc_width * __alloc_height * 2; + void *buf = sbrk_top(alloc_size); + if (buf != (void*)-1) { + data_cache_hit_invalidate(buf, alloc_size); + surf_zbuf = surface_make(UncachedAddr(buf), FMT_RGBA16, (uint16_t)__alloc_width, (uint16_t)__alloc_height, (uint16_t)(__alloc_width * 2)); + zbuf_sbrk_top = true; + } else { + surf_zbuf = surface_alloc(FMT_RGBA16, (uint16_t)__alloc_width, (uint16_t)__alloc_height); + zbuf_sbrk_top = false; + } + } + surf_zbuf_view = surface_make(surf_zbuf.buffer, FMT_RGBA16, (uint16_t)__width, (uint16_t)__height, (uint16_t)(__width * 2)); + return &surf_zbuf_view; +} + void display_show( surface_t* surf ) { /* They tried drawing on a bad context */ - if( surf == NULL ) { return; } + if (surf == NULL) + return; /* Can't have the video interrupt screwing this up */ disable_interrupts(); - /* Correct to ensure we are handling the right screen */ + /* Correct to ensure we are handling the right screen (any allocated buffer is valid). */ int i = surf - surfaces; - assertf(i >= 0 && i < __buffers, "Display context is not valid!"); + assertf(i >= 0 && i < (int)__alloc_buffers, "Display context is not valid!"); /* Check we have not unlocked this display already and is pending drawn. */ - assertf(!(ready_mask & (1 << i)), "display_show called again on the same display %d (mask: %lx)", i, ready_mask); + assertf(!(ready_mask & (1U << i)), "display_show called again on the same display %d (mask: %lx)", i, ready_mask); /* This should match, or something went awry */ - assertf(drawing_mask & (1 << i), "display_show called on non-locked display %d (mask: %lx)", i, drawing_mask); + assertf(drawing_mask & (1U << i), "display_show called on non-locked display %d (mask: %lx)", i, drawing_mask); - drawing_mask &= ~(1 << i); - ready_mask |= 1 << i; - - /* Record the time at which this frame was (asked to be) shown */ - uint32_t old_ticks = frame_times[frame_times_index]; - uint32_t now = TICKS_READ(); - if (old_ticks) - frame_times_duration = TICKS_DISTANCE(old_ticks, now); - frame_times[frame_times_index] = now; - frame_times_index++; - if (frame_times_index == FPS_WINDOW) - frame_times_index = 0; + drawing_mask &= ~(1U << i); + ready_mask |= 1U << i; enable_interrupts(); } -/** - * @brief Force-display a previously locked buffer - * - * Display a valid display context to the screen right away, without waiting - * for vblank interrupt. This function works also with interrupts disabled. - * - * NOTE: this is currently not part of the public API as we use it only - * internally. - * - * @param[in] disp - * A display context retrieved using #display_get - */ -void display_show_force( display_context_t disp ) -{ - /* Can't have the video interrupt screwing this up */ - disable_interrupts(); - display_show(disp); - __display_callback(); - enable_interrupts(); -} - uint32_t display_get_width(void) { - return __width; + return pending_res.width; } uint32_t display_get_height(void) { - return __height; + return pending_res.height; } uint32_t display_get_bitdepth(void) { - return __bitdepth; + return pending_bit == DEPTH_16_BPP ? 2 : 4; } uint32_t display_get_num_buffers(void) { - return __buffers; + return (uint32_t)__num_buffers; } float display_get_fps(void) { - if (!frame_times_duration) return 0; - return (float)FPS_WINDOW * TICKS_PER_SECOND / frame_times_duration; + return frame_rate_snapshot; +} + +float display_get_refresh_rate(void) +{ + return refresh_rate; +} + +float display_get_delta_time(void) +{ + return delta_time; +} + +void display_set_fps_limit(float fps) +{ + assert(fps >= 0.0f); + + disable_interrupts(); + + min_refresh_period = 1.0f / (fps ? MIN(fps, refresh_rate) : refresh_rate); + frame_skip = refresh_period / min_refresh_period; + delta_time = min_refresh_period; + + // Calculate also the minimum period using a rounded refresh rate + // This will be used only for display purposes, so that FPS are capped + // to 60 Hz rather than 59.83 Hz, which would be the hw-accurate value. + min_refresh_period_rounded = 1.0f / (fps ? MIN(fps, roundf(refresh_rate)) : roundf(refresh_rate)); + + fps_limit = fps; + + enable_interrupts(); +} + +surface_t display_get_current_framebuffer(void) +{ + return surface_make_linear( + VirtualUncachedAddr(*VI_ORIGIN), + display_get_bitdepth() == 2 ? FMT_RGBA16 : FMT_RGBA32, + display_get_width(), + display_get_height()); } diff --git a/src/dlfcn.c b/src/dlfcn.c index 9a60b9b13..1b51ae90b 100644 --- a/src/dlfcn.c +++ b/src/dlfcn.c @@ -9,15 +9,22 @@ #include #include #include +#include #include "dlfcn.h" #include "debug.h" #include "asset.h" +#include "asset_internal.h" +#include "scratch.h" #include "dragonfs.h" #include "dma.h" #include "n64sys.h" +#include "joypad.h" +#include "backtrace_internal.h" +#include "exception_internal.h" #include "rompak_internal.h" #include "utils.h" #include "dlfcn_internal.h" +#include "kernel/ktls_internal.h" /** * @defgroup dl Dynamic linker subsystem @@ -45,6 +52,38 @@ * @{ */ + // Embedded GDB script to auto-load DSO symbols +/// @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 + /** @brief Macro to round up pointer */ #define PTR_ROUND_UP(ptr, d) ((void *)ROUND_UP((uintptr_t)(ptr), (d))) /** @brief Macro to add base to pointer */ @@ -60,8 +99,6 @@ extern void __cxa_finalize(void *dso); /** @brief Demangler function */ demangle_func __dl_demangle_func; -/** @brief Module resolver */ -module_lookup_func __dl_lookup_module; /** @brief Module list head */ dl_module_t *__dl_list_head; /** @brief Module list tail */ @@ -138,7 +175,7 @@ static void load_mainexe_sym_table() { mainexe_sym_info_t __attribute__((aligned(8))) mainexe_sym_info; //Search for main executable symbol table - uint32_t rom_addr = rompak_search_ext(".msym"); + uint32_t rom_addr = rompak_search_ext(".msym", NULL); assertf(rom_addr != 0, "Main executable symbol table not found"); //Read header for main executable symbol table data_cache_hit_writeback_invalidate(&mainexe_sym_info, sizeof(mainexe_sym_info)); @@ -151,6 +188,7 @@ static void load_mainexe_sym_table() } //Read main executable symbol table mainexe_sym_table = malloc(mainexe_sym_info.size); + assertf(mainexe_sym_table, "Out of memory"); data_cache_hit_writeback_invalidate(mainexe_sym_table, mainexe_sym_info.size); dma_read_raw_async(mainexe_sym_table, rom_addr+sizeof(mainexe_sym_info), mainexe_sym_info.size); dma_wait(); @@ -283,6 +321,9 @@ static void flush_module(dso_module_t *module) static void relocate_module(dso_module_t *module) { + //Base for GP-Relative symbols + extern uint8_t _gp; + //Process relocations for(uint32_t i=0; inum_relocs; i++) { dso_reloc_t *reloc = &module->relocs[i]; @@ -344,6 +385,54 @@ static void relocate_module(dso_module_t *module) } break; + case R_MIPS_GPREL16: + { + uint16_t lo = *target & 0xFFFF; //Read lo from instruction + uint32_t gpaddr = (uint32_t)(&_gp); + lo += sym_addr-gpaddr; //Calulate new lo + *target = (*target & 0xFFFF0000)|lo; //Write lo to instruction + } + break; + + case R_MIPS_TLS_TPREL_HI16: + { + uint16_t hi = *target & 0xFFFF; //Read hi from instruction + uint32_t addr = hi << 16; //Setup address from hi + bool lo_found = false; + //Search for next R_MIPS_TLS_TPREL_LO16 relocation + for(uint32_t j=i+1; jnum_relocs; j++) { + dso_reloc_t *new_reloc = &module->relocs[j]; + type = new_reloc->info >> 24; + if(type == R_MIPS_TLS_TPREL_LO16) { + //Pair for R_MIPS_TLS_TPREL_LO16 relocation found + u_uint32_t *lo_target = PTR_DECODE(module->prog_base, new_reloc->offset); + int16_t lo = *lo_target & 0xFFFF; //Read lo from target of paired relocation + //Update address + addr += lo; + addr += sym_addr-TP_OFFSET; + //Calculate hi + hi = addr >> 16; + if(addr & 0x8000) { + //Do hi carry + hi++; + } + lo_found = true; + break; + } + } + assertf(lo_found, "Unpaired R_MIPS_TLS_TPREL_HI16 relocation"); + *target = (*target & 0xFFFF0000)|hi; //Write hi to instruction + } + break; + + case R_MIPS_TLS_TPREL_LO16: + { + uint16_t lo = *target & 0xFFFF; //Read lo from instruction + lo += sym_addr-TP_OFFSET; //Calulate new lo + *target = (*target & 0xFFFF0000)|lo; //Write lo to instruction + } + break; + default: assertf(0, "Unknown relocation type %d", type); break; @@ -351,18 +440,27 @@ static void relocate_module(dso_module_t *module) } } -static void link_module(dso_module_t *module, const char *filename) +static void link_module(dso_module_t *module, const char *filename, void *temp_base) { + uint32_t first_export_sym = module->num_import_syms+1; //Relocate module pointers module->syms = PTR_DECODE(module, module->syms); - module->relocs = PTR_DECODE(module, module->relocs); + module->relocs = PTR_DECODE(temp_base, module->relocs); module->prog_base = PTR_DECODE(module, module->prog_base); module->src_elf = PTR_DECODE(module, module->src_elf); module->filename = PTR_DECODE(module, module->filename); - fixup_sym_names(module->syms, (uint8_t *)module, module->num_syms); + for(uint32_t i=0; inum_syms; i++) { + if(i >= 1 && i < first_export_sym) { + module->syms[i].name = PTR_DECODE(temp_base, module->syms[i].name); + } else { + module->syms[i].name = PTR_DECODE(module, module->syms[i].name); + } + } resolve_syms(module, filename); relocate_module(module); flush_module(module); + module->relocs = NULL; + module->num_relocs = 0; } static void start_module(dl_module_t *handle) @@ -418,9 +516,25 @@ void *dlopen(const char *filename, int mode) //Increment use count handle->ref_count++; } else { - handle = asset_load(filename, NULL); - assertf(handle->magic == DSO_MAGIC, "Invalid DSO file"); - link_module(handle, filename); + dso_file_header_t hdr; + int fd = must_open(filename); + int nread = read(fd, &hdr, sizeof(hdr)); + assertf(nread == sizeof(hdr), "cannot read DSO file: %s", filename); + assertf(hdr.magic == DSO_FILE_MAGIC, "Invalid DSO file: %s", filename); + lseek(fd, hdr.resident_off, SEEK_SET); + int rsz = hdr.resident_size; + handle = asset_loadfd(fd, &rsz); + lseek(fd, hdr.loadtmp_off, SEEK_SET); + int tsz = hdr.loadtmp_size, tbuf_size = 0; + asset_loadfd_into(fd, &tsz, NULL, &tbuf_size); + lseek(fd, hdr.loadtmp_off, SEEK_SET); + tsz = hdr.loadtmp_size; + void *temp = scratch_memalign(ASSET_ALIGNMENT, tbuf_size); + assertf(temp, "Out of memory"); + asset_loadfd_into(fd, &tsz, temp, &tbuf_size); + close(fd); + link_module(handle, filename, temp); + scratch_free(temp); handle->mode = mode; if(strncmp(filename, "rom:/", 5) == 0) { sprintf(handle->filename, "%s.sym", filename+5); @@ -440,7 +554,7 @@ void *dlopen(const char *filename, int mode) strcpy(handle->filename, filename); //Add module handle to list handle->ref_count = 1; - __dl_lookup_module = lookup_module; + __bt_lookup_module = lookup_module; __dl_insert_module(handle); //Start running module start_module(handle); @@ -540,7 +654,7 @@ static void end_module(dl_module_t *module) //Deregister exception frames for this module dso_sym_t *eh_frame_begin = search_module_exports(module, "__EH_FRAME_BEGIN__"); if(eh_frame_begin) { - __register_frame_info((void *)eh_frame_begin->value, module->ehframe_obj); + __deregister_frame_info((void *)eh_frame_begin->value); } } @@ -639,4 +753,36 @@ char *dlerror(void) return error_string; } +/** @brief Inspector page showing loaded modules */ +static void inspector_page_modules(surface_t *disp, exception_t* ex, joypad_buttons_t *key_pressed) +{ + static int module_offset = 0; + + dl_module_t *curr_module = __dl_list_head; + size_t module_idx = 0; + if(key_pressed->d_up && module_offset > 0) { + module_offset--; + } + if(key_pressed->d_down && module_offset+18 < __dl_num_loaded_modules) { + module_offset++; + } + printf("Loaded DSOs (modules)\n\n"); + while(curr_module) { + if(module_idx >= module_offset && module_idx < module_offset+18) { + void *module_min = curr_module->prog_base; + void *module_max = ((uint8_t *)module_min)+curr_module->prog_size; + printf("\aG%s \aT(%p-%p)\n", curr_module->filename, module_min, module_max); + } + curr_module = curr_module->next; + module_idx++; + } +} + +/** @brief Register the inspector page for loaded modules */ +__attribute__((constructor)) +void __dl_register_inspector(void) +{ + __inspector_add_page(inspector_page_modules); +} + /** @} */ \ No newline at end of file diff --git a/src/dlfcn_internal.h b/src/dlfcn_internal.h index 2b1eb7083..28afdc4a4 100644 --- a/src/dlfcn_internal.h +++ b/src/dlfcn_internal.h @@ -1,6 +1,7 @@ /** * @file dlfcn_internal.h * @author gamemasterplc + * @author Giovanni Bajo */ #ifndef __DLFCN_INTERNAL_H #define __DLFCN_INTERNAL_H @@ -30,6 +31,9 @@ typedef uint32_t u_uint32_t __attribute__((aligned(1))); #define R_MIPS_26 4 ///< Jump relocation #define R_MIPS_HI16 5 ///< High half of HI/LO pair #define R_MIPS_LO16 6 ///< Low half of HI/LO pair +#define R_MIPS_GPREL16 7 ///< GP relative 16 bit +#define R_MIPS_TLS_TPREL_HI16 49 ///< TP-relative offset, high 16 bits +#define R_MIPS_TLS_TPREL_LO16 50 ///< TP-relative offset, low 16 bits /** @brief Demangler function */ extern demangle_func __dl_demangle_func; diff --git a/src/dma.c b/src/dma.c index c1d43a1b3..11fba0bfe 100644 --- a/src/dma.c +++ b/src/dma.c @@ -5,251 +5,571 @@ * @brief DMA Controller * @ingroup dma */ +#include #include +#include +#include "dma.h" #include "n64types.h" #include "n64sys.h" +#include "vaddr64.h" #include "interrupt.h" #include "debug.h" #include "utils.h" #include "regsinternal.h" - -/** - * @name PI Status Register Bit Definitions - * @{ - */ -/** @brief PI DMA Busy */ -#define PI_STATUS_DMA_BUSY ( 1 << 0 ) -/** @brief PI IO Busy */ -#define PI_STATUS_IO_BUSY ( 1 << 1 ) -/** @brief PI Error */ -#define PI_STATUS_ERROR ( 1 << 2 ) -/** @} */ +#include "interrupt_internal.h" +#include "kernel/kernel_internal.h" +#include "kirq.h" +#include "mi.h" +#include "accounting_internal.h" /** @brief Structure used to interact with the PI registers */ static volatile struct PI_regs_s * const PI_regs = (struct PI_regs_s *)0xa4600000; +/** @brief Write this to PI_STATUS to acknowledge the PI interrupt */ +#define PI_CLEAR_INTERRUPT 0x02 + +#define MAX_PI_MSGS 16 ///< Maximum number of pending PI DMA messages + +#define PI_STATE_IDLE 0 ///< PI queue state: idle (no DMA in progress) +#define PI_STATE_RUNNING 1 ///< PI queue state: DMA in progress + +#define PI_OP_READ_RAW 0 ///< Read raw bytes from PI bus +#define PI_OP_WRITE_RAW 1 ///< Write raw bytes to PI bus +#define PI_OP_READ_MISALIGNED 2 ///< Read misaligned bytes from PI bus + +#define PI_DMA_MAX_LEN 0x1000000UL ///< Maximum length of a PI DMA transfer +#define PI_TICKET_LEN_BITS 24 ///< Number of bits used to encode the transfer length +#define PI_TICKET_LEN_MASK ((1ULL << PI_TICKET_LEN_BITS) - 1) ///< Mask to extract the transfer length from a ticket +#define PI_TICKET_ID(ticket) ((ticket) >> PI_TICKET_LEN_BITS) ///< Extract the transfer ID from a ticket +#define PI_TICKET_LEN(ticket) (((ticket) & PI_TICKET_LEN_MASK) + 1) ///< Extract the transfer length from a ticket + +/** @brief A PI DMA message in the pending queue */ +typedef struct { + void *ram; ///< RDRAM address + pi_addr_t pi_addr; ///< PI bus address + uint32_t len; ///< Transfer length in bytes + uint8_t type; ///< Operation type (#PI_OP_READ_RAW, etc.) +} pi_msg_t; + +/** @brief Pending PI DMA messages (ring buffer) */ +static pi_msg_t pi_msgs[MAX_PI_MSGS]; +/** @brief Pending messages write index */ +static volatile int pi_msgs_widx; +/** @brief Pending messages read index */ +static volatile int pi_msgs_ridx; +/** @brief PI queue state (#PI_STATE_IDLE or #PI_STATE_RUNNING) */ +static volatile int pi_state; +/** @brief Monotonic ticket counter (assigned at enqueue) */ +static volatile uint64_t pi_ticket_issued; +/** @brief Ticket counter advanced when a transfer starts */ +static volatile uint64_t pi_ticket_started; +/** @brief Ticket counter advanced when a transfer completes */ +static volatile uint64_t pi_ticket_done; + +static void pi_interrupt(void); +static void pi_poll(void); + static volatile int __dma_busy(void) { return PI_regs->status & (PI_STATUS_DMA_BUSY | PI_STATUS_IO_BUSY); } -bool io_accessible(uint32_t pi_address) +static bool __pi_queue_empty(void) { - // Below 0x0500_0000, there is RDRAM and RCP registers. - if (pi_address < 0x05000000) - return false; + return pi_msgs_widx == pi_msgs_ridx; +} - // Using 32-bit addresses (like those available from within our C code), - // the CPU can only access addresses up to 0x1FFF_FFFF. - // FIXME: we could theoretically lift this limit up to 0x7FFF_FFFF if the - // I/O functions were rewritten in assembly using 64-bit addresses, but - // there is currently no known PI peripheral that operates in that range anyway. - if (pi_address > 0x1FFFFFFF) - return false; +static bool __pi_queue_full(void) +{ + return (pi_msgs_widx + 1) % MAX_PI_MSGS == pi_msgs_ridx; +} - // The SI bus is partially covering the PI range in the CPU memory map - if (pi_address >= 0x1FC00000 && pi_address <= 0x1FCFFFFF) - return false; +static void pi_manual_interrupt_poll(void) +{ + // Poll PI interrupts manually. This is required when interrupts are + // disabled (eg: during dma_wait spin loops), as otherwise the queue + // would never make progress. + unsigned long status = *MI_INTERRUPT & *MI_MASK; + if (status & MI_INTERRUPT_PI) { + PI_regs->status = PI_CLEAR_INTERRUPT; + pi_interrupt(); + } +} - // All other addresses are memory mapped and can be accessed via CPU. - return true; +static void pi_start_read_raw(void *ram_address, pi_addr_t pi_address, unsigned long len) +{ + assert(len > 0); + *PI_DRAM_ADDR = PhysicalAddr(ram_address); + *PI_CART_ADDR = pi_address; + *PI_WR_LEN = len - 1; } -/** - * @brief Return whether the DMA controller is currently busy +static void pi_start_write_raw(const void *ram_address, pi_addr_t pi_address, unsigned long len) +{ + assert(len > 0); + *PI_DRAM_ADDR = PhysicalAddr(ram_address); + *PI_CART_ADDR = pi_address; + *PI_RD_LEN = len - 1; +} + +/** + * @brief Read arbitrary bytes from the PI bus using aligned 32-bit CPU reads. * - * @return nonzero if the DMA controller is busy or 0 otherwise + * Sub-word CPU accesses to the PI bus are not reliable. Read aligned words + * through the 64-bit uncached address space and copy out the requested bytes. + * + * @note This function must be called with interrupts disabled and PI idle. */ -volatile int dma_busy(void) +static void pi_cpu_read(void *ram_address, pi_addr_t pi_address, unsigned long len) { - return __dma_busy(); + uint8_t *ram = ram_address; + + while (len) { + unsigned int offset = pi_address & 3; + unsigned int count = MIN(len, 4 - offset); + uint32_t word = sys_vaddr_read32(VirtualUncachedAddr64(pi_address ^ offset)); + const uint8_t *bytes = (const uint8_t*)&word; + memcpy(ram, bytes + offset, count); + + pi_address += count; + len -= count; + ram += count; + } } -void dma_read_raw_async(void * ram_address, unsigned long pi_address, unsigned long len) +/** + * @brief Execute a misaligned read: CPU fixups for the borders, then DMA. + * + * Transfers the misaligned head (up to the next 8-byte aligned RAM address) + * and the trailing odd byte (when the DMA would not handle it) via CPU + * reads, then starts the DMA transfer of the aligned portion, if any. + * + * @note This function must be called with interrupts disabled and PI idle. + * + * @return The length of the DMA portion (0 if the whole transfer was done + * via CPU and no DMA was started). + */ +static unsigned long pi_exec_read_misaligned(const pi_msg_t *msg) { - assert(len > 0); - - disable_interrupts(); + uint8_t *ram = UncachedAddr(msg->ram); + pi_addr_t pi_address = msg->pi_addr; + unsigned long len = msg->len; + + assert(io_accessible(pi_address)); + + // Transfer the first bytes manually up until the next 8-byte aligned + // address. Make sure to not transfer more than requested. + unsigned long head = MIN(len, (-(uintptr_t)ram) & 7); + if (head > 0) { + pi_cpu_read(ram, pi_address, head); + ram += head; + pi_address += head; + len -= head; + } - while (__dma_busy()) ; - MEMORY_BARRIER(); - PI_regs->ram_address = ram_address; - MEMORY_BARRIER(); - PI_regs->pi_address = pi_address; - MEMORY_BARRIER(); - PI_regs->write_length = len-1; - MEMORY_BARRIER(); + // If there's an odd number of bytes left to transfer, check if the DMA + // will do that correctly. This happens only if the transfer fits the + // first DMA block, which is either 127 bytes or up to the end of the + // current RDRAM row (0x800 bytes). + int first_block_len = MIN(127, 0x800 - ((uint32_t)ram & 0x7ff)); + if ((len & 1) && len >= first_block_len) { + // Odd transfers would not work correctly. Transfer the last byte + // manually. + pi_cpu_read(ram+len-1, pi_address+len-1, 1); + len -= 1; + } - enable_interrupts(); + // Start the actual DMA transfer, if still needed. + if (len) + pi_start_read_raw(ram, pi_address, len); + return len; } -void dma_write_raw_async(const void * ram_address, unsigned long pi_address, unsigned long len) +/** + * @brief Mark the current transfer as complete and advance the queue. + * + * @note This function must be called with interrupts disabled. + */ +static void pi_complete_current(void) { - assert(len > 0); + pi_ticket_done++; + pi_msgs_ridx = (pi_msgs_ridx + 1) % MAX_PI_MSGS; + pi_state = PI_STATE_IDLE; +} - disable_interrupts(); +/** + * @brief Start executing a queued PI DMA transfer. + * + * @note This function must be called with interrupts disabled and PI idle. + */ +static void pi_exec_entry(pi_msg_t *msg) +{ + assert((PI_regs->status & (PI_STATUS_DMA_BUSY | PI_STATUS_IO_BUSY)) == 0); + + pi_state = PI_STATE_RUNNING; + + switch (msg->type) { + case PI_OP_READ_RAW: + pi_start_read_raw(msg->ram, msg->pi_addr, msg->len); + break; + + case PI_OP_WRITE_RAW: + pi_start_write_raw(msg->ram, msg->pi_addr, msg->len); + break; + + case PI_OP_READ_MISALIGNED: + // Run the CPU fixups for the borders, then start the DMA of the + // aligned portion. If the whole transfer was done via CPU, complete + // the entry inline (no interrupt will come). + if (pi_exec_read_misaligned(msg) == 0) { + pi_ticket_started++; + pi_complete_current(); + return; + } + break; - while (__dma_busy()) ; - MEMORY_BARRIER(); - PI_regs->ram_address = (void*)ram_address; - MEMORY_BARRIER(); - PI_regs->pi_address = pi_address; - MEMORY_BARRIER(); - PI_regs->read_length = len-1; - MEMORY_BARRIER(); + default: + assertf(false, "Internal error: unknown PI queue entry type"); + } - enable_interrupts(); + // Publish the started state only after all synchronous CPU fixups have + // completed and the hardware DMA has been launched. + pi_ticket_started++; } -/** @brief Low-level 32-bit aligned ROM read. - * +/** + * @brief Check whether there are new messages to send. + * * @note This function must be called with interrupts disabled. */ -static uint32_t __io_read32(void* pi_pointer) { - while (__dma_busy()) {} - MEMORY_BARRIER(); - return *(volatile uint32_t*)pi_pointer; -} - -/** @brief Low-level 16-bit aligned PI ROM read. - * - * 16-bit PI ROM reads are undocumented. Testing on real hardware shows - * that they only work for 32-bit aligned addresses, so this function - * falls back to a full 32bit read for misaligned addresses. - * - * @note This function must be called with interrupts disabled. +static void pi_poll(void) +{ + // Execute entries until one starts an actual DMA. Entries which are + // fully executed via CPU (tiny misaligned reads) complete inline, so + // we must keep going to the next one. + while (pi_state == PI_STATE_IDLE && !__pi_queue_empty() && !__dma_busy()) + pi_exec_entry(&pi_msgs[pi_msgs_ridx]); +} + +/** + * @brief PI interrupt handler. */ -static uint16_t __io_read16(void *pi_pointer) { - uint32_t pi_address = (uint32_t)pi_pointer; - if (pi_address & 2) { - return (uint16_t)__io_read32((void*)(pi_address^2)); - } else { - while (__dma_busy()) {} - MEMORY_BARRIER(); - return *(volatile uint16_t*)pi_pointer; +static void pi_interrupt(void) +{ + if (pi_state == PI_STATE_RUNNING) { + pi_complete_current(); + pi_poll(); + return; } + + // External PI DMA (eg: iQue NAND). Poll the queue in case a transfer + // was enqueued while the PI was busy. + pi_poll(); } +static uint64_t pi_enqueue(uint8_t type, void *ram, pi_addr_t pi_addr, unsigned long len) +{ + assert(len > 0 && len <= PI_DMA_MAX_LEN); + assert(pi_ticket_issued < (1ULL << (64 - PI_TICKET_LEN_BITS)) - 1); -/** @brief Low-level 8-bit PI ROM read. - * - * 8-bit PI ROM reads are undocumented. Testing on real hardware shows - * that they do not consistently work, so this function falls back to using - * 16-bit reads and extracting the requested byte. - * - * @note This function must be called with interrupts disabled. + kirq_wait_t w = kirq_begin_wait_pi(); + + disable_interrupts(); + + // If the queue is full, wait until a slot is freed by the interrupt + // handler. Poll the PI interrupt manually so that this also works when + // the caller has interrupts disabled. + if (UNLIKELY(__pi_queue_full())) { + ACCT_SCOPE(ACCT_CAT_PI) while (UNLIKELY(__pi_queue_full())) { + pi_manual_interrupt_poll(); + if (!__pi_queue_full()) + break; + enable_interrupts(); + if (__kernel) + kirq_wait(&w); + disable_interrupts(); + } + } + + // Write the new message into the ring buffer. + pi_msg_t *msg = &pi_msgs[pi_msgs_widx]; + msg->ram = ram; + msg->pi_addr = pi_addr; + msg->len = len; + msg->type = type; + + pi_msgs_widx = (pi_msgs_widx + 1) % MAX_PI_MSGS; + uint64_t ticket = (++pi_ticket_issued << PI_TICKET_LEN_BITS) | (len - 1); + + // If the PI is idle, start the transfer immediately. For misaligned + // reads, this also runs the CPU fixups right now, in caller context. + if (pi_state == PI_STATE_IDLE && !__dma_busy()) + pi_poll(); + + enable_interrupts(); + return ticket; +} + +bool io_accessible(pi_addr_t pi_address) +{ + // Below 0x0500_0000, there is RDRAM and RCP registers. + if (pi_address < 0x05000000) + return false; + + // The SI bus is partially covering the PI range in the CPU memory map + if (pi_address >= 0x1FC00000 && pi_address <= 0x1FCFFFFF) + return false; + + // Upper half of the PI range is not memory mapped + if (pi_address >= 0x80000000) + return false; + + // All other addresses are memory mapped and can be accessed via CPU. + return true; +} + +/** + * @brief Return whether the DMA controller is currently busy + * + * @return nonzero if the DMA controller is busy or 0 otherwise */ -static uint8_t __io_read8(void *pi_pointer) { - uint32_t pi_address = (uint32_t)pi_pointer; - if (pi_address&1) - return (uint8_t)__io_read16((void*)(pi_address^1)); - else - return __io_read16(pi_pointer)>>8; +volatile int dma_busy(void) +{ + disable_interrupts(); + int busy = !__pi_queue_empty() || __dma_busy(); + enable_interrupts(); + return busy; +} + +uint64_t dma_read_raw_async(void *ram_address, pi_addr_t pi_address, unsigned long len) +{ + return pi_enqueue(PI_OP_READ_RAW, ram_address, pi_address, len); } -void dma_read_async(void *ram_pointer, 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) +{ + return pi_enqueue(PI_OP_WRITE_RAW, (void*)ram_address, pi_address, len); +} + +uint64_t dma_read_async(void *ram_pointer, pi_addr_t pi_address, unsigned long len) { void *ram = UncachedAddr(ram_pointer); uint32_t ram_address = (uint32_t)ram; - void *rom = (void*)(pi_address | 0xA0000000); assert(len > 0); - assert(((ram_address ^ pi_address) & 1) == 0); (void)ram_address; + assert(((ram_address ^ pi_address) & 1) == 0); - disable_interrupts(); - - // Check if the PI address can be accessed with CPU. - // If not, we cannot perform a misaligned transfer. - if (!io_accessible(pi_address)) { + if (!io_accessible(pi_address)) { + // Check if the PI address can be accessed with CPU. + // If not, we cannot perform a misaligned transfer. assertf((pi_address & 2) == 0 && (ram_address & 7) == 0, "misaligned transfer not supported at this PI address"); - dma_read_raw_async(ram_pointer, pi_address, len); - enable_interrupts(); - return; + return dma_read_raw_async(ram_pointer, pi_address, len); } - // Check if the address in RAM is misaligned. - if ((uint32_t)ram & 7) { - // Transfer the first bytes manually up until the next 8-byte aligned - // address. Make sure to not transfer more than requested. - if ((uint32_t)ram & 1) { - *(uint8_t*)ram = __io_read16(rom - 1); - ram++; rom++; len--; - } - if ((uint32_t)ram & 2 && len >= 2) { - *(uint16_t*)ram = __io_read16(rom); - ram += 2; rom += 2; len -= 2; - } - if ((uint32_t)ram & 4 && len >= 4) { - *(uint32_t*)ram = (__io_read16(rom) << 16) | __io_read16(rom+2); - ram += 4; rom += 4; len -= 4; - } - while ((uint32_t)ram & 7 && len > 0) { - *(uint8_t*)ram = __io_read8(rom); - ram++; rom++; len--; - } + // Misaligned reads are executed at dequeue time: the CPU fixups for the + // borders run either right now in caller context (if the queue is empty + // and PI is idle) or in the interrupt handler when the entry is dequeued. + if ((ram_address & 7) || ((len & 1) && len >= MIN(127, 0x800 - (ram_address & 0x7ff)))) + return pi_enqueue(PI_OP_READ_MISALIGNED, ram_pointer, pi_address, len); + + return dma_read_raw_async(ram_pointer, pi_address, len); +} + +bool dma_finished(uint64_t ticket) +{ + disable_interrupts(); + // Poll the PI interrupt manually, so that this function makes progress + // even when called with interrupts disabled. + pi_manual_interrupt_poll(); + bool done = ticket && PI_TICKET_ID(ticket) <= pi_ticket_done; + enable_interrupts(); + return done; +} + +static bool dma_started(uint64_t ticket) +{ + disable_interrupts(); + // Poll the PI interrupt manually, so that this function makes progress + // even when called with interrupts disabled. + pi_manual_interrupt_poll(); + bool started = ticket && PI_TICKET_ID(ticket) <= pi_ticket_started; + enable_interrupts(); + return started; +} + +unsigned long dma_get_progress(uint64_t ticket) +{ + if (!ticket) + return 0; + + disable_interrupts(); + // Poll the PI interrupt manually, so that this function makes progress + // even when called with interrupts disabled. + pi_manual_interrupt_poll(); + + uint64_t id = PI_TICKET_ID(ticket); + unsigned long len = PI_TICKET_LEN(ticket); + unsigned long progress = 0; + + if (id <= pi_ticket_done) { + // The PI registers might already refer to a later transfer. + progress = len; + } else if (id == pi_ticket_started) { + // This is the transfer currently running. PI_DRAM_ADDR tracks the + // current RDRAM pointer for both reads and writes. + pi_msg_t *msg = &pi_msgs[pi_msgs_ridx]; + uint32_t start = PhysicalAddr(msg->ram); + uint32_t current = *PI_DRAM_ADDR; + if (current >= start) + progress = MIN(current - start, len); } - // If there's an odd number of bytes left to transfer, check if the DMA - // will do that correctly. This happens only if the transfers fits the - // first DMA block, which is either 127 bytes or up to the end of the - // current RDRAM row (0x800 bytes). - int first_block_len = MIN(127, 0x800 - ((uint32_t)ram & 0x7ff)); - if ((len & 1) && len >= first_block_len) { - // Odd transfers would not work correctly. Transfer the last byte - // manually. - *(uint8_t*)(ram+len-1) = __io_read16(rom+len-1) >> 8; - len -= 1; + enable_interrupts(); + return progress; +} + +void dma_wait_started(uint64_t ticket) +{ + assert(ticket); + + kirq_wait_t w = kirq_begin_wait_pi(); + + // Keep the common case outside the accounting scope. + if (LIKELY(dma_started(ticket))) + return; + + ACCT_SCOPE(ACCT_CAT_PI) while (!dma_started(ticket)) { + if (__kernel) + kirq_wait(&w); } +} - // Start the actual DMA transfer, if still needed. - if (len) - dma_read_raw_async(ram, PhysicalAddr(rom), len); +void dma_wait_finished(uint64_t ticket) +{ + assert(ticket); - enable_interrupts(); + kirq_wait_t w = kirq_begin_wait_pi(); + + // Keep the common case outside the accounting scope. + if (LIKELY(dma_finished(ticket))) + return; + + ACCT_SCOPE(ACCT_CAT_PI) while (!dma_finished(ticket)) { + if (__kernel) + kirq_wait(&w); + } } -void dma_wait(void) +__attribute__((noinline, warn_unused_result)) +static uint32_t dma_wait_and_disable_interrupts(void) { - while (__dma_busy()) {} + kirq_wait_t w = kirq_begin_wait_pi(); + uint32_t sr = __disable_interrupts(); + + // Poll the PI interrupt manually, so that this function makes progress + // even when called with interrupts disabled. + pi_manual_interrupt_poll(); + if (LIKELY(__pi_queue_empty() && !__dma_busy())) + return sr; + __enable_interrupts(sr); + + ACCT_SCOPE(ACCT_CAT_PI) while (1) { + if (__kernel) + kirq_wait(&w); + + sr = __disable_interrupts(); + pi_manual_interrupt_poll(); + if (__pi_queue_empty() && !__dma_busy()) + break; + __enable_interrupts(sr); + } + return sr; } +void dma_wait(void) +{ + uint32_t sr = dma_wait_and_disable_interrupts(); + __enable_interrupts(sr); +} -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) { + // HORROR: this code makes no sense, but it's always been here. The original + // goal was to convert virtual addresses to PI addresses, but it is also + // preventing a large span of the PI address space from being used. pi_address = (pi_address | 0x10000000) & 0x1FFFFFFF; dma_read_async(ram_address, pi_address, len); dma_wait(); } -void dma_write(const void * ram_address, unsigned long rom_address, unsigned long len) +void dma_write(const void * ram_address, pi_addr_t rom_address, unsigned long len) { + // HORROR: this code makes no sense, but it's always been here. The original + // goal was to make virtual addresses to PI addresses, but it is also + // preventing a large span of the PI address space from being used. rom_address = (rom_address | 0x10000000) & 0x1FFFFFFF; dma_write_raw_async(ram_address, rom_address, len); dma_wait(); } -uint32_t io_read(uint32_t pi_address) +uint32_t io_read(pi_addr_t pi_address) { - uint32_t retval; + // HACK: to maintain backward compatibility, this function used to accept + // also CPU virtual addresses. To still allow for that, we need to convert + // them to physical addresses first. + if (UNLIKELY(pi_address >= 0x80000000 && pi_address <= 0xBFFFFFFF)) { + debugf("io_read: WARNING: deprecated usage of virtual address: %08lX\n", pi_address); + pi_address = PhysicalAddr((void*)pi_address); + } - disable_interrupts(); - retval = __io_read32((void*)(pi_address | 0xA0000000)); - enable_interrupts(); + // Convert the PI address into a 64-bit virtual address, which allows a wider + // range of PI addresses to be accessed. + vaddr64_t va64 = VirtualUncachedAddr64(pi_address); + + uint32_t sr = dma_wait_and_disable_interrupts(); + uint32_t retval = sys_vaddr_read32(va64); + __enable_interrupts(sr); return retval; } -void io_write(uint32_t pi_address, uint32_t data) +void io_write(pi_addr_t pi_address, uint32_t data) { - volatile uint32_t *uncached_address = (uint32_t *)(pi_address | 0xa0000000); - - disable_interrupts(); + // HACK: to maintain backward compatibility, this function used to accept + // also CPU virtual addresses. To still allow for that, we need to convert + // them to physical addresses first. Keep this undocumented though, as we + // want to deprecate this behavior. + if (UNLIKELY(pi_address >= 0x80000000 && pi_address <= 0xBFFFFFFF)) { + debugf("io_write: WARNING: deprecated usage of virtual address: %08lX\n", pi_address); + pi_address = PhysicalAddr((void*)pi_address); + } - while (__dma_busy()) ; - MEMORY_BARRIER(); - *uncached_address = data; - MEMORY_BARRIER(); + // Convert the PI address into a 64-bit virtual address, which allows a wider + // range of PI addresses to be accessed. + vaddr64_t va64 = VirtualUncachedAddr64(pi_address); - enable_interrupts(); + uint32_t sr = dma_wait_and_disable_interrupts(); + sys_vaddr_write32(va64, data); + __enable_interrupts(sr); } + +/** + * @brief Initialize the PI DMA queue subsystem. + * + * NOTE: the DMA subsystem requires no explicit init function from the user. + * This constructor runs before main(). + */ +__attribute__((constructor(130))) +void __dma_init(void) +{ + pi_msgs_widx = 0; + pi_msgs_ridx = 0; + pi_state = PI_STATE_IDLE; + pi_ticket_issued = 0; + pi_ticket_started = 0; + pi_ticket_done = 0; + + register_PI_handler(pi_interrupt); + set_PI_interrupt(1); +} \ No newline at end of file diff --git a/src/dragonfs.c b/src/dragonfs.c index ea29cfb45..03c81094c 100644 --- a/src/dragonfs.c +++ b/src/dragonfs.c @@ -1,6 +1,7 @@ /** * @file dragonfs.c * @author Jennifer Taylor + * @author Giovanni Bajo * @brief DragonFS * @ingroup dfs */ @@ -11,12 +12,13 @@ #include #include #include +#include #include "dragonfs.h" #include "n64sys.h" #include "dma.h" #include "debug.h" #include "system.h" -#include "dfsinternal.h" +#include "dfs_internal.h" #include "rompak_internal.h" #include "utils.h" @@ -28,7 +30,9 @@ enum /** @brief Walk the directory structure for the purpose of changing directories */ WALK_CHDIR, /** @brief Walk the directory structure for the purpose of opening a file or directory */ - WALK_OPEN + WALK_OPEN, + /** @brief Walk the directory structure for the purpose of inspecting a file or directory */ + WALK_STAT }; /** @@ -45,17 +49,17 @@ enum }; /** @brief Base filesystem pointer */ -static uint32_t base_ptr = 0; +static pi_addr_t base_ptr = 0; /** @brief Base pointer for lookup data */ static dfs_lookup_t *lookup; /** @brief Base ROM address for path data */ static uint32_t lookup_path_ofs; /** @brief Directory pointer stack */ -static uint32_t directories[MAX_DIRECTORY_DEPTH]; +static pi_addr_t directories[MAX_DIRECTORY_DEPTH]; /** @brief Depth into directory pointer stack */ static uint32_t directory_top = 0; /** @brief Pointer to next directory entry set when doing a directory walk */ -static directory_entry_t *next_entry = 0; +static pi_addr_t next_entry = 0; /** @brief Convert an open file pointer to a handle */ #define OPENFILE_TO_HANDLE(file) ((int)PhysicalAddr(file)) /** @brief Convert a handle to an open file pointer */ @@ -72,63 +76,11 @@ static directory_entry_t *next_entry = 0; * @param[out] ram_loc * Pointer to RAM buffer to place the read sector */ -static inline void grab_sector(void *cart_loc, void *ram_loc) +static inline void grab_sector(pi_addr_t cart_loc, void *ram_loc) { /* Make sure we have fresh cache */ - data_cache_hit_writeback_invalidate(ram_loc, SECTOR_SIZE); - - dma_read((void *)(((uint32_t)ram_loc) & 0x1FFFFFFF), (uint32_t)cart_loc, SECTOR_SIZE); -} - -/** - * @brief Look up a sector number based on offset - * - * Given a byte offset from the start of a filesystem, this - * function will return the sector that this byte offset falls - * into. - * - * @param[in] loc - * Offset in bytes - * - * @return The sector number corresponding to the offset - */ -static inline int sector_from_loc(uint32_t loc) -{ - return (loc / SECTOR_PAYLOAD); -} - -/** - * @brief Look up a byte offset into a sector - * - * Given a byte offset from the start of a filesystem, this - * function will return the offset into the current sector. - * This essentially clamps the ouput from 0 to #SECTOR_PAYLOAD. - * - * @param[in] loc - * Offset in bytes - * - * @return The offset into a sector - */ -static inline int offset_into_sector(uint32_t loc) -{ - return loc % SECTOR_PAYLOAD; -} - -/** - * @brief Look up the remaining data size in a sector - * - * Given a byte offset from the start of a filesystem, this - * function will return the number of bytes from the current - * location to the end of the sector. - * - * @param[in] loc - * Offset in bytes - * - * @return The number of bytes left in a sector based on an offset - */ -static inline int data_left_in_sector(uint32_t loc) -{ - return SECTOR_PAYLOAD - offset_into_sector(loc); + data_cache_hit_writeback_invalidate(ram_loc, MAX_DIRENT_SIZE); + dma_read(ram_loc, cart_loc, MAX_DIRENT_SIZE); } /** @@ -168,9 +120,9 @@ static inline uint32_t get_size(directory_entry_t *dirent) * * @return A pointer to the directory represented by the directory entry. */ -static inline directory_entry_t *get_first_entry(directory_entry_t *dirent) +static inline pi_addr_t get_first_entry(directory_entry_t *dirent) { - return (directory_entry_t *)(dirent->file_pointer ? (dirent->file_pointer + base_ptr) : 0); + return (pi_addr_t)(dirent->file_pointer ? (dirent->file_pointer + base_ptr) : 0); } /** @@ -181,9 +133,9 @@ static inline directory_entry_t *get_first_entry(directory_entry_t *dirent) * * @return A pointer to the next directory entry after the current directory entry. */ -static inline directory_entry_t *get_next_entry(directory_entry_t *dirent) +static inline pi_addr_t get_next_entry(directory_entry_t *dirent) { - return (directory_entry_t *)(dirent->next_entry ? (dirent->next_entry + base_ptr) : 0); + return (pi_addr_t)(dirent->next_entry ? (dirent->next_entry + base_ptr) : 0); } /** @@ -197,7 +149,7 @@ static inline directory_entry_t *get_next_entry(directory_entry_t *dirent) * * @return A location of the start of the file. */ -static inline uint32_t get_start_location(directory_entry_t *dirent) +static inline pi_addr_t get_start_location(directory_entry_t *dirent) { return (dirent->file_pointer ? (dirent->file_pointer + base_ptr) : 0); } @@ -205,7 +157,7 @@ static inline uint32_t get_start_location(directory_entry_t *dirent) /** * @brief Reset the directory stack to the root */ -static inline void clear_directory() +static inline void clear_directory(void) { directory_top = 0; } @@ -216,12 +168,12 @@ static inline void clear_directory() * @param[in] dirent * Directory entry to push onto the stack */ -static inline void push_directory(directory_entry_t *dirent) +static inline void push_directory(pi_addr_t dirent) { if(directory_top < MAX_DIRECTORY_DEPTH) { /* Order of execution for assignment undefined in C, lets force it */ - directories[directory_top] = (uint32_t)dirent; + directories[directory_top] = dirent; directory_top++; } @@ -232,18 +184,18 @@ static inline void push_directory(directory_entry_t *dirent) * * @return The directory entry on the top of the stack */ -static inline directory_entry_t *pop_directory() +static inline pi_addr_t pop_directory(void) { if(directory_top > 0) { /* Order of execution for assignment undefined in C */ directory_top--; - return (directory_entry_t *)directories[directory_top]; + return directories[directory_top]; } /* Just return the root pointer */ - return (directory_entry_t *)(base_ptr + SECTOR_SIZE); + return base_ptr + ID_DIRENT_SIZE; } /** @@ -251,14 +203,14 @@ static inline directory_entry_t *pop_directory() * * @return The directory entry on the top of the stack */ -static inline directory_entry_t *peek_directory() +static inline pi_addr_t peek_directory() { if(directory_top > 0) { - return (directory_entry_t *)directories[directory_top-1]; + return directories[directory_top-1]; } - return (directory_entry_t *)(base_ptr + SECTOR_SIZE); + return base_ptr + ID_DIRENT_SIZE; } /** @@ -369,25 +321,26 @@ static char *get_next_token(char *path, char *token) * @param[in] cur_node * Directory entry to start search from * - * @return The directory entry matching the name requested or NULL if not found. + * @return The address of the directory entry matching the name requested or 0 if not found. */ -static directory_entry_t *find_dirent(char *name, directory_entry_t *cur_node) +static pi_addr_t find_dirent(char *name, pi_addr_t cur_node) { + directory_entry_t* node = alloca(MAX_DIRENT_SIZE); + while(cur_node) { /* Fetch sector off of 'disk' */ - directory_entry_t node; - grab_sector(cur_node, &node); + grab_sector(cur_node, node); /* Do a string comparison on the filename */ - if(strcmp(node.path, name) == 0) + if(strcmp(node->path, name) == 0) { /* We have a match! */ return cur_node; } /* Follow linked list */ - cur_node = get_next_entry(&node); + cur_node = get_next_entry(node); } /* Couldn't find entry */ @@ -406,13 +359,17 @@ static directory_entry_t *find_dirent(char *name, directory_entry_t *cur_node) * itself is returned. If it is a directory, the directory entry of the first * file or directory inside that directory is returned. * + * If mode is WALK_STAT, the result of this function is the same as WALK_OPEN, + * except directories return their own directory entry, instead of the first + * file or directory inside that directory. + * * The type specifier allows a person to specify that only a directory or file - * should be returned. This works for WALK_OPEN only. + * should be returned. This works for WALK_OPEN and WALK_STAT only. * * @param[in] path * The path to walk through * @param[in] mode - * Either #WALK_CHDIR or #WALK_OPEN. + * Either #WALK_CHDIR, #WALK_OPEN or #WALK_STAT. * @param[in,out] dirent * Directory entry to start at, directory entry finished at * @param[in] type @@ -420,7 +377,7 @@ static directory_entry_t *find_dirent(char *name, directory_entry_t *cur_node) * * @return DFS_ESUCCESS on successful recurse, or a negative error on failure. */ -static int recurse_path(const char * const path, int mode, directory_entry_t **dirent, int type) +static int recurse_path(const char * const path, int mode, pi_addr_t *dirent, int type) { int ret = DFS_ESUCCESS; char token[MAX_FILENAME_LEN+1]; @@ -474,20 +431,29 @@ static int recurse_path(const char * const path, int mode, directory_entry_t **d else { /* Find directory entry, push */ - directory_entry_t *tmp_node = find_dirent(token, peek_directory()); + pi_addr_t tmp_node = find_dirent(token, peek_directory()); if(tmp_node) { /* Grab node, make sure it is a directory, push subdirectory, try again! */ - directory_entry_t node; - grab_sector(tmp_node, &node); + directory_entry_t* node = alloca(MAX_DIRENT_SIZE); + grab_sector(tmp_node, node); - uint32_t flags = get_flags(&node); + uint32_t flags = get_flags(node); if(FILETYPE(flags) == FLAGS_DIR) { - /* Push subdirectory onto stack and loop */ - push_directory(get_first_entry(&node)); + /* Only perform special handling if this is the last thing we are doing */ + if(mode == WALK_STAT && !cur_path) + { + /* Push current directory onto stack in preparation of a return */ + push_directory(tmp_node); + } + else + { + /* Push subdirectory onto stack and loop */ + push_directory(get_first_entry(node)); + } last_type = TYPE_DIR; } else @@ -532,7 +498,7 @@ static int recurse_path(const char * const path, int mode, directory_entry_t **d ret = DFS_ENOFILE; } - if(mode == WALK_OPEN) + if(mode == WALK_OPEN || mode == WALK_STAT) { /* Must return the node found if we found one */ if(ret == DFS_ESUCCESS && dirent) @@ -541,7 +507,7 @@ static int recurse_path(const char * const path, int mode, directory_entry_t **d } } - if(mode == WALK_OPEN || ret != DFS_ESUCCESS) + if(mode == WALK_OPEN || mode == WALK_STAT || ret != DFS_ESUCCESS) { /* Restore stack */ directory_top = dir_loc; @@ -554,12 +520,13 @@ static int recurse_path(const char * const path, int mode, directory_entry_t **d static bool init_dfs_lookup(directory_entry_t *id_node) { - uint32_t romaddr = get_start_location(id_node); + pi_addr_t romaddr = get_start_location(id_node); if(romaddr == 0) { return false; } uint32_t size = id_node->next_entry; lookup = malloc(size); + assertf(lookup, "Out of memory"); data_cache_hit_writeback_invalidate(lookup, size); dma_read(lookup, romaddr, size); lookup_path_ofs = base_ptr+lookup->path_ofs; @@ -574,19 +541,19 @@ static bool init_dfs_lookup(directory_entry_t *id_node) * * @return DFS_ESUCCESS on successful initialization or a negative error on failure. */ -static int __dfs_init(uint32_t base_fs_loc) +static int __dfs_init(pi_addr_t base_fs_loc) { /* Check to see if it passes the check */ - directory_entry_t id_node; - grab_sector((void *)base_fs_loc, &id_node); + directory_entry_t* id_node = alloca(MAX_DIRENT_SIZE); + grab_sector(base_fs_loc, id_node); - if(id_node.flags == ROOT_FLAGS && !strcmp(id_node.path, ROOT_PATH)) + if(id_node->flags == ROOT_FLAGS && !strcmp(id_node->path, ROOT_PATH)) { /* Passes, set up the FS */ base_ptr = base_fs_loc; clear_directory(); /* Initialize lookup data */ - return (init_dfs_lookup(&id_node)) ? DFS_ESUCCESS : DFS_EBADFS; + return (init_dfs_lookup(id_node)) ? DFS_ESUCCESS : DFS_EBADFS; } /* Failed! */ @@ -622,9 +589,9 @@ int dfs_chdir(const char * const path) return __dfs_chdir(path); } -static int __dfs_findfirst(const char * const path, char *buf, directory_entry_t **next_entry) +static int __dfs_findfirst(const char * const path, char *buf, pi_addr_t *next_entry) { - directory_entry_t *dirent; + pi_addr_t dirent; int ret = recurse_path(path, WALK_OPEN, &dirent, TYPE_DIR); /* Ensure that if this fails, they can't call findnext */ @@ -636,22 +603,28 @@ static int __dfs_findfirst(const char * const path, char *buf, directory_entry_t return ret; } + if(!dirent) + { + /* Directory exists but has no entries */ + return FLAGS_EOF; + } + /* We now have the pointer to the first entry */ - directory_entry_t t_node; - grab_sector(dirent, &t_node); + directory_entry_t* t_node = alloca(MAX_DIRENT_SIZE); + grab_sector(dirent, t_node); if(buf) { - strcpy(buf, t_node.path); + strcpy(buf, t_node->path); } /* Set up directory to point to next entry */ - *next_entry = get_next_entry(&t_node); + *next_entry = get_next_entry(t_node); - return get_flags(&t_node); + return get_flags(t_node); } -static int __dfs_findnext(char *buf, directory_entry_t **next_entry) +static int __dfs_findnext(char *buf, pi_addr_t *next_entry) { if(!*next_entry) { @@ -660,18 +633,18 @@ static int __dfs_findnext(char *buf, directory_entry_t **next_entry) } /* We already calculated the pointer, just grab the information */ - directory_entry_t t_node; - grab_sector(*next_entry, &t_node); + directory_entry_t *t_node = alloca(MAX_DIRENT_SIZE); + grab_sector(*next_entry, t_node); if(buf) { - strcpy(buf, t_node.path); + strcpy(buf, t_node->path); } /* Set up directory to point to next entry */ - *next_entry = get_next_entry(&t_node); + *next_entry = get_next_entry(t_node); - return get_flags(&t_node); + return get_flags(t_node); } /** @@ -799,7 +772,7 @@ int dfs_open(const char *path) file->cart_start_loc = base_ptr+entry->data_ofs; } else { /* Try to find file */ - directory_entry_t *dirent; + pi_addr_t dirent; int ret = recurse_path(path, WALK_OPEN, &dirent, TYPE_FILE); if(ret != DFS_ESUCCESS) @@ -817,13 +790,13 @@ int dfs_open(const char *path) } /* We now have the pointer to the file entry */ - directory_entry_t t_node; - grab_sector(dirent, &t_node); + directory_entry_t *t_node = alloca(MAX_DIRENT_SIZE); + grab_sector(dirent, t_node); /* Set up file handle */ - file->size = get_size(&t_node); + file->size = get_size(t_node); file->loc = 0; - file->cart_start_loc = get_start_location(&t_node); + file->cart_start_loc = get_start_location(t_node); } return OPENFILE_TO_HANDLE(file); } @@ -1016,7 +989,7 @@ int dfs_size(uint32_t handle) return file->size; } -uint32_t dfs_rom_addr(const char *path) +pi_addr_t dfs_rom_addr(const char *path) { //Skip initial slash if(path[0] == '/') { @@ -1032,7 +1005,7 @@ uint32_t dfs_rom_addr(const char *path) return base_ptr+entry->data_ofs; } else { /* Try to find file */ - directory_entry_t *dirent; + pi_addr_t dirent; int ret = recurse_path(path, WALK_OPEN, &dirent, TYPE_FILE); if(ret != DFS_ESUCCESS) @@ -1042,11 +1015,11 @@ uint32_t dfs_rom_addr(const char *path) } /* We now have the pointer to the file entry */ - directory_entry_t t_node; - grab_sector(dirent, &t_node); + directory_entry_t *t_node = alloca(MAX_DIRENT_SIZE); + grab_sector(dirent, t_node); /* Return the starting location in ROM */ - return get_start_location(&t_node); + return get_start_location(t_node); } } @@ -1071,7 +1044,7 @@ int dfs_rom_size(const char *path) else { /* Try to find file */ - directory_entry_t *dirent; + pi_addr_t dirent; int ret = recurse_path(path, WALK_OPEN, &dirent, TYPE_FILE); if(ret != DFS_ESUCCESS) @@ -1081,11 +1054,11 @@ int dfs_rom_size(const char *path) } /* We now have the pointer to the file entry */ - directory_entry_t t_node; - grab_sector(dirent, &t_node); + directory_entry_t *t_node = alloca(MAX_DIRENT_SIZE); + grab_sector(dirent, t_node); /* Return the starting location in ROM */ - return (int)(get_size(&t_node)); + return (int)(get_size(t_node)); } } @@ -1109,6 +1082,24 @@ int dfs_eof(uint32_t handle) return 0; } +/** + * @brief Set the errno variable based on the error code + * + * @param[in] error + * Error code to set the errno variable to + */ +static void __dfs_set_errno(int error) +{ + switch (error) { + case DFS_EBADINPUT: errno = EINVAL; break; + case DFS_ENOFILE: errno = ENOENT; break; + case DFS_EBADFS: errno = ENODEV; break; + case DFS_ENFILE: errno = ENFILE; break; + case DFS_EBADHANDLE: errno = EBADF; break; + default: errno = EPERM; break; + } +} + /** * @brief Newlib-compatible open * @@ -1124,17 +1115,16 @@ static void *__open( char *name, int flags ) /* Always want a consistent interface */ __dfs_chdir("/"); + /* DragonFS only supports read-only access */ + if ((flags & O_ACCMODE) != O_RDONLY) { + errno = EACCES; + return NULL; + } + /* We disregard flags here */ int handle = dfs_open( name ); if (handle <= 0) { - switch (handle) { - case DFS_EBADINPUT: errno = EINVAL; break; - case DFS_ENOFILE: errno = ENOENT; break; - case DFS_EBADFS: errno = ENODEV; break; - case DFS_ENFILE: errno = ENFILE; break; - case DFS_EBADHANDLE: errno = EBADF; break; - default: errno = EPERM; break; - } + __dfs_set_errno(handle); return NULL; } return (void *)handle; @@ -1152,24 +1142,55 @@ static void *__open( char *name, int flags ) */ static int __fstat( void *file, struct stat *st ) { - st->st_dev = 0; - st->st_ino = 0; + memset(st, 0, sizeof(struct stat)); st->st_mode = S_IFREG; st->st_nlink = 1; - st->st_uid = 0; - st->st_gid = 0; - st->st_rdev = 0; st->st_size = dfs_size( (uint32_t)file ); - st->st_atime = 0; - st->st_mtime = 0; - st->st_ctime = 0; - st->st_blksize = 0; - st->st_blocks = 0; - //st->st_attr = S_IAREAD | S_IAREAD; - return 0; } +/** + * @brief Newlib-compatible stat + * + * @param[in] file + * File name of the file in question + * @param[out] st + * Stat structure to populate + * + * @return 0 on success or a negative value on error. + */ +static int __stat( char *name, struct stat *st ) +{ + pi_addr_t dirent; + int ret = recurse_path(name, WALK_STAT, &dirent, TYPE_ANY); + + if(ret != DFS_ESUCCESS) { + /* File not found, or other error */ + __dfs_set_errno(ret); + return -1; + } + + /* Initialize the stat structure */ + memset(st, 0, sizeof(struct stat)); + st->st_nlink = 1; + + /* We now have the pointer to the first entry */ + directory_entry_t *t_node = alloca(MAX_DIRENT_SIZE); + grab_sector(dirent, t_node); + + /* Populate the structure with the proper values */ + uint32_t flags = get_flags(t_node); + if (FILETYPE(flags) == FLAGS_FILE) { + st->st_mode = S_IFREG; + st->st_size = (int)(get_size(t_node)); + } else { + st->st_mode = S_IFDIR; + st->st_size = 0; + } + + return DFS_ESUCCESS; +} + /** * @brief Newlib-compatible lseek * @@ -1184,7 +1205,11 @@ static int __fstat( void *file, struct stat *st ) */ static int __lseek( void *file, int ptr, int dir ) { - dfs_seek( (uint32_t)file, ptr, dir ); + int err = dfs_seek( (uint32_t)file, ptr, dir ); + if (err != DFS_ESUCCESS) { + __dfs_set_errno(err); + return -1; + } return dfs_tell( (uint32_t)file ); } @@ -1203,7 +1228,12 @@ static int __lseek( void *file, int ptr, int dir ) */ static int __read( void *file, uint8_t *ptr, int len ) { - return dfs_read( ptr, 1, len, (uint32_t)file ); + int err = dfs_read( ptr, 1, len, (uint32_t)file ); + if (err < 0) { + __dfs_set_errno(err); + return -1; + } + return err; } /** @@ -1216,7 +1246,12 @@ static int __read( void *file, uint8_t *ptr, int len ) */ static int __close( void *file ) { - return dfs_close( (uint32_t)file ); + int err = dfs_close( (uint32_t)file ); + if (err != DFS_ESUCCESS) { + __dfs_set_errno(err); + return -1; + } + return 0; } /** @@ -1227,30 +1262,26 @@ static int __close( void *file ) * @param[out] dir * Directory structure to populate with information on the first entry found * - * @return 0 on success or a negative value on failure. + * @return 0 on successful lookup, -1 if the directory existed and is empty, + * or a different negative value on error (in which case, errno will be set). */ static int __findfirst( char *path, dir_t *dir ) { /* Grab first entry, return if bad */ - int flags = __dfs_findfirst( path, dir->d_name, (struct directory_entry**)&dir->d_cookie ); - if( flags < 0 ) { return -1; } - - if( flags == FLAGS_FILE ) - { - dir->d_type = DT_REG; - } - else if( flags == FLAGS_DIR ) - { - dir->d_type = DT_DIR; - } - else - { - /* Unknown type */ - return -1; + int flags = __dfs_findfirst( path, dir->d_name, &dir->d_cookie ); + switch (flags) { + case FLAGS_FILE: + dir->d_type = DT_REG; + return 0; + case FLAGS_DIR: + dir->d_type = DT_DIR; + return 0; + case FLAGS_EOF: + return -1; + default: + __dfs_set_errno(flags); + return -2; } - - /* Success */ - return 0; } /** @@ -1261,28 +1292,19 @@ static int __findfirst( char *path, dir_t *dir ) * * @return 0 on success or a negative value on failure. */ -static int __findnext( dir_t *dir ) +static int __findnext( const char * path, dir_t *dir ) { - /* Grab first entry, return if bad */ - int flags = __dfs_findnext( dir->d_name, (struct directory_entry**)&dir->d_cookie ); - if( flags < 0 ) { return -1; } - - if( flags == FLAGS_FILE ) - { - dir->d_type = DT_REG; - } - else if( flags == FLAGS_DIR ) - { - dir->d_type = DT_DIR; - } - else - { - /* Unknown type */ - return -1; + switch (__dfs_findnext( dir->d_name, &dir->d_cookie )) { + case FLAGS_FILE: + dir->d_type = DT_REG; + return 0; + case FLAGS_DIR: + dir->d_type = DT_DIR; + return 0; + default: + case FLAGS_EOF: + return -1; } - - /* Success */ - return 0; } static int __ioctl(void *file, unsigned long cmd, void *argp) @@ -1290,9 +1312,9 @@ static int __ioctl(void *file, unsigned long cmd, void *argp) switch(cmd) { case IODFS_GET_ROM_BASE: { - uint32_t *rom_addr = argp; + pi_addr_t *rom_addr = argp; dfs_open_file_t *openfile = HANDLE_TO_OPENFILE(file); - *rom_addr = openfile->cart_start_loc & 0x1FFFFFFF; + *rom_addr = openfile->cart_start_loc; } return 0; @@ -1311,11 +1333,12 @@ static int __ioctl(void *file, unsigned long cmd, void *argp) static filesystem_t dragon_fs = { .open = __open, .fstat = __fstat, + .stat = __stat, .lseek = __lseek, .read = __read, .close = __close, .findfirst = __findfirst, - .findnext = __findnext, + .findnext2 = __findnext, .ioctl = __ioctl }; @@ -1344,7 +1367,7 @@ static void __dfs_check_emulation(void) assertf(0, "Your emulator is not accurate enough to run this ROM.\nSpecifically, it doesn't support accurate PI DMA"); } -int dfs_init(uint32_t base_fs_loc) +int dfs_init(pi_addr_t base_fs_loc) { /* Detect if we are running on emulator accurate enough to emulate DragonFS. */ __dfs_check_emulation(); @@ -1352,7 +1375,7 @@ int dfs_init(uint32_t base_fs_loc) if( base_fs_loc == DFS_DEFAULT_LOCATION ) { /* Search for the DFS image location in the ROM */ - base_fs_loc = rompak_search_ext( ".dfs" ); + base_fs_loc = rompak_search_ext( ".dfs", NULL ); if( !base_fs_loc ) { /* We could not find the DragonFS via rompak. @@ -1360,8 +1383,14 @@ int dfs_init(uint32_t base_fs_loc) * to hardcode as default. */ base_fs_loc = 0x10101000; } - /* Convert the address to virtual (as expected for base_fs_loc). */ - base_fs_loc |= 0xA0000000; + } + else if ( base_fs_loc >= 0xB0000000 && base_fs_loc <= 0xBFFFFFFF ) + { + /* Historically, we accepts base_fs_loc as virtual address. Keep allowing + that for backward compatibility. */ + debugf("dfs_init: WARNING: base_fs_loc is a virtual address: %08lX\n" + "Please update your code to use a PI addresses instead.\n", base_fs_loc); + base_fs_loc -= 0xA0000000; } /* Try opening the filesystem */ diff --git a/src/dso_format.h b/src/dso_format.h index 281228ace..9d91a8695 100644 --- a/src/dso_format.h +++ b/src/dso_format.h @@ -1,6 +1,7 @@ /** * @file dso_format.h * @author gamemasterplc + * @author Giovanni Bajo */ #ifndef __DSO_FORMAT_H #define __DSO_FORMAT_H @@ -8,15 +9,27 @@ #include #include -/** @brief DSO magic number */ -#define DSO_MAGIC 0x44534F30 //'DSO0' +/** @brief DSO file container magic number */ +#define DSO_FILE_MAGIC 0x44534F31 //'DSO1' /** @brief Main executable symbol table magic */ #define DSO_MAINEXE_SYM_DATA_MAGIC 0x4D53594D //'MSYM' +/** @brief On-disk DSO1 file header size */ +#define DSO_FILE_HEADER_SIZE 20 + +/** @brief DSO1 on-disk file header */ +typedef struct dso_file_header_s { + uint32_t magic; ///< #DSO_FILE_MAGIC + uint32_t resident_off; ///< Offset of resident asset blob + uint32_t resident_size; ///< Size of resident asset blob + uint32_t loadtmp_off; ///< Offset of load-time asset blob + uint32_t loadtmp_size; ///< Size of load-time asset blob +} dso_file_header_t; + /** @brief Offset of syms member of dso_module_t on N64 */ -#define DSO_SYMS_OFS 0x18 +#define DSO_SYMS_OFS 0x14 /** @brief Offset of num_import_syms member of dso_module_t on N64 */ -#define DSO_NUM_IMPORT_SYMS_OFS 0x20 +#define DSO_NUM_IMPORT_SYMS_OFS 0x1C /** @brief Size of dso_sym_t on N64 */ #define DSO_SYM_SIZE 0xC @@ -35,7 +48,6 @@ typedef struct dso_reloc_s { /** @brief DSO module data */ typedef struct dso_module_s { - uint32_t magic; ///< Magic number struct dso_module_s *prev; ///< Previous loaded dynamic library struct dso_module_s *next; ///< Next loaded dynamic library uint32_t ref_count; ///< Dynamic library reference count diff --git a/src/ed64.c b/src/ed64.c new file mode 100644 index 000000000..47191f9a7 --- /dev/null +++ b/src/ed64.c @@ -0,0 +1,51 @@ +/** + * @file ed64.c + * @author Giovanni Bajo + * @brief EverDrive 64 V-series utilities + * @ingroup peripherals + */ + +#include "debug.h" +#include "dma.h" +#include "ed64.h" +#include "rtc_internal.h" +#include "utils.h" + +/** @brief ED64 register to perform I2C bitbanging */ +#define ED64_REG_I2C 0x08040030 + +/** @brief Init I2C communication (callback from i2c_internal.h) */ +#define I2C_INIT() ({ }) +/** @brief Write I2C clock and data line (callback from i2c_internal.h) */ +#define I2C_WRITE(clock, data) ({ io_write(ED64_REG_I2C, ((clock)<<2) | ((data))); }) +/** @brief Read I2C data line, while setting the clock (callback from i2c_internal.h) */ +#define I2C_READ(clock) ({ io_write(ED64_REG_I2C, ((clock)<<2) | 1); io_read(ED64_REG_I2C) & 1; }) + +#include "i2c_internal.h" + +#define DS1337_DEVICE_ADDR 0x68 ///< DS1337 I2C device address + +void ed64_rtc_encode(time_t new_time, uint8_t buf[7]) +{ + struct tm * rtc_time = gmtime(&new_time); + + // ED64 stores the day of week encoded as 1=Monday 7=Sunday + int ed64_wday = rtc_time->tm_wday; + if (ed64_wday == 0) ed64_wday = 7; + + // Encoding according to DS1337 datasheet + buf[0] = bcd_encode(rtc_time->tm_sec); + buf[1] = bcd_encode(rtc_time->tm_min); + buf[2] = bcd_encode(rtc_time->tm_hour); // bit 6 toggles 12/24 hour mode + buf[3] = bcd_encode(ed64_wday); + buf[4] = bcd_encode(rtc_time->tm_mday); + buf[5] = bcd_encode(rtc_time->tm_mon + 1); // bit 7 is the century bit + buf[6] = bcd_encode(rtc_time->tm_year % 100); +} + +int ed64_rtc_write( uint8_t buf[7] ) +{ + if (!i2c_write_data(DS1337_DEVICE_ADDR, 0, 7, buf)) + return -1; + return 0; +} diff --git a/src/ed64.h b/src/ed64.h new file mode 100644 index 000000000..14d511d47 --- /dev/null +++ b/src/ed64.h @@ -0,0 +1,50 @@ +/** + * @file ed64.h + * @author Giovanni Bajo + * @brief EverDrive 64 V-series utilities. + * @ingroup peripherals + */ + + #ifndef __LIBDRAGON_ED64_H + #define __LIBDRAGON_ED64_H + + #include + #include + #include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Encode a time into the raw RTC format. + * + * This function encodes a time into the raw format used by the Analog Devices + * DS1337 RTC, which is used by the EverDrive 64. See the datasheet for + * more information. + * + * @param time The time to encode. + * @param buf The buffer to store the encoded time. + */ +void ed64_rtc_encode( time_t time, uint8_t buf[7] ); + +/** + * @brief Write an encoded time to the EverDrive 64 V3 RTC. + * + * This function writes an encoded time to the EverDrive 64 V3 RTC, using + * direct I2C access via hardware registers. It can be used to change the time, + * as ED64 V3 does not support using the joybus RTC write commands. + * + * @param buf The encoded time to write (see #ed64_rtc_encode). + * @return 0 on success, -1 on failure. + * + * @see #ed64_rtc_encode + * @see #ed64x_rtc_write + */ +int ed64_rtc_write( uint8_t buf[7] ); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/ed64x.c b/src/ed64x.c new file mode 100644 index 000000000..c8a24f448 --- /dev/null +++ b/src/ed64x.c @@ -0,0 +1,125 @@ +/** + * @file ed64x.c + * @author Christopher Bonhage + * @author Giovanni Bajo + * @brief EverDrive 64 X-series utilities + * @ingroup peripherals + */ + +#include "debug.h" +#include "dma.h" +#include "ed64x.h" +#include "rtc_internal.h" +#include "utils.h" +#include "n64types.h" + +#define DS1337_DEVICE_ADDR 0x68 ///< DS1337 I2C device address + +static const uint32_t ED64X_REG_I2C_CMD = 0x1F800018; +static const uint32_t ED64X_REG_I2C_DAT = 0x1F80001C; +static const uint32_t ED64X_REG_RTC_CACHE = 0x1F808010; + +static int ed64x_i2c_status(void) +{ + return io_read(ED64X_REG_I2C_CMD) & 1; +} + +static int ed64x_i2c_cmd(uint8_t cmd) +{ + io_write(ED64X_REG_I2C_DAT, cmd); + while (io_read(ED64X_REG_I2C_CMD) & 0x80) {} + return ed64x_i2c_status(); +} + +__attribute__((used)) +static uint8_t ed64x_i2c_dat(uint8_t cmd) +{ + io_write(ED64X_REG_I2C_DAT, cmd); + while (io_read(ED64X_REG_I2C_CMD) & 0x80) {} + return io_read(ED64X_REG_I2C_DAT); +} + +static void ed64x_i2c_start(void) +{ + uint8_t val = io_read(ED64X_REG_I2C_CMD); + io_write(ED64X_REG_I2C_CMD, 0x20); + io_write(ED64X_REG_I2C_DAT, 0xFF); + while (io_read(ED64X_REG_I2C_CMD) & 0x80) {} + io_write(ED64X_REG_I2C_CMD, val | 0x11); // set write mode +} + +static void ed64x_i2c_end(void) +{ + io_write(ED64X_REG_I2C_CMD, 0x30); + io_write(ED64X_REG_I2C_DAT, 0xFF); + while (io_read(ED64X_REG_I2C_CMD) & 0x80) {} +} + +__attribute__((used)) +static void ed64x_i2c_setwr(void) +{ + io_write(ED64X_REG_I2C_CMD, io_read(ED64X_REG_I2C_CMD) | 0x11); +} + +__attribute__((used)) +static void ed64x_i2c_setrd(void) +{ + io_write(ED64X_REG_I2C_CMD, io_read(ED64X_REG_I2C_CMD) | 0x10); +} + +static int ed64x_i2c_write(uint16_t addr, const uint8_t* data, int len) +{ + uint8_t bus_addr = addr >> 8; + uint8_t dev_addr = addr & 0xFF; + + for (int i=0; i + * @author Giovanni Bajo + * @brief EverDrive 64 X-Series utilities. + * @ingroup peripherals + */ + +#ifndef __LIBDRAGON_ED64X_H +#define __LIBDRAGON_ED64X_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Write an encoded time to the EverDrive 64 X7 RTC. + * + * This function writes an encoded time to the EverDrive 64 X7 RTC, using + * direct I2C access via hardware registers. It can be used to change the time, + * as ED64 X7 does not support using the joybus RTC write commands. + * + * @param buf The encoded time to write (see #ed64_rtc_encode). + * @return 0 on success, -1 on failure. + * + * @see #ed64_rtc_encode + * @see #ed64_rtc_write + */ +int ed64x_rtc_write( uint8_t buf[7] ); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/eia608.c b/src/eia608.c new file mode 100644 index 000000000..bebbd7d38 --- /dev/null +++ b/src/eia608.c @@ -0,0 +1,621 @@ +/** + * @file eia608.c + * @author Giovanni Bajo + * @brief Library to generate NTSC EIA-608 closed captions + * @ingroup display + */ + +#include "eia608.h" +#include "surface.h" +#include "vi.h" +#include "vi_internal.h" +#include "n64sys.h" +#include "n64types.h" +#include "debug.h" +#include "utils.h" +#include +#include +#include +#include + +/** + * @brief Activate debugging mode for EIA608 signal + * + * This macro will do a few things: + * - Allow PAL + * - Stick the EIA608 line to the top of the output window + * - Make the EAI608 line 8 lines big so that it's more visible + * + * This would allow to visually debug the implementation by configuring large + * borders, so that EIA608 becomes visible. Obviously it will likely break + * *actual* captions. + */ +#define DEBUG_EIA608 0 + +#define EIA608_IRQ_SAFEMARGIN 4 ///< How many halflines to trigger the IRQ before the actual line + +#define SIG_ON 0x8421 ///< IRE 50 (50% intensity RGBA16) +#define SIG_OFF 0x0000 ///< IRE 0 + +#define SIGW_SCALE 4 ///< Scale factor for the signal wrt linebuffer pixels +#define SIGW_BLANK 7 ///< Blank before/after the actual eia608 signal +#define SIGW_LEADIN 19 ///< From spec: 19 bits of clock leadin +#define SIGW_BIT 2 ///< From spec: Encode each bit in 2 pixels, to make it take twice the time of the leadin (per bit) +#define SIGW_DATA (16*SIGW_BIT) ///< From spec: 16 bits of data for each packet +#define SIGW_LEN (SIGW_BLANK + (SIGW_LEADIN + SIGW_DATA) * SIGW_SCALE + SIGW_BLANK) ///< Total length of EIA608 signal in pixels + +#define RING_BUFFER_SIZE 512 ///< Size of the ring buffer for EIA608 data + +static uint16_t *ring_buffer; +static volatile int rb_wpos, rb_rpos; +static surface_t linebuffer; +static int force_clear_timer; +static int irq_errors; + +static struct { + bool interlaced; ///< Whether the parameters refer to interlaced mode or not + uint32_t reg_ctrl; ///< Value to set for VI_CTRL + uint32_t reg_h_video; ///< Value to set for VI_H_VIDEO + uint32_t reg_xscale; ///< Value to set for VI_X_SCALE + int v_line_start; ///< Lines to draw + int v_line_end; ///< Lines to draw + int out_x0, out_y0, out_x1, out_y1; ///< Previous output window +} sigparms; + +static uint16_t* linebuffer_write_bits(uint16_t* buffer, bool on, int nbits) +{ + uint16_t val16 = on ? SIG_ON : SIG_OFF; + uint32_t val32 = (val16 << 16) | val16; + uint64_t val64 = (uint64_t)val32 << 32 | val32; + int size = SIGW_SCALE * nbits; + while (size >= 8) { + *(u_uint64_t*)buffer = val64; + buffer += 8; + size -= 8; + } + if (size >= 4) { + *(u_uint32_t*)buffer = val32; + buffer += 4; + size -= 4; + } + if (size >= 2) { + *(uint16_t*)buffer = val16; + buffer += 2; + size -= 2; + } + return buffer; +} + +static void linebuffer_init(surface_t *lb) +{ + uint16_t *buffer = lb->buffer; + + for (int i=0; i>= 1; + } + + buffer += SIGW_DATA * SIGW_SCALE; // skip the data part + + for (int i=0; ibuffer; + buffer += SIGW_BLANK; // skip the blank part + buffer += SIGW_LEADIN * SIGW_SCALE; // skip the leadin part + data = (data << 8) | (data >> 8); + for (int i=0; i<16; i++) { + buffer = linebuffer_write_bits(buffer, data & 1, SIGW_BIT); + data >>= 1; + } +} + +static void recalc_parms(void) +{ + const float eia_databit_us = 1.986f; // from spec: duration of a data bit in us + const float ntsc_pixel_us = 0.082166f; // fixed for N64 NTSC + float xscale = (SIGW_BIT * SIGW_SCALE) / (eia_databit_us / ntsc_pixel_us); + uint32_t xscale_fx = (uint32_t)(xscale * 1024.0f + 0.5f); + + uint32_t ctrl = 0; + ctrl |= !sys_bbplayer() ? VI_PIXEL_ADVANCE_DEFAULT : VI_PIXEL_ADVANCE_BBPLAYER; + ctrl |= VI_CTRL_TYPE_16_BPP; // 16-bit color + ctrl |= VI_AA_MODE_RESAMPLE; // resample AA mode + ctrl |= vi_read(VI_CTRL) & VI_CTRL_SERRATE; // keep interlace mode if active + + bool interlaced = ctrl & VI_CTRL_SERRATE; + + // Calculate vertical line on which to draw. The spec says to draw on + // NTSC line 21. NTSC counts from the start of the vsync pulse, while + // the N64 starts counting after it. Moreover, in interlace mode, we want + const int LINE_21 = 21; // from spec: draw on NTSC line 21 + const int vsync_height = vi_read(VI_BURST) >> 16 & 0xF; + const int v_line = LINE_21 - vsync_height; + int v_halfline = v_line * 2 + 1; + if (!interlaced) v_halfline++; + + const int h_start = 96; // earliest possible start of active video on NTSC + const int h_end = h_start + 640; // standard 640 pixel output width + const float clock_target_us = 10.5f; // EIA-608-B spec: clock lead in starts at 10.5 us after hsync + const float clock_target = clock_target_us / ntsc_pixel_us; // clock target in NTSC dots + float blank = (clock_target - h_start) * xscale; // framebuffer pixel of blank before clock leadin + blank -= 3; // compensate for VI bug that delays the start by 3 pixels + assert(blank >= 7); // VI blanks the first 7 pixels, so we need at least that much + // If this triggers, try playing with h_start or SIGW_SCALE + // We now need to make sure we have "blank" fractional pixels of blank before the clock leadin + // The fraction part can be adjusted using VI's XOFFSET. For the integer part, + // we could just use it as SIGW_BLANK, but instead we keep SIGW_BLANK hardcoded, + // and just adjust xoffset accordingly. + float xoffset = blank - SIGW_BLANK; // adjustement required on top of SIGW_BLANK + assert(xoffset >= 0 && xoffset < 3); // xoffset is max 3; if this triggers, change SIGW_BLANK + uint32_t xoffset_fx = (uint32_t)(xoffset * 1024.0f + 0.5f); + + sigparms.interlaced = interlaced; + sigparms.reg_ctrl = ctrl; + sigparms.reg_h_video = (h_start << 16) | h_end; + sigparms.reg_xscale = (xoffset_fx << 16) | xscale_fx; + sigparms.v_line_start = v_halfline; + sigparms.v_line_end = v_halfline + (interlaced ? 2 : 1); + + // For now, we only support default NTSC output where the real output + // begins on line 23, just one line after the EIA-608 signal. + vi_get_output(&sigparms.out_x0, &sigparms.out_y0, &sigparms.out_x1, &sigparms.out_y1); + #if DEBUG_EIA608 + sigparms.v_line_start = sigparms.out_y0 - 16; + sigparms.v_line_end = sigparms.out_y0; + #endif + assertf(sigparms.v_line_end == sigparms.out_y0, + "EIA-608: unimplemented support for borders"); +} + +static void __eia608_interrupt(void*) +{ + static int framecounter = 0; ++framecounter; + + // We need to update the linebuffer at 30 Hz, so every + // other frame. Even if the display is interlaced, it doesn't + // really matter *which* field we do the update, as we emit + // the signal on both anyway. + if (framecounter & 1) { + // Fetch the data from the ring buffer (or the NOP filler if empty) + uint16_t data; + + if (force_clear_timer && --force_clear_timer < 2) { + data = EIA608_CC1_EDM; + } else if (rb_rpos == rb_wpos) { + data = EIA608_NOP; + } else { + data = ring_buffer[rb_rpos]; + rb_rpos = (rb_rpos + 1) % RING_BUFFER_SIZE; + } + + // Draw the data into the linebuffer + linebuffer_write(&linebuffer, data); + } + + // The VI should still be drawing the line *before* the one that we want + // to draw on. If we're not there, it means that the interrupt was late, + // so we'll just skip this frame. + if ((*VI_V_CURRENT|1) >= (sigparms.v_line_start|1)-2) { + #if DEBUG_EIA608 + debugf("EIA-608: IRQ error %ld %d\n", *VI_V_CURRENT, sigparms.v_line_start); + #endif + ++irq_errors; + return; + } + + // Fetch the current VI configuration + uint32_t old_origin = *VI_ORIGIN; + uint32_t old_ctrl = *VI_CTRL; + uint32_t old_h_video = *VI_H_VIDEO; + uint32_t old_x_scale = *VI_X_SCALE; + uint32_t old_y_scale = *VI_Y_SCALE; + + uint32_t new_origin = (uint32_t)(linebuffer.buffer); + uint32_t new_ctrl = sigparms.reg_ctrl; + uint32_t new_h_video = sigparms.reg_h_video; + uint32_t new_x_scale = sigparms.reg_xscale; + uint32_t new_y_scale = 0; // FIXME: this seems to revert the interlacing fix? + + // Wait for the line to match the one we're expecting + while ((*VI_V_CURRENT|1) < (sigparms.v_line_start|1)-2) {} + + // We are now in hblank. Quickly switch VI configuration to draw the + // linebuffer. Notice that we don't need to set VI_WIDTH because Y scale is 0 anyway. + *VI_ORIGIN = new_origin; + *VI_CTRL = new_ctrl; + *VI_H_VIDEO = new_h_video; + *VI_X_SCALE = new_x_scale; + *VI_Y_SCALE = new_y_scale; + MEMORY_BARRIER(); + + // Wait for the EIA-608 signal to be displayed + // Displaying one line takes less than 60us, so we realistically don't + // have time to do much else, and it would be a waste to retrigger another + // interrupt. + while ((*VI_V_CURRENT|1) < (sigparms.v_line_end|1)) {} + + // Restore the original VI configuration + *VI_ORIGIN = old_origin; + *VI_CTRL = old_ctrl; + *VI_H_VIDEO = old_h_video; + *VI_X_SCALE = old_x_scale; + *VI_Y_SCALE = old_y_scale; + MEMORY_BARRIER(); +} + +void eia608_init(void) +{ + #if !DEBUG_EIA608 + assertf(get_tv_type() == TV_NTSC, "EIA-608 is only supported on NTSC"); + #endif + assert(ring_buffer == NULL); + ring_buffer = malloc(RING_BUFFER_SIZE * 2); + assertf(ring_buffer, "Out of memory"); + rb_rpos = rb_wpos = 0; + + // Allocate and initialize the linebuffer + linebuffer = surface_alloc(FMT_RGBA16, SIGW_LEN, 1); + linebuffer_init(&linebuffer); +} + +void eia608_close(void) +{ + if (ring_buffer) free(ring_buffer); + ring_buffer = NULL; + surface_free(&linebuffer); +} + +void eia608_start(void) +{ + recalc_parms(); + + // Enqueue one second of NOP fillers, so that we give time to the TV + // to stabilize before we start sending the actual data + rb_rpos = rb_wpos = 0; + for (int i=0; i<30; i++) + ring_buffer[rb_wpos++] = EIA608_NOP; + + vi_write_begin(); + // Enable the line interrupt + vi_set_line_interrupt(sigparms.v_line_start-EIA608_IRQ_SAFEMARGIN, __eia608_interrupt, NULL); + // Increase the output window to include the EIA-608 signal + vi_set_output(sigparms.out_x0, sigparms.v_line_start, sigparms.out_x1, sigparms.out_y1); + vi_write_end(); +} + +void eia608_stop(void) +{ + vi_write_begin(); + vi_set_output(sigparms.out_x0, sigparms.out_y0, sigparms.out_x1, sigparms.out_y1); + vi_set_line_interrupt(sigparms.v_line_start-EIA608_IRQ_SAFEMARGIN, NULL, NULL); + vi_write_end(); +} + +static uint8_t odd_parity(uint8_t value) +{ + uint8_t v = value; + v ^= v >> 4; + v ^= v >> 2; + v ^= v >> 1; + return value | ((v ^ 1) << 7); +} + +bool eia608_write_raw(uint16_t data, bool calc_parity) +{ + debugf("EIA-608: %04X\n", data); + int next = (rb_wpos + 1) % RING_BUFFER_SIZE; + if (next == rb_rpos) + return false; + + if (calc_parity) + data = odd_parity(data) | (odd_parity(data >> 8) << 8); + + ring_buffer[rb_wpos] = data; + rb_wpos = next; + return true; +} + +void eia608_write_ctrl_raw(eia608_ctrl_t ctrl) +{ + // EIA-608 control codes are 16-bit values, but the spec suggests to always + // send them twice for reliability. + eia608_write_raw(ctrl, false); + eia608_write_raw(ctrl, false); +} + +static void eia608_write_indent(eia608_channel_t cc, int row, int indent, bool underline) +{ + assert(cc == EIA608_CC1 || cc == EIA608_CC2); + assert(row >= 1 && row <= 15); + assert(indent >= 0 && indent <= 31); + static const uint16_t pac_code[15] = { + 0x1150, 0x1170, 0x1250, 0x1270, 0x1550, 0x1570, 0x1650, 0x1670, + 0x1750, 0x1770, 0x1050, 0x1350, 0x1370, 0x1450, 0x1470 + }; + + uint16_t data = pac_code[row-1]; + data |= (indent / 4) << 1; + if (underline) data |= 0x1; + + eia608_write_raw(data, true); + eia608_write_raw(data, true); +} + +/** + * @brief Encode a UTF8-character from a string into EIA-608 format + * + * This function takes a pointer to a UTF8 string, and encodes the next character + * into EIA-608 format. The pointer is updated to point to the next character in + * the string. + * + * The EIA-608 format is a 32-bit value as follows: + * * 0 means that the character is not supported by EIA-608 + * * 1-byte values are standard 1-byte EIA-608 characters + * * 2-byte values are characters from the North American set, that must be + * transmitted in a single packet. + * * 3-byte values are extended characters made by 1 byte value (MSB) which is + * the alternate representation (eg: à => a), and a 2-byte value which must + * be transmitted in a whole packet. Notice that the semantic of the 2-value + * character also encodes a backspace, so the single-byte alternative value + * must always be transmitted. + */ +static uint32_t eia608_encode_char(const char **utf8_str) +{ + uint8_t ch0 = *(*utf8_str)++; + if (ch0 < 0x80) { + // Map unicode codepoints into EIA-608 equivalents + switch (ch0) { + case 0x27: return 0x1229 | '\''<<16; // APOSTROPHE -> RIGHT_SINGLE_QUOTATION_MARK + case 0x2A: return 0x1228 | '#'<<16; // ASTERISK + case 0x5C: return 0x132B | '/'<<16; // REVERSE SOLIDUS + case 0x5E: return 0x132C | '/'<<16; // CIRCUMFLEX ACCENT + case 0x5F: return 0x132D | '-'<<16; // LOW LINE + case 0x60: return 0x1226 | '\''<<16; // GRAVE ACCENT + case 0x7B: return 0x1329 | '['<<16; // LEFT CURLY BRACKET + case 0x7C: return 0x132E | '-'<<16; // VERTICAL LINE + case 0x7D: return 0x132A | ']'<<16; // RIGHT CURLY BRACKET + case 0x7E: return 0x132F | '-'<<16; // TILDE + default: return ch0; // ASCII default + } + } + if (ch0 == 0xC2) switch ((uint8_t)*(*utf8_str)++) { + // Special North American character set + case 0xAE: return 0x1130; // REGISTERED SIGN + case 0xB0: return 0x1131; // DEGREE SIGN + case 0xBD: return 0x1132; // VULGAR_FRACTION_ONE_HALF + case 0xBF: return 0x1133; // INVERTED_QUESTION_MARK + case 0xA2: return 0x1135; // CENT SIGN + case 0xA3: return 0x1136; // POUND SIGN + // Extended Spanish/Miscellaneous + case 0xA1: return 0x1227 | '!'<<16; // INVERTED_EXCLAMATION_MARK + case 0xA9: return 0x122B | 'c'<<16; // COPYRIGHT SIGN + // Extended French + case 0xAB: return 0x123E | '<'<<16; // LEFT_POINTING_DOUBLE_ANGLE_QUOTATION_MARK + case 0xBB: return 0x123F | '>'<<16; // RIGHT_POINTING_DOUBLE_ANGLE_QUOTATION_MARK + // German/Danish + case 0xA5: return 0x1335 | 'Y'<<16; // YEN SIGN + case 0xA4: return 0x1336 | 'C'<<16; // CURRENCY SIGN + case 0xA6: return 0x1337 | '|'<<16; // BROKEN BAR + default: return 0; + } + if (ch0 == 0xC3) switch ((uint8_t)*(*utf8_str)++) { + // ASCII exceptions + case 0xA1: return 0x2A; // LATIN_SMALL_LETTER_A_WITH_ACUTE + case 0xA9: return 0x5C; // LATIN_SMALL_LETTER_E_WITH_ACUTE + case 0xAD: return 0x5E; // LATIN_SMALL_LETTER_I_WITH_ACUTE + case 0xB3: return 0x5F; // LATIN_SMALL_LETTER_O_WITH_ACUTE + case 0xBA: return 0x60; // LATIN_SMALL_LETTER_U_WITH_ACUTE + case 0xA7: return 0x7B; // LATIN_SMALL_LETTER_C_WITH_CEDILLA + case 0xB7: return 0x7C; // DIVISION_SIGN + case 0x91: return 0x7D; // LATIN_CAPITAL_LETTER_N_WITH_TILDE + case 0xB1: return 0x7E; // LATIN_SMALL_LETTER_N_WITH_TILDE + // Special North American character set + case 0xA0: return 0x1138; // LATIN_SMALL_LETTER_A_WITH_GRAVE + case 0xA8: return 0x113A; // LATIN_SMALL_LETTER_E_WITH_GRAVE + case 0xA2: return 0x113B; // LATIN_SMALL_LETTER_A_WITH_CIRCUMFLEX + case 0xAA: return 0x113C; // LATIN_SMALL_LETTER_E_WITH_CIRCUMFLEX + case 0xAE: return 0x113D; // LATIN_SMALL_LETTER_I_WITH_CIRCUMFLEX + case 0xB4: return 0x113E; // LATIN_SMALL_LETTER_O_WITH_CIRCUMFLEX + case 0xBB: return 0x113F; // LATIN_SMALL_LETTER_U_WITH_CIRCUMFLEX + // Extended Spanish/Miscellaneous characters + case 0x81: return 0x1220 | 'A'<<16; // LATIN_CAPITAL_LETTER_A_WITH_ACUTE + case 0x89: return 0x1221 | 'E'<<16; // LATIN_CAPITAL_LETTER_E_WITH_ACUTE + case 0x93: return 0x1222 | 'O'<<16; // LATIN_CAPITAL_LETTER_O_WITH_ACUTE + case 0x9A: return 0x1223 | 'U'<<16; // LATIN_CAPITAL_LETTER_U_WITH_ACUTE + case 0x9C: return 0x1224 | 'U'<<16; // LATIN_CAPITAL_LETTER_U_WITH_DIAERESIS + case 0xBC: return 0x1225 | 'u'<<16; // LATIN_SMALL_LETTER_U_WITH_DIAERESIS + // Extended French + case 0x80: return 0x1230 | 'A'<<16; // LATIN_CAPITAL_LETTER_A_WITH_GRAVE + case 0x82: return 0x1231 | 'A'<<16; // LATIN_CAPITAL_LETTER_A_WITH_CIRCUMFLEX + case 0x87: return 0x1232 | 'C'<<16; // LATIN_CAPITAL_LETTER_C_WITH_CEDILLA + case 0x88: return 0x1233 | 'E'<<16; // LATIN_CAPITAL_LETTER_E_WITH_GRAVE + case 0x8A: return 0x1234 | 'E'<<16; // LATIN_CAPITAL_LETTER_E_WITH_CIRCUMFLEX + case 0x8B: return 0x1235 | 'E'<<16; // LATIN_CAPITAL_LETTER_E_WITH_DIAERESIS + case 0xAB: return 0x1236 | 'e'<<16; // LATIN_SMALL_LETTER_E_WITH_DIAERESIS + case 0x8E: return 0x1237 | 'I'<<16; // LATIN_CAPITAL_LETTER_I_WITH_CIRCUMFLEX + case 0x8F: return 0x1238 | 'I'<<16; // LATIN_CAPITAL_LETTER_I_WITH_DIAERESIS + case 0xAF: return 0x1239 | 'i'<<16; // LATIN_SMALL_LETTER_I_WITH_DIAERESIS + case 0x94: return 0x123A | 'O'<<16; // LATIN_CAPITAL_LETTER_O_WITH_CIRCUMFLEX + case 0x99: return 0x123B | 'U'<<16; // LATIN_CAPITAL_LETTER_U_WITH_GRAVE + case 0xB9: return 0x123C | 'u'<<16; // LATIN_SMALL_LETTER_U_WITH_GRAVE + case 0x9B: return 0x123D | 'U'<<16; // LATIN_CAPITAL_LETTER_U_WITH_CIRCUMFLEX + // Portuguese + case 0x83: return 0x1320 | 'A'<<16; // LATIN_CAPITAL_LETTER_A_WITH_TILDE + case 0xA3: return 0x1321 | 'a'<<16; // LATIN_SMALL_LETTER_A_WITH_TILDE + case 0x8D: return 0x1322 | 'I'<<16; // LATIN_CAPITAL_LETTER_I_WITH_ACUTE + case 0x8C: return 0x1323 | 'I'<<16; // LATIN_CAPITAL_LETTER_I_WITH_GRAVE + case 0xAC: return 0x1324 | 'i'<<16; // LATIN_SMALL_LETTER_I_WITH_GRAVE + case 0x92: return 0x1325 | 'O'<<16; // LATIN_CAPITAL_LETTER_O_WITH_GRAVE + case 0xB2: return 0x1326 | 'o'<<16; // LATIN_SMALL_LETTER_O_WITH_GRAVE + case 0x95: return 0x1327 | 'O'<<16; // LATIN_CAPITAL_LETTER_O_WITH_TILDE + case 0xB5: return 0x1328 | 'o'<<16; // LATIN_SMALL_LETTER_O_WITH_TILDE + // German/Danish + case 0x84: return 0x1330 | 'A'<<16; // LATIN_CAPITAL_LETTER_A_WITH_DIAERESIS + case 0xA4: return 0x1331 | 'a'<<16; // LATIN_SMALL_LETTER_A_WITH_DIAERESIS + case 0x96: return 0x1332 | 'O'<<16; // LATIN_CAPITAL_LETTER_O_WITH_DIAERESIS + case 0xB6: return 0x1333 | 'o'<<16; // LATIN_SMALL_LETTER_O_WITH_DIAERESIS + case 0x9F: return 0x1334 | 'S'<<16; // LATIN_CAPITAL_LETTER_SHARP_S + case 0x85: return 0x1338 | 'A'<<16; // LATIN_CAPITAL_LETTER_A_WITH_RING_ABOVE + case 0xA5: return 0x1339 | 'a'<<16; // LATIN_SMALL_LETTER_A_WITH_RING_ABOVE + case 0x98: return 0x133A | 'O'<<16; // LATIN_CAPITAL_LETTER_O_WITH_STROKE + case 0xB8: return 0x133B | 'o'<<16; // LATIN_SMALL_LETTER_O_WITH_STROKE + default: return 0; + } + if (ch0 == 0xE2) { + uint16_t ch1 = *(*utf8_str)++ << 8; ch1 |= *(*utf8_str)++; + switch (ch1) { + // ASCII exceptions + case 0x9688: return 0x7F; // FULL_BLOCK + // Special North American character set + case 0x84A2: return 0x1134; // TRADE_MARK_SIGN + case 0x99AA: return 0x1137; // EIGHTH_NOTE + // Extended Spanish/Miscellaneous + case 0x8098: return 0x1226 | '\''<<16; // LEFT_SINGLE_QUOTATION_MARK + case 0x8099: return 0x27; // RIGHT_SINGLE_QUOTATION_MARK -> APOSTROPHE + case 0x8094: return 0x122A | '-'<<16; // EM_DASH + case 0x84A0: return 0x122C | 's'<<16; // SERVICE_MARK + case 0x80A2: return 0x122D | '.'<<16; // BULLET + case 0x809C: return 0x122E | '"'<<16; // LEFT_DOUBLE_QUOTATION_MARK + case 0x809D: return 0x122F | '"'<<16; // RIGHT_DOUBLE_QUOTATION_MARK + // German/Danish + case 0x948C: return 0x133C | '+'<<16; // BOX_DRAWINGS_LIGHT_DOWN_AND_RIGHT + case 0x9490: return 0x133D | '+'<<16; // BOX_DRAWINGS_LIGHT_DOWN_AND_LEFT + case 0x9494: return 0x133E | '+'<<16; // BOX_DRAWINGS_LIGHT_UP_AND_RIGHT + case 0x9498: return 0x133F | '+'<<16; // BOX_DRAWINGS_LIGHT_UP_AND_LEFT + default: return 0; + } + } + if (ch0 < 0xE0) { utf8_str+=1; return 0; } + if (ch0 < 0xF0) { utf8_str+=2; return 0; } + if (ch0 < 0xF8) { utf8_str+=3; return 0; } + return 0; +} + +void eia608_caption_prepare(eia608_channel_t cc, const char *utf8_str, eia608_captionparms_t *parms) +{ + // EIA-608 POP-ON mode can only hold up to three lines of 32 characters each + enum { MAX_ROWS = 4 }; + + eia608_captionparms_t _default = {0}; + if (parms == NULL) parms = &_default; + + int first_row = parms->row ? parms->row : 11; + + uint32_t buffer[32*MAX_ROWS+8]; + int buf_len = 0; + + // Emit start caption command (in pop-on mode) + eia608_write_ctrl_raw(cc == EIA608_CC1 ? EIA608_CC1_RCL : EIA608_CC2_RCL); + + // Convert the input string from UTF-8 into EIA-608 16-bit characters. + for (int i = 0; i < sizeof(buffer)/2; i++) { + if (*utf8_str == 0) break; + uint32_t ch = eia608_encode_char(&utf8_str); + if (ch > 0x100 && cc == EIA608_CC2) ch |= 0x8000; + if (ch == 0) continue; + buffer[buf_len++] = ch; + } + + // Now split in lines of 32 characters max + int buf_idx = 0; + for (int row = 0; row < MAX_ROWS; row++) { + // Go through the buffer and find the next line break + // Notice that each character in buffer is one visual glyph + // (irrespective of how many raw bytes it requires). + int wrap = -1; int ch; + for (ch = 0; ch < 32; ch++) { + if (buf_idx+ch >= buf_len) + break; + + if (buffer[buf_idx+ch] == '\n') { + wrap = ch; + break; + } + + if (buffer[buf_idx+ch] == ' ') + wrap = ch; + } + + // Empty line, just skip it (or stop if we're done) + if (ch == 0) { + if (buf_idx >= buf_len) break; + buf_idx++; + continue; + } + + // We need to wrap if we reached 32 chars and there are more + if (ch == 32 && buf_idx+ch < buf_len) + ch = wrap; + + // Calculate indentation to center the line + // rounding it to the nearest multiple of 4 + int indent = (32 - ch) / 2; + eia608_write_indent(cc, first_row, indent & ~3, parms->underline); + for (int i=0; i<(indent & 3); i++) + eia608_write_raw(cc == EIA608_CC1 ? EAI608_CC1_TS : EAI608_CC2_TS, false); + + // Now emit the characters. + uint16_t accum = 0; + for (int i = 0; i < ch; i++) { + uint32_t token = buffer[buf_idx++]; + + // Split the token into 1, 2, or 1+2 byte values + uint16_t single = 0, pair = 0; + if (token >> 16) { + single = token >> 16; + pair = token & 0xffff; + } else if (token >> 8) { + pair = token; + } else { + single = token; + } + + // Emit single bytes, accumulating them into pairs + // if possible (otherwise use 0 to pad the pair) + if (single) { + if (accum) { + accum |= single; + eia608_write_raw(accum, true); + accum = 0; + } else { + accum = single << 8; + } + } + + // Emit the pair + if (pair) { + if (accum) { + eia608_write_raw(accum, true); + accum = 0; + } + eia608_write_raw(pair, true); + } + } + + // Emit any pending single byte value, since we are going + // to emit a control code next. + if (accum) { + eia608_write_raw(accum, true); + } + + // Move to the next line + first_row++; + + // Skip wrapping character + buf_idx++; + } +} + +void eia608_caption_show(eia608_channel_t cc, float duration_secs) +{ + eia608_write_ctrl_raw(cc == EIA608_CC1 ? EIA608_CC1_EOC : EIA608_CC2_EOC); + force_clear_timer = duration_secs * 30; +} diff --git a/src/entropy.c b/src/entropy.c new file mode 100644 index 000000000..f589eb168 --- /dev/null +++ b/src/entropy.c @@ -0,0 +1,112 @@ +/** + * @file entropy.c + * @author Giovanni Bajo + * @brief Entropy source for non-deterministic random number generation. + */ + +#include "entropy.h" +#include "entropy_internal.h" +#include "rand_internal.h" +#include "interrupt_internal.h" +#include +#include + +/** @brief Current entropy state */ +uint64_t __entropy_state = 0; + +/** @brief Entropy calculation constants (MurMurHash3-128) + * These are not marked as static const to coerce GCC to load them for code efficiency. + */ +uint64_t __entropy_K[4] = { + 0x87c37b91114253d5ull, 0x4cf5ad432745937full, + 0xff51afd7ed558ccdull, 0xc4ceb9fe1a85ec53ull, +}; + +// Mix the entropy state to ensure it advances a bit in a non predictable way. +static void __entropy_mix(void) +{ + __entropy_state = __entropy_state<<27 | __entropy_state>>37; + __entropy_state = __entropy_state * 5 + 0x52dce729; +} + +/** + * @brief Add some non-deterministic data to the entropy pool. + * + * This is an internal function that can be used by libdragon libraries to add + * some non-deterministic data to the entropy pool. One example of such data + * would be the joypad inputs at any given point. + * + * The entropy pool is then used t + * + * @param k Non-deterministic data (up to 64 bits) + */ +__attribute__((noinline)) +void __entropy_add(uint64_t k) { + // This is half of MurMurHash3-128. + k *= __entropy_K[0]; + k = k<<31 | k>>33; + k *= __entropy_K[1]; + uint32_t sr = __disable_interrupts(); + __entropy_state ^= k; + __entropy_mix(); + __enable_interrupts(sr); +} + +/** @brief Registers used to pull entropy from */ +volatile uint32_t* __entropy_regs[] = { + (uint32_t*)0xA3F00200, // RI_BANK0_ROW + (uint32_t*)0xA3F00600, // RI_BANK1_ROW + (uint32_t*)0xA450000C, // AI_STATUS + (uint32_t*)0xA4080000, // SP_PC + (uint32_t*)0xA4100010, // DP_CLOCK + (uint32_t*)0xA4600034, // PI_UNKNOWN +}; + +// Since libdragon doesn't call __entropy_add() enough for now, let's always +// pull additional entropy from the hardware by reading a few registers that +// are likely to change at the point of sampling. +static void __entropy_add_internal(void) +{ + for (int i=0; i> 33; + h *= __entropy_K[2]; + h ^= h >> 33; + h *= __entropy_K[3]; + h ^= h >> 33; + return h; +} + +static _Atomic uint64_t xs_state = 88172645463325252ULL; + +__attribute__((noinline)) +static void reseed_from_entropy(void) { + __entropy_add_internal(); + uint64_t e = __entropy_get(); + atomic_fetch_xor_explicit(&xs_state, e, memory_order_relaxed); +} + +int getentropy(void *buf, size_t len) { + reseed_from_entropy(); + __xorshift64_buffer(buf, len, &xs_state); + return 0; +} + +uint64_t getentropy64(void) { + reseed_from_entropy(); + return __xorshift64(&xs_state); +} + +uint32_t getentropy32(void) { + return getentropy64() >> 32; +} diff --git a/src/entropy_internal.h b/src/entropy_internal.h new file mode 100644 index 000000000..b04390f13 --- /dev/null +++ b/src/entropy_internal.h @@ -0,0 +1,12 @@ +/** + * @file entropy_internal.h + * @author Giovanni Bajo + */ +#ifndef LIBDRAGON_ENTROPY_INTERNAL_H +#define LIBDRAGON_ENTROPY_INTERNAL_H + +#include + +void __entropy_add(uint64_t k); + +#endif // LIBDRAGON_ENTROPY_INTERNAL_H diff --git a/src/entrypoint.S b/src/entrypoint.S index 0a9256336..f38cc9f50 100644 --- a/src/entrypoint.S +++ b/src/entrypoint.S @@ -9,6 +9,7 @@ ############################################################ #include "regs.S" +#include "emux.inc" .set noreorder @@ -32,7 +33,12 @@ _start: /* Copy bootcode flags, stored in DMEM */ lui v0, 0xA400 lw t0, 0x0000(v0) + lw t1, 0x0004(v0) + lhu t2, 0x000C(v0) sw t0, %gprel(__boot_memsize)(gp) + sw t1, %gprel(__entropy_state+0)(gp) + sw t1, %gprel(__entropy_state+4)(gp) + sw t2, %gprel(__boot_address_page)(gp) /* Copy other bootcode flags */ lbu t0, 0x0009(v0) @@ -53,5 +59,6 @@ _start: li a1, 0 _abort: - j _abort + xioctl_exit /* request the emulator to exit */ +1: j 1b nop diff --git a/src/exception.c b/src/exception.c index a692ea0d5..64e504b2b 100644 --- a/src/exception.c +++ b/src/exception.c @@ -11,12 +11,16 @@ #include "n64sys.h" #include "debug.h" #include "regsinternal.h" +#include "kernel/kernel_internal.h" +#include "kernel/ktls_internal.h" #include #include #include #include #include +/** Invalid TLS Minimum Address */ +#define TLS_INVALID_MIN (uint32_t)(KERNEL_TP_INVALID-TP_OFFSET) /** * @brief Syscall exception handler entry */ @@ -100,6 +104,9 @@ void __exception_dump_header(FILE *out, exception_t* ex) { fprintf(out, "Syscall code: %05lX\n", (*(uint32_t*)ex->regs->epc >> 6) & 0xfffff); break; + case EXCEPTION_CODE_EMUX: + fprintf(out, "On hardware, the console would now be frozen.\n"); + /* fall through */ case EXCEPTION_CODE_D_BUS_ERROR: { uint32_t opcode = *(uint32_t*)epc; uint64_t base = ex->regs->gpr[((opcode >> 21) & 0x1F)]; @@ -292,7 +299,7 @@ static const char* __get_exception_name(exception_t *ex) "Coprocessor Unusable", // 11 "Arithmetic Overflow", // 12 "Trap", // 13 - "Reserved", // 13 + "Reserved", // 14 "Floating-Point", // 15 "Reserved", // 16 "Reserved", // 17 @@ -302,7 +309,7 @@ static const char* __get_exception_name(exception_t *ex) "Reserved", // 21 "Reserved", // 22 "Watch", // 23 - "Reserved", // 24 + "Emux", // 24 "Reserved", // 25 "Reserved", // 26 "Reserved", // 27 @@ -400,13 +407,21 @@ static const char* __get_exception_name(exception_t *ex) // so leave some margin to the actual faulting address. return "NULL pointer dereference (read)"; } else { - return "Read from invalid memory address"; + if (badvaddr >= TLS_INVALID_MIN && badvaddr < (TLS_INVALID_MIN+TLS_SIZE)) { + return "Read from TLS in interrupt handler"; + } else { + return "Read from invalid memory address"; + } } case EXCEPTION_CODE_TLB_STORE_MISS: if (badvaddr < 128) { return "NULL pointer dereference (write)"; } else { - return "Write to invalid memory address"; + if (badvaddr >= TLS_INVALID_MIN && badvaddr < (TLS_INVALID_MIN+TLS_SIZE)) { + return "Write to TLS in interrupt handler"; + } else { + return "Write to invalid memory address"; + } } case EXCEPTION_CODE_TLB_MODIFICATION: return "Write to read-only memory"; @@ -421,11 +436,22 @@ static const char* __get_exception_name(exception_t *ex) } else { if (is_unmapped_kx64(badvaddr)) return "Read from invalid 64-bit address"; - else - return "Misaligned read from memory"; + else { + if ((badvaddr & 0xFFFF0000) == 0xFEFE0000) + return "Uninitialized pointer dereference"; + if (badvaddr >= TLS_INVALID_MIN && badvaddr < (TLS_INVALID_MIN+TLS_SIZE)) { + return "Read from TLS in interrupt handler"; + } else { + return "Misaligned read from memory"; + } + } } case EXCEPTION_CODE_STORE_ADDRESS_ERROR: - return "Misaligned write to memory"; + if (badvaddr >= TLS_INVALID_MIN && badvaddr < (TLS_INVALID_MIN+TLS_SIZE)) { + return "Write to TLS in interrupt handler"; + } else { + return "Misaligned write to memory"; + } case EXCEPTION_CODE_SYS_CALL: return "Unhandled syscall"; case EXCEPTION_CODE_TRAP: { @@ -434,6 +460,16 @@ static const char* __get_exception_name(exception_t *ex) if (code == 7) return "Integer divide by zero"; } return exceptionMap[ex->code]; + case EXCEPTION_CODE_EMUX: { + uint32_t kind = C0_CACHEERR() & 0xFF; + C0_WRITE_CACHEERR(0); + switch (kind) { + case 0: return "Cached access to non-RDRAM area"; + case 1: return "64-bit read from non-RDRAM area"; + case 2: return "Access to RCP unmapped area"; + default: return "Unknown emux"; + } + } default: return exceptionMap[ex->code]; } @@ -500,13 +536,15 @@ void register_syscall_handler( syscall_handler_t handler, uint32_t first_code, u * @brief Respond to a syscall exception. * * Calls the handlers registered by #register_syscall_handler. + * + * As a special case, if the specified syscall code is 1, the kernel scheduling + * function will be called. That function potentially returns a new stack pointer + * to be used if switching threads. */ -void __onSyscallException( reg_block_t* regs ) +reg_block_t* __onSyscallException( reg_block_t* regs ) { exception_t e; - if(!__exception_handler) { return; } - __fetch_regs(&e, EXCEPTION_TYPE_SYSCALL, regs); // Fetch the syscall code from the opcode @@ -514,6 +552,9 @@ void __onSyscallException( reg_block_t* regs ) uint32_t opcode = *(uint32_t*)epc; uint32_t code = (opcode >> 6) & 0xfffff; + if (code == 0) + return __kthread_syscall_schedule(regs); + bool called = false; for (int i=0; iepc += 4; + return regs; } extern inline bool exception_is_running(void); diff --git a/src/exception_internal.h b/src/exception_internal.h index d70294fe9..d6ef59d2f 100644 --- a/src/exception_internal.h +++ b/src/exception_internal.h @@ -14,6 +14,11 @@ extern "C" { #endif +///@cond +typedef struct surface_s surface_t; +typedef union joypad_buttons_u joypad_buttons_t; +///@endcond + /** * @brief Names of the 34 MIPS general-purpose registers. */ @@ -54,6 +59,19 @@ void __inspector_assertion(const char *failedexpr, const char *msg, va_list args __attribute__((noreturn)) void __inspector_cppexception(const char *exctype, const char *what); +/** + * @brief A page in the inspector. + */ +typedef void (*inspector_page_t)(surface_t *disp, exception_t* ex, joypad_buttons_t *key_pressed); + + +/** + * @brief Add a page to the inspector. + * + * @param page The page to add. + */ +void __inspector_add_page(inspector_page_t page); + #ifdef __cplusplus } #endif diff --git a/src/fat.c b/src/fat.c new file mode 100644 index 000000000..8c8207d56 --- /dev/null +++ b/src/fat.c @@ -0,0 +1,540 @@ +/** + * @file fat.c + * @author Giovanni Bajo + * @brief FAT filesystem interface and newlib wrappers. + */ +#include +#include +#include +#include +#include +#include "fat.h" +#include "fatfs/ff.h" +#include "fatfs/ffconf.h" +#include "fatfs/diskio.h" +#include "debug.h" +#include "system.h" +#include "n64sys.h" +#include "dma.h" +#include "utils.h" + +static fat_disk_t fat_disks[FF_VOLUMES] = {0}; +static FATFS *fat_volumes[FF_VOLUMES] = {0}; +static filesystem_t *fat_filesystems[FF_VOLUMES] = {0}; + +/** @brief FatFS disk API: implementation by forwarding to a disk-specific function. */ +DSTATUS disk_initialize(BYTE pdrv) +{ + if (fat_disks[pdrv].disk_initialize) + return fat_disks[pdrv].disk_initialize(); + return STA_NOINIT; +} + +/** @brief FatFS disk API: implementation by forwarding to a disk-specific function. */ +DSTATUS disk_status(BYTE pdrv) +{ + if (fat_disks[pdrv].disk_status) + return fat_disks[pdrv].disk_status(); + return STA_NOINIT; +} + +/** @brief FatFS disk API: implementation by forwarding to a disk-specific function. */ +DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count) +{ + if (fat_disks[pdrv].disk_read) + return fat_disks[pdrv].disk_read(buff, sector, count); + return RES_PARERR; +} + +/** @brief FatFS disk API: implementation by forwarding to a disk-specific function. */ +DRESULT disk_write(BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count) +{ + if (fat_disks[pdrv].disk_write) + return fat_disks[pdrv].disk_write(buff, sector, count); + return RES_PARERR; +} + +/** @brief FatFS disk API: implementation by forwarding to a disk-specific function. */ +DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void* buff) +{ + if (fat_disks[pdrv].disk_ioctl) + return fat_disks[pdrv].disk_ioctl(cmd, buff); + return RES_PARERR; +} + +/** @brief FatFS API: read the current time from the RTC using standard C functions. */ +DWORD get_fattime(void) +{ + time_t t = time(NULL); + if (t == -1) { + return (DWORD)( + (FF_NORTC_YEAR - 1980) << 25 | + FF_NORTC_MON << 21 | + FF_NORTC_MDAY << 16 + ); + } + struct tm tm; + localtime_r(&t, &tm); + return (DWORD)( + (tm.tm_year - 80) << 25 | + (tm.tm_mon + 1) << 21 | + tm.tm_mday << 16 | + tm.tm_hour << 11 | + tm.tm_min << 5 | + (tm.tm_sec >> 1) + ); +} + +/********************************************************************* + * FAT newlib wrappers + *********************************************************************/ + +/** + * @brief Create a FAT filename string for a given volume and name. + * @param volid Volume ID (0-3). + * @param name File name string. + * @return Pointer to the constructed FAT filename string. + */ +#define MAKE_FAT_NAME(volid, name) ({ \ + char *fat_name = alloca(strlen(name)+2+1); \ + fat_name[0] = '0'+volid; fat_name[1] = ':'; \ + strcpy(fat_name+2, name); \ + fat_name; \ +}) + +/** @brief Number of static FAT file objects. */ +#define NUM_STATIC_FAT_FILES 4 +static FIL static_fat_files[NUM_STATIC_FAT_FILES] = {0}; + +static void __fresult_set_errno(FRESULT err) +{ + assertf(err != FR_INT_ERR, "FatFS assertion error"); + switch (err) { + case FR_OK: return; + case FR_DISK_ERR: errno = EIO; return; + case FR_NOT_READY: errno = EBUSY; return; + case FR_NO_FILE: errno = ENOENT; return; + case FR_NO_PATH: errno = ENOENT; return; + case FR_INVALID_NAME: errno = EINVAL; return; + case FR_DENIED: errno = EACCES; return; + case FR_EXIST: errno = EEXIST; return; + case FR_INVALID_OBJECT: errno = EINVAL; return; + case FR_WRITE_PROTECTED: errno = EROFS; return; + case FR_INVALID_DRIVE: errno = ENODEV; return; + case FR_NOT_ENABLED: errno = ENODEV; return; + case FR_NO_FILESYSTEM: errno = ENODEV; return; + case FR_MKFS_ABORTED: errno = EIO; return; + case FR_TIMEOUT: errno = ETIMEDOUT; return; + case FR_LOCKED: errno = EBUSY; return; + case FR_NOT_ENOUGH_CORE: errno = ENOMEM; return; + case FR_TOO_MANY_OPEN_FILES: errno = EMFILE; return; + case FR_INVALID_PARAMETER: errno = EINVAL; return; + default: errno = EIO; return; + } +} + +static void *__fat_open(char *name, int flags, int volid) +{ + FIL *fp = NULL; + for (int i=0; i= static_fat_files && fp < static_fat_files+NUM_STATIC_FAT_FILES) + fp->obj.fs = NULL; + else + free(fp); + return NULL; + } + return fp; +} + +static void __fat_stat_fill(FSIZE_t size, BYTE attr, struct stat *st) +{ + memset(st, 0, sizeof(struct stat)); + st->st_size = size; + if (attr & AM_RDO) + st->st_mode |= 0444; + else + st->st_mode |= 0666; + if (attr & AM_DIR) + st->st_mode |= S_IFDIR; + else + st->st_mode |= S_IFREG; + st->st_rdev = (dev_t)attr; +} + +static int __fat_stat(char *name, struct stat *st, int volid) +{ + FILINFO fno; + FRESULT res = f_stat(MAKE_FAT_NAME(volid, name), &fno); + if (res != FR_OK) { + __fresult_set_errno(res); + return -1; + } + __fat_stat_fill(fno.fsize, fno.fattrib, st); + struct tm tm; + memset(&tm, 0, sizeof(struct tm)); + tm.tm_sec = (fno.ftime & 0x1F) << 1; + tm.tm_min = (fno.ftime >> 5) & 0x3F; + tm.tm_hour = fno.ftime >> 11; + tm.tm_mday = fno.fdate & 0x1F; + tm.tm_mon = ((fno.fdate >> 5) & 0x0F) - 1; + tm.tm_year = (fno.fdate >> 9) + 80; + st->st_mtim.tv_sec = mktime(&tm); + st->st_mtim.tv_nsec = 0; + return 0; +} + +static int __fat_fstat(void *file, struct stat *st) +{ + FIL *f = file; + __fat_stat_fill(f_size(f), f->obj.attr, st); + return 0; +} + +static int __fat_read(void *file, uint8_t *ptr, int len) +{ + UINT read; + FRESULT res = f_read(file, ptr, len, &read); + if (res != FR_OK) { + __fresult_set_errno(res); + return -1; + } + return read; +} + +static int __fat_write(void *file, uint8_t *ptr, int len) +{ + UINT written; + FRESULT res = f_write(file, ptr, len, &written); + if (res != FR_OK) { + __fresult_set_errno(res); + return -1; + } + return written; +} + +static int __fat_close(void *file) +{ + FIL *fp = file; + FRESULT res = f_close(fp); + + if (fp >= static_fat_files && fp < static_fat_files+NUM_STATIC_FAT_FILES) + fp->obj.fs = NULL; + else + free(fp); + + if (res != FR_OK) { + __fresult_set_errno(res); + return -1; + } + return 0; +} + +static int __fat_lseek(void *file, int offset, int whence) +{ + FRESULT res; + FIL *f = file; + switch (whence) + { + case SEEK_SET: res = f_lseek(f, offset); break; + case SEEK_CUR: res = f_lseek(f, f_tell(f) + offset); break; + case SEEK_END: res = f_lseek(f, f_size(f) + offset); break; + default: return -1; + } + if (res != FR_OK) { + __fresult_set_errno(res); + return -1; + } + return f_tell(f); +} + +static int __fat_ioctl(void *file, unsigned long request, void *arg) +{ + FIL *f = file; + switch (request) { + case IOFAT_GET_CLUSTER: { + int32_t *cluster = arg; + *cluster = f->clust; + return 0; + } + case IOFAT_GET_SECTOR: { + int64_t *sector = arg; + *sector = f->sect; + return 0; + } + case IOFAT_GET_CLUSTER_SIZE: { + int32_t *cluster_size = arg; + *cluster_size = f->obj.fs->csize; + return 0; + } + default: { + errno = ENOTTY; + return -1; + } + } +} + +static int __fat_unlink(char *name, int volid) +{ + FRESULT res = f_unlink(MAKE_FAT_NAME(volid, name)); + if (res != FR_OK) { + __fresult_set_errno(res); + return -1; + } + return 0; +} + +static int __fat_findnext(dir_t *dir) +{ + DIR *find_dir = (DIR*)dir->d_cookie; + FILINFO info; + FRESULT res = f_readdir(find_dir, &info); + if (res != FR_OK) { + __fresult_set_errno(res); + return -2; + } + + // Check if we reached the end of the directory + if (info.fname[0] == 0) { + res = f_closedir(find_dir); + free(find_dir); + dir->d_cookie = 0; + if (res != FR_OK) { + __fresult_set_errno(res); + return -2; + } + return -1; + } + + strlcpy(dir->d_name, info.fname, sizeof(dir->d_name)); + if (info.fattrib & AM_DIR) + dir->d_type = DT_DIR; + else + dir->d_type = DT_REG; + dir->d_size = info.fsize; + return 0; +} + +static int __fat_findfirst(char *name, dir_t *dir, int volid) +{ + DIR *find_dir = malloc(sizeof(DIR)); + if (!find_dir) { + errno = ENOMEM; + return -2; + } + FRESULT res = f_opendir(find_dir, MAKE_FAT_NAME(volid, name)); + if (res != FR_OK) { + free(find_dir); + __fresult_set_errno(res); + return -2; + } + dir->d_cookie = (uint32_t)find_dir; + return __fat_findnext(dir); +} + +static int __fat_mkdir(char *path, mode_t mode, int volid) +{ + FRESULT res = f_mkdir(MAKE_FAT_NAME(volid, path)); + if (res != FR_OK) { + __fresult_set_errno(res); + return -1; + } + return 0; +} + +static int __fat_utimes(const char *path, const struct timeval times[2], int volid) +{ + // FatFS does not support setting access time, so we ignore times[0] + // We also ignore sub-second precision in times[1] + time_t t = times[1].tv_sec; + + // FAT valid time range is 1980-01-01 00:00:00 to 2107-12-31 23:59:58 + t = CLAMP(t, 315532800, 4354819198); + + // Extract components + struct tm tm; + localtime_r(&t, &tm); + + // Convert to FAT date/time format + FILINFO fno = {0}; + fno.fdate = ((tm.tm_year + 1900 - 1980) << 9) | ((tm.tm_mon + 1) << 5) | tm.tm_mday; + fno.ftime = (tm.tm_hour << 11) | (tm.tm_min << 5) | (tm.tm_sec >> 1); + + FRESULT res = f_utime(MAKE_FAT_NAME(volid, path), &fno); + if (res != FR_OK) { + __fresult_set_errno(res); + return -1; + } + return 0; +} + +static const filesystem_t fat_newlib_fs = { + .open = NULL, // per-volume function + .stat = NULL, // per-volume function + .fstat = __fat_fstat, + .lseek = __fat_lseek, + .read = __fat_read, + .write = __fat_write, + .close = __fat_close, + .ioctl = __fat_ioctl, + .unlink = NULL, // per-volume function + .findfirst = NULL, // per-volume function + .findnext = __fat_findnext, + .mkdir = NULL, // per-volume function + .utimes = NULL, // per-volume function +}; + +static void *__fat_open_vol0(char *name, int flags) { return __fat_open(name, flags, 0); } +static void *__fat_open_vol1(char *name, int flags) { return __fat_open(name, flags, 1); } +static void *__fat_open_vol2(char *name, int flags) { return __fat_open(name, flags, 2); } +static void *__fat_open_vol3(char *name, int flags) { return __fat_open(name, flags, 3); } + +static int __fat_findfirst_vol0(char *name, dir_t *dir) { return __fat_findfirst(name, dir, 0); } +static int __fat_findfirst_vol1(char *name, dir_t *dir) { return __fat_findfirst(name, dir, 1); } +static int __fat_findfirst_vol2(char *name, dir_t *dir) { return __fat_findfirst(name, dir, 2); } +static int __fat_findfirst_vol3(char *name, dir_t *dir) { return __fat_findfirst(name, dir, 3); } + +static int __fat_stat_vol0(char *name, struct stat *st) { return __fat_stat(name, st, 0); } +static int __fat_stat_vol1(char *name, struct stat *st) { return __fat_stat(name, st, 1); } +static int __fat_stat_vol2(char *name, struct stat *st) { return __fat_stat(name, st, 2); } +static int __fat_stat_vol3(char *name, struct stat *st) { return __fat_stat(name, st, 3); } + +static int __fat_unlink_vol0(char *name) { return __fat_unlink(name, 0); } +static int __fat_unlink_vol1(char *name) { return __fat_unlink(name, 1); } +static int __fat_unlink_vol2(char *name) { return __fat_unlink(name, 2); } +static int __fat_unlink_vol3(char *name) { return __fat_unlink(name, 3); } + +static int __fat_mkdir_vol0(char *path, mode_t mode) { return __fat_mkdir(path, mode, 0); } +static int __fat_mkdir_vol1(char *path, mode_t mode) { return __fat_mkdir(path, mode, 1); } +static int __fat_mkdir_vol2(char *path, mode_t mode) { return __fat_mkdir(path, mode, 2); } +static int __fat_mkdir_vol3(char *path, mode_t mode) { return __fat_mkdir(path, mode, 3); } + +static int __fat_utimes_vol0(char *path, struct timeval times[2]) { return __fat_utimes(path, times, 0); } +static int __fat_utimes_vol1(char *path, struct timeval times[2]) { return __fat_utimes(path, times, 1); } +static int __fat_utimes_vol2(char *path, struct timeval times[2]) { return __fat_utimes(path, times, 2); } +static int __fat_utimes_vol3(char *path, struct timeval times[2]) { return __fat_utimes(path, times, 3); } + +static const void (*__fat_open_func[4]) = { __fat_open_vol0, __fat_open_vol1, __fat_open_vol2, __fat_open_vol3 }; +static const void (*__fat_findfirst_func[4]) = { __fat_findfirst_vol0, __fat_findfirst_vol1, __fat_findfirst_vol2, __fat_findfirst_vol3 }; +static const void (*__fat_stat_func[4]) = { __fat_stat_vol0, __fat_stat_vol1, __fat_stat_vol2, __fat_stat_vol3 }; +static const void (*__fat_unlink_func[4]) = { __fat_unlink_vol0, __fat_unlink_vol1, __fat_unlink_vol2, __fat_unlink_vol3 }; +static const void (*__fat_mkdir_func[4]) = { __fat_mkdir_vol0, __fat_mkdir_vol1, __fat_mkdir_vol2, __fat_mkdir_vol3 }; +static const void (*__fat_utimes_func[4]) = { __fat_utimes_vol0, __fat_utimes_vol1, __fat_utimes_vol2, __fat_utimes_vol3 }; + +int fat_mount(const char *prefix, const fat_disk_t* disk, int flags) +{ + int vol_id; + + for (vol_id = 0; vol_id < FF_VOLUMES; vol_id++) { + if (fat_disks[vol_id].disk_initialize == NULL) + break; + } + assertf(vol_id != FF_VOLUMES, "Not enough FAT volumes available (max: %d)", FF_VOLUMES); + + fat_disks[vol_id] = *disk; + fat_filesystems[vol_id] = NULL; + + FATFS *fatfs = malloc(sizeof(FATFS)); + assertf(fatfs, "Out of memory"); + char path[3] = {'0' + vol_id, ':', 0}; + + FRESULT err = f_mount(fatfs, path, (flags & FAT_MOUNT_DEFERRED) ? 0 : 1); + if (err != FR_OK) { + __fresult_set_errno(err); + free(fatfs); + fat_disks[vol_id] = (fat_disk_t){0}; + return -1; + } + fat_volumes[vol_id] = fatfs; + + if (prefix) { + filesystem_t *newlib_fs = malloc(sizeof(filesystem_t)); + assertf(newlib_fs, "Out of memory"); + memcpy(newlib_fs, &fat_newlib_fs, sizeof(filesystem_t)); + + newlib_fs->open = __fat_open_func[vol_id]; + newlib_fs->findfirst = __fat_findfirst_func[vol_id]; + newlib_fs->stat = __fat_stat_func[vol_id]; + newlib_fs->unlink = __fat_unlink_func[vol_id]; + newlib_fs->mkdir = __fat_mkdir_func[vol_id]; + newlib_fs->utimes = __fat_utimes_func[vol_id]; + + if (attach_filesystem(prefix, newlib_fs) < 0) { + f_mount(NULL, path, 0); + free(fatfs); + fat_volumes[vol_id] = NULL; + free(newlib_fs); + fat_disks[vol_id] = (fat_disk_t){0}; + return -1; + } + + fat_filesystems[vol_id] = newlib_fs; + } + + return vol_id; +} + +int fat_unmount(int vol_id) +{ + if (vol_id < 0 || vol_id >= FF_VOLUMES || fat_volumes[vol_id] == NULL) { + errno = EINVAL; + return -1; + } + + char path[3] = {'0' + vol_id, ':', 0}; + FRESULT err = f_mount(NULL, path, 0); + if (err != FR_OK) { + __fresult_set_errno(err); + return -1; + } + + int detach_errno = 0; + if (fat_filesystems[vol_id] && detach_filesystem_by_pointer(fat_filesystems[vol_id]) < 0) + detach_errno = errno; + + free(fat_filesystems[vol_id]); + fat_filesystems[vol_id] = NULL; + free(fat_volumes[vol_id]); + fat_volumes[vol_id] = NULL; + fat_disks[vol_id] = (fat_disk_t){0}; + + if (detach_errno) { + errno = detach_errno; + return -1; + } + return 0; +} diff --git a/src/fatfs/00history.txt b/src/fatfs/00history.txt index 29c76b076..8d410b2a6 100644 --- a/src/fatfs/00history.txt +++ b/src/fatfs/00history.txt @@ -375,3 +375,15 @@ R0.15a (November 22, 2024) Made f_setlabel() accept a volume label in Unix style volume ID when FF_STR_VOLUME_ID == 2. Made FatFs update PercInUse field in exFAT VBR. (A preceding f_getfree() is needed for the accuracy) + + +R0.15b (June 21, 2025) + Added support for timestamp of created time. (FF_FS_CRTIME) + Fixed FatFs fails to load the FsInfo in FAT32 volumes and the f_getfree always be forced a full FAT scan which takes a long time. (appeared at R0.15a) + + + +R0.16 (July 22, 2025) + Removed a long-pending limitation that f_getcwd and double-dot .. in the path name did not work on the exFAT volume. + Fixed f_readdir cannot detect end of directory and it leads the application process into infinite loop. (appeared at R0.15b) + Fixed dot names with terminating separator or duplicated separator are rejected when LFN is not enabled. diff --git a/src/fatfs/00readme.txt b/src/fatfs/00readme.txt index edd62e2a9..19fdc31cd 100644 --- a/src/fatfs/00readme.txt +++ b/src/fatfs/00readme.txt @@ -1,4 +1,4 @@ -FatFs Module Source Files R0.15a +FatFs Module Source Files R0.16 w/patch 2 FILES diff --git a/src/fatfs/ff.c b/src/fatfs/ff.c index ccd7b1594..83c7c468f 100644 --- a/src/fatfs/ff.c +++ b/src/fatfs/ff.c @@ -1,8 +1,8 @@ /*----------------------------------------------------------------------------/ -/ FatFs - Generic FAT Filesystem Module R0.15a / +/ FatFs - Generic FAT Filesystem Module R0.16 w/patch 2 / /-----------------------------------------------------------------------------/ / -/ Copyright (C) 2024, ChaN, all right reserved. +/ Copyright (C) 2025, ChaN, all right reserved. / / FatFs module is an open source software. Redistribution and use of FatFs in / source and binary forms, with or without modification, are permitted provided @@ -20,9 +20,8 @@ #include -#include "ff.h" /* Declarations of FatFs API */ -#include "diskio.h" /* Declarations of device I/O functions */ - +#include "ff.h" /* Basic definitions and declarations of API */ +#include "diskio.h" /* Declarations of MAI */ /*-------------------------------------------------------------------------- @@ -30,7 +29,7 @@ ---------------------------------------------------------------------------*/ -#if FF_DEFINED != 5380 /* Revision ID */ +#if FF_DEFINED != 80386 /* Revision ID */ #error Wrong include file (ff.h). #endif @@ -42,6 +41,9 @@ #define MAX_FAT16 0xFFF5 /* Max FAT16 clusters (differs from specs, but right for real DOS/Windows behavior) */ #define MAX_FAT32 0x0FFFFFF5 /* Max FAT32 clusters (not defined in specs, practical limit) */ #define MAX_EXFAT 0x7FFFFFFD /* Max exFAT clusters (differs from specs, implementation limit) */ +#define MIN_EXFAT 0x00000100 /* Min exFAT clusters (Not defined in specs, implementation limit) */ +#define MIN_FAT12 32 /* Min FAT12 clusters (Not defined in specs, implementation limit) */ +#define MIN_VOLUME 64 /* Min volume sectors (Not defined in specs, implementation limit) */ /* Character code support macros */ @@ -194,7 +196,7 @@ #define FSI_StrucSig 484 /* FAT32 FSI: Structure signature (DWORD) */ #define FSI_Free_Count 488 /* FAT32 FSI: Number of free clusters (DWORD) */ #define FSI_Nxt_Free 492 /* FAT32 FSI: Last allocated cluster (DWORD) */ -#define FSI_TrailSig 498 /* FAT32 FSI: Trailing signature (DWORD) */ +#define FSI_TrailSig 508 /* FAT32 FSI: Trailing signature (DWORD) */ #define MBR_Table 446 /* MBR: Offset of partition table in the MBR */ #define SZ_PTE 16 /* MBR: Size of a partition table entry */ @@ -222,7 +224,7 @@ #define GPTH_PtNum 80 /* GPT HDR: Number of table entries (DWORD) */ #define GPTH_PteSize 84 /* GPT HDR: Size of table entry (DWORD) */ #define GPTH_PtBcc 88 /* GPT HDR: Partition table BCC (DWORD) */ -#define SZ_GPTE 128 /* GPT PTE: Size of partition table entry */ +#define SZ_GPTE 128 /* GPT PTE: Size of a GPT partition table entry */ #define GPTE_PtGuid 0 /* GPT PTE: Partition type GUID (16-byte) */ #define GPTE_UpGuid 16 /* GPT PTE: Partition unique GUID (16-byte) */ #define GPTE_FstLba 32 /* GPT PTE: First LBA of partition (QWORD) */ @@ -465,7 +467,7 @@ typedef struct { /* Open object identifier with status */ static FATFS *FatFs[FF_VOLUMES]; /* Pointer to the filesystem objects (logical drives) */ static WORD Fsid; /* Filesystem mount ID */ -#if FF_FS_RPATH != 0 +#if FF_FS_RPATH static BYTE CurrVol; /* Current drive number set by f_chdrive() */ #endif @@ -500,9 +502,9 @@ static const BYTE GUID_MS_Basic[16] = {0xA2,0xA0,0xD0,0xEB,0xE5,0xB9,0x33,0x44,0 #if FF_FS_EXFAT #error LFN must be enabled when enable exFAT #endif -#define DEF_NAMBUF -#define INIT_NAMBUF(fs) -#define FREE_NAMBUF() +#define DEF_NAMEBUFF +#define INIT_NAMEBUFF(fs) +#define FREE_NAMEBUFF() #define LEAVE_MKFS(res) return res #else /* LFN configurations */ @@ -523,32 +525,32 @@ static const BYTE LfnOfs[] = {1,3,5,7,9,14,16,18,20,22,24,28,30}; /* FAT: Offset static BYTE DirBuf[MAXDIRB(FF_MAX_LFN)]; /* Directory entry block scratchpad buffer */ #endif static WCHAR LfnBuf[FF_MAX_LFN + 1]; /* LFN working buffer */ -#define DEF_NAMBUF -#define INIT_NAMBUF(fs) -#define FREE_NAMBUF() +#define DEF_NAMEBUFF +#define INIT_NAMEBUFF(fs) +#define FREE_NAMEBUFF() #define LEAVE_MKFS(res) return res #elif FF_USE_LFN == 2 /* LFN enabled with dynamic working buffer on the stack */ #if FF_FS_EXFAT -#define DEF_NAMBUF WCHAR lbuf[FF_MAX_LFN+1]; BYTE dbuf[MAXDIRB(FF_MAX_LFN)]; /* LFN working buffer and directory entry block scratchpad buffer */ -#define INIT_NAMBUF(fs) { (fs)->lfnbuf = lbuf; (fs)->dirbuf = dbuf; } -#define FREE_NAMBUF() +#define DEF_NAMEBUFF WCHAR lbuf[FF_MAX_LFN+1]; BYTE dbuf[MAXDIRB(FF_MAX_LFN)]; /* LFN working buffer and directory entry block scratchpad buffer */ +#define INIT_NAMEBUFF(fs) { (fs)->lfnbuf = lbuf; (fs)->dirbuf = dbuf; } +#define FREE_NAMEBUFF() #else -#define DEF_NAMBUF WCHAR lbuf[FF_MAX_LFN+1]; /* LFN working buffer */ -#define INIT_NAMBUF(fs) { (fs)->lfnbuf = lbuf; } -#define FREE_NAMBUF() +#define DEF_NAMEBUFF WCHAR lbuf[FF_MAX_LFN+1]; /* LFN working buffer */ +#define INIT_NAMEBUFF(fs) { (fs)->lfnbuf = lbuf; } +#define FREE_NAMEBUFF() #endif #define LEAVE_MKFS(res) return res #elif FF_USE_LFN == 3 /* LFN enabled with dynamic working buffer on the heap */ #if FF_FS_EXFAT -#define DEF_NAMBUF WCHAR *lfn; /* Pointer to LFN working buffer and directory entry block scratchpad buffer */ -#define INIT_NAMBUF(fs) { lfn = ff_memalloc((FF_MAX_LFN+1)*2 + MAXDIRB(FF_MAX_LFN)); if (!lfn) LEAVE_FF(fs, FR_NOT_ENOUGH_CORE); (fs)->lfnbuf = lfn; (fs)->dirbuf = (BYTE*)(lfn+FF_MAX_LFN+1); } -#define FREE_NAMBUF() ff_memfree(lfn) +#define DEF_NAMEBUFF WCHAR *lfn; /* Pointer to LFN working buffer and directory entry block scratchpad buffer */ +#define INIT_NAMEBUFF(fs) { lfn = ff_memalloc((FF_MAX_LFN+1)*2 + MAXDIRB(FF_MAX_LFN)); if (!lfn) LEAVE_FF(fs, FR_NOT_ENOUGH_CORE); (fs)->lfnbuf = lfn; (fs)->dirbuf = (BYTE*)(lfn+FF_MAX_LFN+1); } +#define FREE_NAMEBUFF() ff_memfree(lfn) #else -#define DEF_NAMBUF WCHAR *lfn; /* Pointer to LFN working buffer */ -#define INIT_NAMBUF(fs) { lfn = ff_memalloc((FF_MAX_LFN+1)*2); if (!lfn) LEAVE_FF(fs, FR_NOT_ENOUGH_CORE); (fs)->lfnbuf = lfn; } -#define FREE_NAMBUF() ff_memfree(lfn) +#define DEF_NAMEBUFF WCHAR *lfn; /* Pointer to LFN working buffer */ +#define INIT_NAMEBUFF(fs) { lfn = ff_memalloc((FF_MAX_LFN+1)*2); if (!lfn) LEAVE_FF(fs, FR_NOT_ENOUGH_CORE); (fs)->lfnbuf = lfn; } +#define FREE_NAMEBUFF() ff_memfree(lfn) #endif #define LEAVE_MKFS(res) { if (!work) ff_memfree(buf); return res; } #define MAX_MALLOC 0x8000 /* Must be >=FF_MAX_SS */ @@ -617,7 +619,7 @@ static const BYTE DbcTbl[] = MKCVTBL(TBL_DC, FF_CODE_PAGE); /* Load/Store multi-byte word in the FAT structure */ /*-----------------------------------------------------------------------*/ -static WORD ld_word (const BYTE* ptr) /* Load a 2-byte little-endian word */ +static WORD ld_16 (const BYTE* ptr) /* Load a 2-byte little-endian word */ { WORD rv; @@ -626,7 +628,7 @@ static WORD ld_word (const BYTE* ptr) /* Load a 2-byte little-endian word */ return rv; } -static DWORD ld_dword (const BYTE* ptr) /* Load a 4-byte little-endian word */ +static DWORD ld_32 (const BYTE* ptr) /* Load a 4-byte little-endian word */ { DWORD rv; @@ -638,7 +640,7 @@ static DWORD ld_dword (const BYTE* ptr) /* Load a 4-byte little-endian word */ } #if FF_FS_EXFAT -static QWORD ld_qword (const BYTE* ptr) /* Load an 8-byte little-endian word */ +static QWORD ld_64 (const BYTE* ptr) /* Load an 8-byte little-endian word */ { QWORD rv; @@ -655,13 +657,13 @@ static QWORD ld_qword (const BYTE* ptr) /* Load an 8-byte little-endian word */ #endif #if !FF_FS_READONLY -static void st_word (BYTE* ptr, WORD val) /* Store a 2-byte word in little-endian */ +static void st_16 (BYTE* ptr, WORD val) /* Store a 2-byte word in little-endian */ { *ptr++ = (BYTE)val; val >>= 8; *ptr++ = (BYTE)val; } -static void st_dword (BYTE* ptr, DWORD val) /* Store a 4-byte word in little-endian */ +static void st_32 (BYTE* ptr, DWORD val) /* Store a 4-byte word in little-endian */ { *ptr++ = (BYTE)val; val >>= 8; *ptr++ = (BYTE)val; val >>= 8; @@ -670,7 +672,7 @@ static void st_dword (BYTE* ptr, DWORD val) /* Store a 4-byte word in little-end } #if FF_FS_EXFAT -static void st_qword (BYTE* ptr, QWORD val) /* Store an 8-byte word in little-endian */ +static void st_64 (BYTE* ptr, QWORD val) /* Store an 8-byte word in little-endian */ { *ptr++ = (BYTE)val; val >>= 8; *ptr++ = (BYTE)val; val >>= 8; @@ -1122,11 +1124,11 @@ static FRESULT sync_fs ( /* Returns FR_OK or FR_DISK_ERR */ if (fs->fs_type == FS_FAT32) { /* FAT32: Update FSInfo sector */ /* Create FSInfo structure */ memset(fs->win, 0, sizeof fs->win); - st_dword(fs->win + FSI_LeadSig, 0x41615252); /* Leading signature */ - st_dword(fs->win + FSI_StrucSig, 0x61417272); /* Structure signature */ - st_dword(fs->win + FSI_Free_Count, fs->free_clst); /* Number of free clusters */ - st_dword(fs->win + FSI_Nxt_Free, fs->last_clst); /* Last allocated culuster */ - st_dword(fs->win + FSI_TrailSig, 0xAA550000); /* Trailing signature */ + st_32(fs->win + FSI_LeadSig, 0x41615252); /* Leading signature */ + st_32(fs->win + FSI_StrucSig, 0x61417272); /* Structure signature */ + st_32(fs->win + FSI_Free_Count, fs->free_clst); /* Number of free clusters */ + st_32(fs->win + FSI_Nxt_Free, fs->last_clst); /* Last allocated culuster */ + st_32(fs->win + FSI_TrailSig, 0xAA550000); /* Trailing signature */ disk_write(fs->pdrv, fs->win, fs->winsect = fs->volbase + 1, 1); /* Write it into the FSInfo sector (Next to VBR) */ } #if FF_FS_EXFAT @@ -1202,12 +1204,12 @@ static DWORD get_fat ( /* 0xFFFFFFFF:Disk error, 1:Internal error, 2..0x7FFFFFF case FS_FAT16 : if (move_window(fs, fs->fatbase + (clst / (SS(fs) / 2))) != FR_OK) break; - val = ld_word(fs->win + clst * 2 % SS(fs)); /* Simple WORD array */ + val = ld_16(fs->win + clst * 2 % SS(fs)); /* Simple WORD array */ break; case FS_FAT32 : if (move_window(fs, fs->fatbase + (clst / (SS(fs) / 4))) != FR_OK) break; - val = ld_dword(fs->win + clst * 4 % SS(fs)) & 0x0FFFFFFF; /* Simple DWORD array but mask out upper 4 bits */ + val = ld_32(fs->win + clst * 4 % SS(fs)) & 0x0FFFFFFF; /* Simple DWORD array but mask out upper 4 bits */ break; #if FF_FS_EXFAT case FS_EXFAT : @@ -1228,7 +1230,7 @@ static DWORD get_fat ( /* 0xFFFFFFFF:Disk error, 1:Internal error, 2..0x7FFFFFF val = 0x7FFFFFFF; /* Generate EOC */ } else { if (move_window(fs, fs->fatbase + (clst / (SS(fs) / 4))) != FR_OK) break; - val = ld_dword(fs->win + clst * 4 % SS(fs)) & 0x7FFFFFFF; + val = ld_32(fs->win + clst * 4 % SS(fs)) & 0x7FFFFFFF; } break; } @@ -1282,7 +1284,7 @@ static FRESULT put_fat ( /* FR_OK(0):succeeded, !=0:error */ case FS_FAT16: res = move_window(fs, fs->fatbase + (clst / (SS(fs) / 2))); if (res != FR_OK) break; - st_word(fs->win + clst * 2 % SS(fs), (WORD)val); /* Simple WORD array */ + st_16(fs->win + clst * 2 % SS(fs), (WORD)val); /* Simple WORD array */ fs->wflag = 1; break; @@ -1293,9 +1295,9 @@ static FRESULT put_fat ( /* FR_OK(0):succeeded, !=0:error */ res = move_window(fs, fs->fatbase + (clst / (SS(fs) / 4))); if (res != FR_OK) break; if (!FF_FS_EXFAT || fs->fs_type != FS_EXFAT) { - val = (val & 0x0FFFFFFF) | (ld_dword(fs->win + clst * 4 % SS(fs)) & 0xF0000000); + val = (val & 0x0FFFFFFF) | (ld_32(fs->win + clst * 4 % SS(fs)) & 0xF0000000); } - st_dword(fs->win + clst * 4 % SS(fs), val); + st_32(fs->win + clst * 4 % SS(fs), val); fs->wflag = 1; break; } @@ -1870,9 +1872,9 @@ static DWORD ld_clust ( /* Returns the top cluster value of the SFN entry */ { DWORD cl; - cl = ld_word(dir + DIR_FstClusLO); + cl = ld_16(dir + DIR_FstClusLO); if (fs->fs_type == FS_FAT32) { - cl |= (DWORD)ld_word(dir + DIR_FstClusHI) << 16; + cl |= (DWORD)ld_16(dir + DIR_FstClusHI) << 16; } return cl; @@ -1886,9 +1888,9 @@ static void st_clust ( DWORD cl /* Value to be set */ ) { - st_word(dir + DIR_FstClusLO, (WORD)cl); + st_16(dir + DIR_FstClusLO, (WORD)cl); if (fs->fs_type == FS_FAT32) { - st_word(dir + DIR_FstClusHI, (WORD)(cl >> 16)); + st_16(dir + DIR_FstClusHI, (WORD)(cl >> 16)); } } #endif @@ -1909,12 +1911,12 @@ static int cmp_lfn ( /* 1:matched, 0:not matched */ WCHAR pchr, chr; - if (ld_word(dir + LDIR_FstClusLO) != 0) return 0; /* Check if LDIR_FstClusLO is 0 */ + if (ld_16(dir + LDIR_FstClusLO) != 0) return 0; /* Check if LDIR_FstClusLO is 0 */ ni = (UINT)((dir[LDIR_Ord] & 0x3F) - 1) * 13; /* Offset in the name to be compared */ for (pchr = 1, di = 0; di < 13; di++) { /* Process all characters in the entry */ - chr = ld_word(dir + LfnOfs[di]); /* Pick a character from the entry */ + chr = ld_16(dir + LfnOfs[di]); /* Pick a character from the entry */ if (pchr != 0) { if (ni >= FF_MAX_LFN + 1 || ff_wtoupper(chr) != ff_wtoupper(lfnbuf[ni++])) { /* Compare it with name */ return 0; /* Not matched */ @@ -1945,12 +1947,12 @@ static int pick_lfn ( /* 1:succeeded, 0:buffer overflow or invalid LFN entry */ WCHAR pchr, chr; - if (ld_word(dir + LDIR_FstClusLO) != 0) return 0; /* Check if LDIR_FstClusLO is 0 */ + if (ld_16(dir + LDIR_FstClusLO) != 0) return 0; /* Check if LDIR_FstClusLO is 0 */ ni = (UINT)((dir[LDIR_Ord] & ~LLEF) - 1) * 13; /* Offset in the name buffer */ for (pchr = 1, di = 0; di < 13; di++) { /* Process all characters in the entry */ - chr = ld_word(dir + LfnOfs[di]); /* Pick a character from the entry */ + chr = ld_16(dir + LfnOfs[di]); /* Pick a character from the entry */ if (pchr != 0) { if (ni >= FF_MAX_LFN + 1) return 0; /* Buffer overflow? */ lfnbuf[ni++] = pchr = chr; /* Store it */ @@ -1988,13 +1990,13 @@ static void put_lfn ( dir[LDIR_Chksum] = sum; /* Set checksum */ dir[LDIR_Attr] = AM_LFN; /* Set attribute */ dir[LDIR_Type] = 0; - st_word(dir + LDIR_FstClusLO, 0); + st_16(dir + LDIR_FstClusLO, 0); ni = (UINT)(ord - 1) * 13; /* Offset in the name */ di = chr = 0; do { /* Fill the directory entry */ if (chr != 0xFFFF) chr = lfn[ni++]; /* Get an effective character */ - st_word(dir + LfnOfs[di], chr); /* Set it */ + st_16(dir + LfnOfs[di], chr); /* Set it */ if (chr == 0) chr = 0xFFFF; /* Padding characters after the terminator */ } while (++di < 13); if (chr == 0xFFFF || !lfn[ni]) ord |= LLEF; /* Last LFN part is the start of an enrty set */ @@ -2015,19 +2017,19 @@ static void gen_numname ( BYTE* dst, /* Pointer to the buffer to store numbered SFN */ const BYTE* src, /* Pointer to SFN in directory form */ const WCHAR* lfn, /* Pointer to LFN */ - UINT seq /* Sequence number */ + WORD seq /* Sequence number */ ) { BYTE ns[8], c; UINT i, j; - WCHAR wc; - DWORD crc_sreg; memcpy(dst, src, 11); /* Prepare the SFN to be modified */ if (seq > 5) { /* In case of many collisions, generate a hash number instead of sequential number */ - crc_sreg = seq; + WCHAR wc; + DWORD crc_sreg = seq; + while (*lfn) { /* Create a CRC value as a hash of LFN */ wc = *lfn++; for (i = 0; i < 16; i++) { @@ -2036,10 +2038,10 @@ static void gen_numname ( if (crc_sreg & 0x10000) crc_sreg ^= 0x11021; } } - seq = (UINT)crc_sreg; + seq = (WORD)crc_sreg; } - /* Make suffix (~ + hexdecimal) */ + /* Make suffix (~ + 4-digit hexadecimal) */ i = 7; do { c = (BYTE)((seq % 16) + '0'); seq /= 16; @@ -2186,7 +2188,7 @@ static FRESULT load_xdir ( /* FR_INT_ERR: invalid entry block */ /* Sanity check (do it for only accessible object) */ if (i <= MAXDIRB(FF_MAX_LFN)) { - if (xdir_sum(dirb) != ld_word(dirb + XDIR_SetSum)) return FR_INT_ERR; + if (xdir_sum(dirb) != ld_16(dirb + XDIR_SetSum)) return FR_INT_ERR; } return FR_OK; @@ -2198,19 +2200,27 @@ static FRESULT load_xdir ( /* FR_INT_ERR: invalid entry block */ /*------------------------------------------------------------------*/ static void init_alloc_info ( - FATFS* fs, /* Filesystem object */ - FFOBJID* obj /* Object allocation information to be initialized */ + FFOBJID* dobj, /* Object allocation information to be initialized */ + DIR* sdir /* Additional source about containing direcotry */ ) { - obj->sclust = ld_dword(fs->dirbuf + XDIR_FstClus); /* Start cluster */ - obj->objsize = ld_qword(fs->dirbuf + XDIR_FileSize); /* Size */ - obj->stat = fs->dirbuf[XDIR_GenFlags] & 2; /* Allocation status */ - obj->n_frag = 0; /* No last fragment info */ + FATFS *fs = dobj->fs; + + + if (sdir) { /* Initialize the containing directory. This block needs to precede the followings. */ + dobj->c_scl = sdir->obj.sclust; + dobj->c_size = ((DWORD)sdir->obj.objsize & 0xFFFFFF00) | sdir->obj.stat; + dobj->c_ofs = sdir->blk_ofs; + } + dobj->sclust = ld_32(fs->dirbuf + XDIR_FstClus); /* Start cluster */ + dobj->objsize = ld_64(fs->dirbuf + XDIR_FileSize); /* Size */ + dobj->stat = fs->dirbuf[XDIR_GenFlags] & 2; /* Allocation status */ + dobj->n_frag = 0; /* No last fragment info */ } -#if !FF_FS_READONLY || FF_FS_RPATH != 0 +#if !FF_FS_READONLY || FF_FS_RPATH /*------------------------------------------------*/ /* exFAT: Load the object's directory entry block */ /*------------------------------------------------*/ @@ -2253,7 +2263,7 @@ static FRESULT store_xdir ( BYTE *dirb = dp->obj.fs->dirbuf; /* Pointer to the entry set 85+C0+C1s */ - st_word(dirb + XDIR_SetSum, xdir_sum(dirb)); /* Create check sum */ + st_16(dirb + XDIR_SetSum, xdir_sum(dirb)); /* Create check sum */ /* Store the entry set to the directory */ nent = dirb[XDIR_NumSec] + 1; /* Number of entries */ @@ -2300,7 +2310,7 @@ static void create_xdir ( dirb[i++] = ET_FILENAME; dirb[i++] = 0; do { /* Fill name field */ if (chr != 0 && (chr = lfn[nlen]) != 0) nlen++; /* Get a character if exist */ - st_word(dirb + i, chr); /* Store it */ + st_16(dirb + i, chr); /* Store it */ i += 2; } while (i % SZDIRE != 0); n_c1++; @@ -2308,7 +2318,7 @@ static void create_xdir ( dirb[XDIR_NumName] = nlen; /* Set name length */ dirb[XDIR_NumSec] = 1 + n_c1; /* Set secondary count (C0 + C1s) */ - st_word(dirb + XDIR_NameHash, xname_sum(lfn)); /* Set name hash */ + st_16(dirb + XDIR_NameHash, xname_sum(lfn)); /* Set name hash */ } #endif /* !FF_FS_READONLY */ @@ -2331,7 +2341,7 @@ static FRESULT dir_read ( { FRESULT res = FR_NO_FILE; FATFS *fs = dp->obj.fs; - BYTE attr, b; + BYTE attr, et; #if FF_USE_LFN BYTE ord = 0xFF, sum = 0xFF; #endif @@ -2339,16 +2349,16 @@ static FRESULT dir_read ( while (dp->sect) { res = move_window(fs, dp->sect); if (res != FR_OK) break; - b = dp->dir[DIR_Name]; /* Test for the entry type */ - if (b == 0) { + et = dp->dir[DIR_Name]; /* Test for the entry type */ + if (et == 0) { res = FR_NO_FILE; break; /* Reached to end of the directory */ } #if FF_FS_EXFAT if (fs->fs_type == FS_EXFAT) { /* On the exFAT volume */ if (FF_USE_LABEL && vol) { - if (b == ET_VLABEL) break; /* Volume label entry? */ + if (et == ET_VLABEL) break; /* Volume label entry? */ } else { - if (b == ET_FILEDIR) { /* Start of the file entry block? */ + if (et == ET_FILEDIR) { /* Start of the file entry block? */ dp->blk_ofs = dp->dptr; /* Get location of the block */ res = load_xdir(dp); /* Load the entry block */ if (res == FR_OK) { @@ -2362,17 +2372,17 @@ static FRESULT dir_read ( { /* On the FAT/FAT32 volume */ dp->obj.attr = attr = dp->dir[DIR_Attr] & AM_MASK; /* Get attribute */ #if FF_USE_LFN /* LFN configuration */ - if (b == DDEM || b == '.' || (int)((attr & ~AM_ARC) == AM_VOL) != vol) { /* An entry without valid data */ + if (et == DDEM || et == '.' || (int)((attr & ~AM_ARC) == AM_VOL) != vol) { /* An entry without valid data */ ord = 0xFF; } else { if (attr == AM_LFN) { /* An LFN entry is found */ - if (b & LLEF) { /* Is it start of an LFN sequence? */ + if (et & LLEF) { /* Is it start of an LFN sequence? */ sum = dp->dir[LDIR_Chksum]; - b &= (BYTE)~LLEF; ord = b; + et &= (BYTE)~LLEF; ord = et; dp->blk_ofs = dp->dptr; } /* Check LFN validity and capture it */ - ord = (b == ord && sum == dp->dir[LDIR_Chksum] && pick_lfn(fs->lfnbuf, dp->dir)) ? ord - 1 : 0xFF; + ord = (et == ord && sum == dp->dir[LDIR_Chksum] && pick_lfn(fs->lfnbuf, dp->dir)) ? ord - 1 : 0xFF; } else { /* An SFN entry is found */ if (ord != 0 || sum != sum_sfn(dp->dir)) { /* Is there a valid LFN? */ dp->blk_ofs = 0xFFFFFFFF; /* It has no LFN. */ @@ -2381,7 +2391,7 @@ static FRESULT dir_read ( } } #else /* Non LFN configuration */ - if (b != DDEM && b != '.' && attr != AM_LFN && (int)((attr & ~AM_ARC) == AM_VOL) == vol) { /* Is it a valid entry? */ + if (et != DDEM && et != '.' && attr != AM_LFN && (int)((attr & ~AM_ARC) == AM_VOL) == vol) { /* Is it a valid entry? */ break; } #endif @@ -2408,9 +2418,9 @@ static FRESULT dir_find ( /* FR_OK(0):succeeded, !=0:error */ { FRESULT res; FATFS *fs = dp->obj.fs; - BYTE c; + BYTE et; #if FF_USE_LFN - BYTE a, ord, sum; + BYTE attr, ord, sum; #endif res = dir_sdi(dp, 0); /* Rewind directory object */ @@ -2425,10 +2435,10 @@ static FRESULT dir_find ( /* FR_OK(0):succeeded, !=0:error */ #if FF_MAX_LFN < 255 if (fs->dirbuf[XDIR_NumName] > FF_MAX_LFN) continue; /* Skip comparison if inaccessible object name */ #endif - if (ld_word(fs->dirbuf + XDIR_NameHash) != hash) continue; /* Skip comparison if hash mismatched */ + if (ld_16(fs->dirbuf + XDIR_NameHash) != hash) continue; /* Skip comparison if hash mismatched */ for (nc = fs->dirbuf[XDIR_NumName], di = SZDIRE * 2, ni = 0; nc; nc--, di += 2, ni++) { /* Compare the name */ if ((di % SZDIRE) == 0) di += 2; - if (ff_wtoupper(ld_word(fs->dirbuf + di)) != ff_wtoupper(fs->lfnbuf[ni])) break; + if (ff_wtoupper(ld_16(fs->dirbuf + di)) != ff_wtoupper(fs->lfnbuf[ni])) break; } if (nc == 0 && !fs->lfnbuf[ni]) break; /* Name matched? */ } @@ -2442,23 +2452,23 @@ static FRESULT dir_find ( /* FR_OK(0):succeeded, !=0:error */ do { res = move_window(fs, dp->sect); if (res != FR_OK) break; - c = dp->dir[DIR_Name]; - if (c == 0) { res = FR_NO_FILE; break; } /* Reached end of directory table */ + et = dp->dir[DIR_Name]; /* Entry type */ + if (et == 0) { res = FR_NO_FILE; break; } /* Reached end of directory table */ #if FF_USE_LFN /* LFN configuration */ - dp->obj.attr = a = dp->dir[DIR_Attr] & AM_MASK; - if (c == DDEM || ((a & AM_VOL) && a != AM_LFN)) { /* An entry without valid data */ + dp->obj.attr = attr = dp->dir[DIR_Attr] & AM_MASK; + if (et == DDEM || ((attr & AM_VOL) && attr != AM_LFN)) { /* An entry without valid data */ ord = 0xFF; dp->blk_ofs = 0xFFFFFFFF; /* Reset LFN sequence */ } else { - if (a == AM_LFN) { /* Is it an LFN entry? */ + if (attr == AM_LFN) { /* Is it an LFN entry? */ if (!(dp->fn[NSFLAG] & NS_NOLFN)) { - if (c & LLEF) { /* Is it start of an entry set? */ - c &= (BYTE)~LLEF; - ord = c; /* Number of LFN entries */ + if (et & LLEF) { /* Is it start of an entry set? */ + et &= (BYTE)~LLEF; + ord = et; /* Number of LFN entries */ dp->blk_ofs = dp->dptr; /* Start offset of LFN */ sum = dp->dir[LDIR_Chksum]; /* Sum of the SFN */ } /* Check validity of the LFN entry and compare it with given name */ - ord = (c == ord && sum == dp->dir[LDIR_Chksum] && cmp_lfn(fs->lfnbuf, dp->dir)) ? ord - 1 : 0xFF; + ord = (et == ord && sum == dp->dir[LDIR_Chksum] && cmp_lfn(fs->lfnbuf, dp->dir)) ? ord - 1 : 0xFF; } } else { /* SFN entry */ if (ord == 0 && sum == sum_sfn(dp->dir)) break; /* LFN matched? */ @@ -2492,7 +2502,7 @@ static FRESULT dir_register ( /* FR_OK:succeeded, FR_DENIED:no free entry or too FATFS *fs = dp->obj.fs; #if FF_USE_LFN /* LFN configuration */ UINT n, len, n_ent; - BYTE sn[12], sum; + BYTE sn[12]; if (dp->fn[NSFLAG] & (NS_DOT | NS_NONAME)) return FR_INVALID_NAME; /* Check name validity */ @@ -2517,11 +2527,17 @@ static FRESULT dir_register ( /* FR_OK:succeeded, FR_DENIED:no free entry or too res = load_obj_xdir(&dj, &dp->obj); /* Load the object status */ if (res != FR_OK) return res; dp->obj.objsize += (DWORD)fs->csize * SS(fs); /* Increase the directory size by cluster size */ - st_qword(fs->dirbuf + XDIR_FileSize, dp->obj.objsize); - st_qword(fs->dirbuf + XDIR_ValidFileSize, dp->obj.objsize); - fs->dirbuf[XDIR_GenFlags] = dp->obj.stat | 1; /* Update the allocation status */ + st_64(fs->dirbuf + XDIR_FileSize, dp->obj.objsize); + st_64(fs->dirbuf + XDIR_ValidFileSize, dp->obj.objsize); + fs->dirbuf[XDIR_GenFlags] = dp->obj.stat | 1; /* Update the allocation status */ res = store_xdir(&dj); /* Store the object status */ if (res != FR_OK) return res; +#if FF_FS_RPATH /* Refrect changes to the current dir chain if the stretched dir is in it */ + for (n = 1; n <= fs->xcwds.depth && dp->obj.sclust != fs->xcwds.tbl[n].d_scl; n++) ; /* Check if the dir is in the current dir path */ + if (n <= fs->xcwds.depth) { /* If exist, update it */ + fs->xcwds.tbl[n].d_size = (DWORD)dp->obj.objsize | dp->obj.stat; + } +#endif } } @@ -2534,7 +2550,7 @@ static FRESULT dir_register ( /* FR_OK:succeeded, FR_DENIED:no free entry or too if (sn[NSFLAG] & NS_LOSS) { /* When LFN is out of 8.3 format, generate a numbered name */ dp->fn[NSFLAG] = NS_NOLFN; /* Find only SFN */ for (n = 1; n < 100; n++) { - gen_numname(dp->fn, sn, fs->lfnbuf, n); /* Generate a numbered name */ + gen_numname(dp->fn, sn, fs->lfnbuf, (WORD)n); /* Generate a numbered name */ res = dir_find(dp); /* Check if the name collides with existing SFN */ if (res != FR_OK) break; } @@ -2549,7 +2565,8 @@ static FRESULT dir_register ( /* FR_OK:succeeded, FR_DENIED:no free entry or too if (res == FR_OK && --n_ent) { /* Set LFN entry if needed */ res = dir_sdi(dp, dp->dptr - n_ent * SZDIRE); if (res == FR_OK) { - sum = sum_sfn(dp->fn); /* Checksum value of the SFN tied to the LFN */ + BYTE sum = sum_sfn(dp->fn); /* Checksum value of the SFN tied to the LFN */ + do { /* Store LFN entries in bottom first */ res = move_window(fs, dp->sect); if (res != FR_OK) break; @@ -2572,7 +2589,7 @@ static FRESULT dir_register ( /* FR_OK:succeeded, FR_DENIED:no free entry or too memset(dp->dir, 0, SZDIRE); /* Clean the entry */ memcpy(dp->dir + DIR_Name, dp->fn, 11); /* Put SFN */ #if FF_USE_LFN - dp->dir[DIR_NTres] = dp->fn[NSFLAG] & (NS_BODY | NS_EXT); /* Put NT flag */ + dp->dir[DIR_NTres] = dp->fn[NSFLAG] & (NS_BODY | NS_EXT); /* Put low-case flags */ #endif fs->wflag = 1; } @@ -2643,7 +2660,6 @@ static void get_fileinfo ( { UINT si, di; #if FF_USE_LFN - BYTE lcf; WCHAR wc, hs; FATFS *fs = dp->obj.fs; UINT nw; @@ -2652,7 +2668,7 @@ static void get_fileinfo ( #endif - fno->fname[0] = 0; /* Invaidate file info */ + fno->fname[0] = 0; if (dp->sect == 0) return; /* Exit if read pointer has reached end of directory */ #if FF_USE_LFN /* LFN configuration */ @@ -2667,7 +2683,7 @@ static void get_fileinfo ( di = 0; break; } if ((si % SZDIRE) == 0) si += 2; /* Skip entry type field */ - wc = ld_word(fs->dirbuf + si); si += 2; nc++; /* Get a character */ + wc = ld_16(fs->dirbuf + si); si += 2; nc++; /* Get a character */ if (hs == 0 && IsSurrogate(wc)) { /* Is it a surrogate? */ hs = wc; continue; /* Get low surrogate */ } @@ -2683,10 +2699,14 @@ static void get_fileinfo ( fno->fname[di] = 0; /* Terminate the name */ fno->altname[0] = 0; /* exFAT does not support SFN */ - fno->fattrib = fs->dirbuf[XDIR_Attr] & AM_MASKX; /* Attribute */ - fno->fsize = (fno->fattrib & AM_DIR) ? 0 : ld_qword(fs->dirbuf + XDIR_FileSize); /* Size */ - fno->ftime = ld_word(fs->dirbuf + XDIR_ModTime + 0); /* Time */ - fno->fdate = ld_word(fs->dirbuf + XDIR_ModTime + 2); /* Date */ + fno->fattrib = fs->dirbuf[XDIR_Attr] & AM_MASKX; /* Attribute */ + fno->fsize = (fno->fattrib & AM_DIR) ? 0 : ld_64(fs->dirbuf + XDIR_FileSize); /* Size */ + fno->ftime = ld_16(fs->dirbuf + XDIR_ModTime + 0); /* Last modified time */ + fno->fdate = ld_16(fs->dirbuf + XDIR_ModTime + 2); /* Last modified date */ +#if FF_FS_CRTIME + fno->crtime = ld_16(fs->dirbuf + XDIR_CrtTime + 0); /* Created time */ + fno->crdate = ld_16(fs->dirbuf + XDIR_CrtTime + 2); /* Created date */ +#endif return; } else #endif @@ -2736,14 +2756,16 @@ static void get_fileinfo ( } fno->altname[di] = 0; /* Terminate the SFN (null string means SFN is invalid) */ - if (fno->fname[0] == 0) { /* If LFN is invalid, altname[] needs to be copied to fname[] */ - if (di == 0) { /* If LFN and SFN both are invalid, this object is inaccessible */ - fno->fname[di++] = '\?'; + if (!fno->fname[0]) { /* If LFN is invalid, altname[] needs to be copied to fname[] */ + if (di == 0) { /* If LFN and SFN both are invalid, */ + fno->fname[di++] = '\?'; /* This object is inaccessible due to wrong buffer or locale settings */ } else { - for (si = di = 0, lcf = NS_BODY; fno->altname[si]; si++, di++) { /* Copy altname[] to fname[] with case information */ + BYTE lcflg = NS_BODY; + + for (si = di = 0; fno->altname[si]; si++, di++) { /* Copy altname[] to fname[] with case information */ wc = (WCHAR)fno->altname[si]; - if (wc == '.') lcf = NS_EXT; - if (IsUpper(wc) && (dp->dir[DIR_NTres] & lcf)) wc += 0x20; + if (wc == '.') lcflg = NS_EXT; + if (IsUpper(wc) && (dp->dir[DIR_NTres] & lcflg)) wc += 0x20; fno->fname[di] = (TCHAR)wc; } } @@ -2763,10 +2785,14 @@ static void get_fileinfo ( fno->fname[di] = 0; /* Terminate the SFN */ #endif - fno->fattrib = dp->dir[DIR_Attr] & AM_MASK; /* Attribute */ - fno->fsize = ld_dword(dp->dir + DIR_FileSize); /* Size */ - fno->ftime = ld_word(dp->dir + DIR_ModTime + 0); /* Time */ - fno->fdate = ld_word(dp->dir + DIR_ModTime + 2); /* Date */ + fno->fattrib = dp->dir[DIR_Attr] & AM_MASK; /* Attribute */ + fno->fsize = ld_32(dp->dir + DIR_FileSize); /* Size */ + fno->ftime = ld_16(dp->dir + DIR_ModTime + 0); /* Last modified time */ + fno->fdate = ld_16(dp->dir + DIR_ModTime + 2); /* Last Modified date */ +#if FF_FS_CRTIME + fno->crtime = ld_16(dp->dir + DIR_CrtTime + 0); /* Created time */ + fno->crdate = ld_16(dp->dir + DIR_CrtTime + 2); /* Created date */ +#endif } #endif /* FF_FS_MINIMIZE <= 1 || FF_FS_RPATH >= 2 */ @@ -2880,7 +2906,7 @@ static FRESULT create_name ( /* FR_OK: successful, FR_INVALID_NAME: could not cr UINT i, ni, si, di; - /* Create LFN into LFN working buffer */ + /* Create an LFN into LFN working buffer */ p = *path; lfn = dp->obj.fs->lfnbuf; di = 0; for (;;) { uc = tchar2uni(&p); /* Get a character */ @@ -2901,7 +2927,7 @@ static FRESULT create_name ( /* FR_OK: successful, FR_INVALID_NAME: could not cr } *path = p; /* Return pointer to the next segment */ -#if FF_FS_RPATH != 0 +#if FF_FS_RPATH if ((di == 1 && lfn[di - 1] == '.') || (di == 2 && lfn[di - 1] == '.' && lfn[di - 2] == '.')) { /* Is this segment a dot name? */ lfn[di] = 0; @@ -3008,14 +3034,19 @@ static FRESULT create_name ( /* FR_OK: successful, FR_INVALID_NAME: could not cr p = *path; sfn = dp->fn; memset(sfn, ' ', 11); si = i = 0; ni = 8; -#if FF_FS_RPATH != 0 +#if FF_FS_RPATH if (p[si] == '.') { /* Is this a dot entry? */ - for (;;) { + for (;;) { /* Copy one or two dots */ c = (BYTE)p[si++]; if (c != '.' || si >= 3) break; sfn[i++] = c; } - if (!IsSeparator(c) && c > ' ') return FR_INVALID_NAME; + if (IsSeparator(c)) { + while (IsSeparator(p[si])) si++; /* Skip duplicated separators */ + if ((BYTE)p[si] <= ' ') c = 0; /* Terminate if no segment follows */ + } else if (c > ' ') { /* Not in dot name */ + return FR_INVALID_NAME; + } *path = p + si; /* Return pointer to the next segment */ sfn[NSFLAG] = (c <= ' ') ? NS_LAST | NS_DOT : NS_DOT; /* Set last segment flag if end of the path */ return FR_OK; @@ -3025,7 +3056,7 @@ static FRESULT create_name ( /* FR_OK: successful, FR_INVALID_NAME: could not cr c = (BYTE)p[si++]; /* Get a byte */ if (c <= ' ') break; /* Break if end of the path name */ if (IsSeparator(c)) { /* Break if a separator is found */ - while (IsSeparator(p[si])) si++; /* Skip duplicated separator if exist */ + while (IsSeparator(p[si])) si++; /* Skip duplicated separators */ break; } if (c == '.' || i >= ni) { /* End of body or field overflow? */ @@ -3080,28 +3111,33 @@ static FRESULT follow_path ( /* FR_OK(0): successful, !=0: error code */ FATFS *fs = dp->obj.fs; -#if FF_FS_RPATH != 0 + /* Determins the start directory (current directory or forced root directory) */ +#if FF_FS_RPATH if (!IsSeparator(*path) && (FF_STR_VOLUME_ID != 2 || !IsTerminator(*path))) { /* Without heading separator */ dp->obj.sclust = fs->cdir; /* Start at the current directory */ } else #endif { /* With heading separator */ - while (IsSeparator(*path)) path++; /* Strip separators */ - dp->obj.sclust = 0; /* Start from the root directory */ + while (IsSeparator(*path)) path++; /* Strip heading separators */ + dp->obj.sclust = 0; /* Start at the root directory */ } + #if FF_FS_EXFAT dp->obj.n_frag = 0; /* Invalidate last fragment counter of the object */ -#if FF_FS_RPATH != 0 - if (fs->fs_type == FS_EXFAT && dp->obj.sclust) { /* exFAT: Retrieve the sub-directory's status */ - DIR dj; - - dp->obj.c_scl = fs->cdc_scl; - dp->obj.c_size = fs->cdc_size; - dp->obj.c_ofs = fs->cdc_ofs; - res = load_obj_xdir(&dj, &dp->obj); - if (res != FR_OK) return res; - dp->obj.objsize = ld_dword(fs->dirbuf + XDIR_FileSize); - dp->obj.stat = fs->dirbuf[XDIR_GenFlags] & 2; +#if FF_FS_RPATH + if (fs->fs_type == FS_EXFAT) { /* exFAT: Retrieve the start-directory's status */ + if (dp->obj.sclust) { /* Start directory is a sub-directory */ + /* Load the current directory chain into working buffer and initialize directory object as current dir */ + memcpy(&fs->xcwds2, &fs->xcwds, sizeof fs->xcwds2); + dp->obj.stat = (BYTE)fs->xcwds2.tbl[fs->xcwds2.depth].d_size; + dp->obj.objsize = fs->xcwds2.tbl[fs->xcwds2.depth].d_size & 0xFFFFFF00; + dp->obj.c_scl = fs->xcwds2.tbl[fs->xcwds2.depth - 1].d_scl; + dp->obj.c_size = fs->xcwds2.tbl[fs->xcwds2.depth - 1].d_size & 0xFFFFFF00; + dp->obj.c_ofs = fs->xcwds2.tbl[fs->xcwds2.depth - 1].nxt_ofs; + } else { /* Start directory is the root directory */ + /* Clear the directory path working buffer as root directory */ + memset(&fs->xcwds2, 0, sizeof fs->xcwds2); + } } #endif #endif @@ -3114,11 +3150,31 @@ static FRESULT follow_path ( /* FR_OK(0): successful, !=0: error code */ for (;;) { res = create_name(dp, &path); /* Get a segment name of the path */ if (res != FR_OK) break; - res = dir_find(dp); /* Find an object with the segment name */ ns = dp->fn[NSFLAG]; +#if FF_FS_EXFAT && FF_FS_RPATH + if (fs->fs_type == FS_EXFAT && (ns & NS_DOT)) { /* Is it a dot name? */ + /* There is no dot entry in exFAT volume, so it needs to follow the parent directory with recorded path */ + if (fs->lfnbuf[1] == '.' && fs->xcwds2.depth) { /* ".." in the sub-dir? */ + fs->xcwds2.depth--; /* Get into the parent directory and load the directory info */ + dp->obj.sclust = fs->xcwds2.tbl[fs->xcwds2.depth].d_scl; + dp->obj.stat = (BYTE)fs->xcwds2.tbl[fs->xcwds2.depth].d_size; + dp->obj.objsize = fs->xcwds2.tbl[fs->xcwds2.depth].d_size & 0xFFFFFF00; + if (fs->xcwds2.depth) { /* Load containing dir info if needed */ + dp->obj.c_scl = fs->xcwds2.tbl[fs->xcwds2.depth - 1].d_scl; + dp->obj.c_size = fs->xcwds2.tbl[fs->xcwds2.depth - 1].d_size; + dp->obj.c_ofs = fs->xcwds2.tbl[fs->xcwds2.depth - 1].nxt_ofs; + } + } + dp->obj.attr |= AM_DIR; /* This is a directory */ + dp->fn[NSFLAG] |= NS_NONAME; /* but dot names in exFAT volume are not directory entry */ + if (ns & NS_LAST) break; /* Last segment? */ + continue; /* Follow next segment */ + } +#endif + res = dir_find(dp); /* Find an object with the segment name */ if (res != FR_OK) { /* Failed to find the object */ if (res == FR_NO_FILE) { /* Object is not found */ - if (FF_FS_RPATH && (ns & NS_DOT)) { /* If dot entry is not exist, stay there */ + if (FF_FS_RPATH && (ns & NS_DOT)) { /* If dot entry is not exist, stay there (may be root dir in FAT volume) */ if (!(ns & NS_LAST)) continue; /* Continue to follow if not last segment */ dp->fn[NSFLAG] = NS_NONAME; res = FR_OK; @@ -3128,17 +3184,24 @@ static FRESULT follow_path ( /* FR_OK(0): successful, !=0: error code */ } break; } - if (ns & NS_LAST) break; /* Last segment matched. Function completed. */ +#if FF_FS_EXFAT && FF_FS_RPATH + if (fs->fs_type == FS_EXFAT && (dp->obj.attr & AM_DIR)) { /* Record the path if it is a sub-directory */ + fs->xcwds2.tbl[fs->xcwds2.depth].nxt_ofs = dp->blk_ofs; + if (++fs->xcwds2.depth >= sizeof fs->xcwds2.tbl / sizeof fs->xcwds2.tbl[0]) { /* Is it too deep path? */ + res = FR_NOT_ENOUGH_CORE; break; + } + fs->xcwds2.tbl[fs->xcwds2.depth].d_scl = ld_32(fs->dirbuf + XDIR_FstClus); + fs->xcwds2.tbl[fs->xcwds2.depth].d_size = ld_32(fs->dirbuf + XDIR_FileSize) | (fs->dirbuf[XDIR_GenFlags] & 2); + } +#endif + if (ns & NS_LAST) break; /* If last segment matched, the function completed */ /* Get into the sub-directory */ - if (!(dp->obj.attr & AM_DIR)) { /* It is not a sub-directory and cannot follow */ - res = FR_NO_PATH; break; + if (!(dp->obj.attr & AM_DIR)) { + res = FR_NO_PATH; break; /* It is not a sub-directory and cannot follow the path */ } #if FF_FS_EXFAT - if (fs->fs_type == FS_EXFAT) { /* Save containing directory information for next dir */ - dp->obj.c_scl = dp->obj.sclust; - dp->obj.c_size = ((DWORD)dp->obj.objsize & 0xFFFFFF00) | dp->obj.stat; - dp->obj.c_ofs = dp->blk_ofs; - init_alloc_info(fs, &dp->obj); /* Open next directory */ + if (fs->fs_type == FS_EXFAT) { + init_alloc_info(&dp->obj, dp); /* Open next directory */ } else #endif { @@ -3216,7 +3279,7 @@ static int get_ldnumber ( /* Returns logical drive number (-1:invalid drive numb } #endif /* No drive prefix */ -#if FF_FS_RPATH != 0 +#if FF_FS_RPATH return (int)CurrVol; /* Default drive is current drive */ #else return 0; /* Default drive is 0 */ @@ -3261,14 +3324,14 @@ static int test_gpt_header ( /* 0:Invalid, 1:Valid */ if (memcmp(gpth + GPTH_Sign, "EFI PART" "\0\0\1", 12)) return 0; /* Check signature and version (1.0) */ - hlen = ld_dword(gpth + GPTH_Size); /* Check header size */ + hlen = ld_32(gpth + GPTH_Size); /* Check header size */ if (hlen < 92 || hlen > FF_MIN_SS) return 0; for (i = 0, bcc = 0xFFFFFFFF; i < hlen; i++) { /* Check header BCC */ bcc = crc32(bcc, i - GPTH_Bcc < 4 ? 0 : gpth[i]); } - if (~bcc != ld_dword(gpth + GPTH_Bcc)) return 0; - if (ld_dword(gpth + GPTH_PteSize) != SZ_GPTE) return 0; /* Table entry size (must be SZ_GPTE bytes) */ - if (ld_dword(gpth + GPTH_PtNum) > 128) return 0; /* Table size (must be 128 entries or less) */ + if (~bcc != ld_32(gpth + GPTH_Bcc)) return 0; + if (ld_32(gpth + GPTH_PteSize) != SZ_GPTE) return 0; /* Table entry size (must be SZ_GPTE bytes) */ + if (ld_32(gpth + GPTH_PtNum) > 128) return 0; /* Table size (must be 128 entries or less) */ return 1; } @@ -3315,7 +3378,7 @@ static UINT check_fs ( /* 0:FAT/FAT32 VBR, 1:exFAT VBR, 2:Not FAT and valid BS, fs->wflag = 0; fs->winsect = (LBA_t)0 - 1; /* Invaidate window */ if (move_window(fs, sect) != FR_OK) return 4; /* Load the boot sector */ - sign = ld_word(fs->win + BS_55AA); + sign = ld_16(fs->win + BS_55AA); #if FF_FS_EXFAT if (sign == 0xAA55 && !memcmp(fs->win + BS_JmpBoot, "\xEB\x76\x90" "EXFAT ", 11)) return 1; /* It is an exFAT VBR */ #endif @@ -3325,15 +3388,15 @@ static UINT check_fs ( /* 0:FAT/FAT32 VBR, 1:exFAT VBR, 2:Not FAT and valid BS, return 0; /* It is an FAT32 VBR */ } /* FAT volumes created in the early MS-DOS era lack BS_55AA and BS_FilSysType, so FAT VBR needs to be identified without them. */ - w = ld_word(fs->win + BPB_BytsPerSec); + w = ld_16(fs->win + BPB_BytsPerSec); b = fs->win[BPB_SecPerClus]; if ((w & (w - 1)) == 0 && w >= FF_MIN_SS && w <= FF_MAX_SS /* Properness of sector size (512-4096 and 2^n) */ && b != 0 && (b & (b - 1)) == 0 /* Properness of cluster size (2^n) */ - && ld_word(fs->win + BPB_RsvdSecCnt) != 0 /* Properness of number of reserved sectors (MNBZ) */ + && ld_16(fs->win + BPB_RsvdSecCnt) != 0 /* Properness of number of reserved sectors (MNBZ) */ && (UINT)fs->win[BPB_NumFATs] - 1 <= 1 /* Properness of number of FATs (1 or 2) */ - && ld_word(fs->win + BPB_RootEntCnt) != 0 /* Properness of root dir size (MNBZ) */ - && (ld_word(fs->win + BPB_TotSec16) >= 128 || ld_dword(fs->win + BPB_TotSec32) >= 0x10000) /* Properness of volume size (>=128) */ - && ld_word(fs->win + BPB_FATSz16) != 0) { /* Properness of FAT size (MNBZ) */ + && ld_16(fs->win + BPB_RootEntCnt) != 0 /* Properness of root dir size (MNBZ) */ + && (ld_16(fs->win + BPB_TotSec16) >= MIN_VOLUME || ld_32(fs->win + BPB_TotSec32) >= 0x10000) /* Properness of volume size */ + && ld_16(fs->win + BPB_FATSz16) != 0) { /* Properness of FAT size (MNBZ) */ return 0; /* It can be presumed an FAT VBR */ } } @@ -3356,7 +3419,7 @@ static UINT find_volume ( /* Returns BS status found in the hosting drive */ fmt = check_fs(fs, 0); /* Load sector 0 and check if it is an FAT VBR as SFD format */ if (fmt != 2 && (fmt >= 3 || part == 0)) return fmt; /* Returns if it is an FAT VBR as auto scan, not a BS or disk error */ - /* Sector 0 is not an FAT VBR or forced partition number wants a partition */ + /* Sector 0 is not an FAT VBR or forced partition number wants a partitioned drive */ #if FF_LBA64 if (fs->win[MBR_Table + PTE_System] == 0xEE) { /* GPT protective MBR? */ @@ -3365,14 +3428,14 @@ static UINT find_volume ( /* Returns BS status found in the hosting drive */ if (move_window(fs, 1) != FR_OK) return 4; /* Load GPT header sector (next to MBR) */ if (!test_gpt_header(fs->win)) return 3; /* Check if GPT header is valid */ - n_ent = ld_dword(fs->win + GPTH_PtNum); /* Number of entries */ - pt_lba = ld_qword(fs->win + GPTH_PtOfs); /* Table location */ + n_ent = ld_32(fs->win + GPTH_PtNum); /* Number of entries */ + pt_lba = ld_64(fs->win + GPTH_PtOfs); /* Table location */ for (v_ent = i = 0; i < n_ent; i++) { /* Find FAT partition */ if (move_window(fs, pt_lba + i * SZ_GPTE / SS(fs)) != FR_OK) return 4; /* PT sector */ ofs = i * SZ_GPTE % SS(fs); /* Offset in the sector */ if (!memcmp(fs->win + ofs + GPTE_PtGuid, GUID_MS_Basic, 16)) { /* MS basic data partition? */ - v_ent++; - fmt = check_fs(fs, ld_qword(fs->win + ofs + GPTE_FstLba)); /* Load VBR and check status */ + v_ent++; /* Order of MS BDP */ + fmt = check_fs(fs, ld_64(fs->win + ofs + GPTE_FstLba)); /* Load VBR and check status */ if (part == 0 && fmt <= 1) return fmt; /* Auto search (valid FAT volume found first) */ if (part != 0 && v_ent == part) return fmt; /* Forced partition order (regardless of it is valid or not) */ } @@ -3380,9 +3443,9 @@ static UINT find_volume ( /* Returns BS status found in the hosting drive */ return 3; /* Not found */ } #endif - if (FF_MULTI_PARTITION && part > 4) return 3; /* MBR has 4 partitions max */ + if (FF_MULTI_PARTITION && part > 4) return 3; /* MBR has four primary partitions max (FatFs does not support logical partition) */ for (i = 0; i < 4; i++) { /* Load partition offset in the MBR */ - mbr_pt[i] = ld_dword(fs->win + MBR_Table + i * SZ_PTE + PTE_StLba); + mbr_pt[i] = ld_32(fs->win + MBR_Table + i * SZ_PTE + PTE_StLba); } i = part ? part - 1 : 0; /* Table index to find first */ do { /* Find an FAT volume */ @@ -3408,8 +3471,6 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ FATFS *fs; DSTATUS stat; LBA_t bsect; - DWORD tsect, sysect, fasize, nclst, szbfat; - WORD nrsv; UINT fmt; @@ -3464,21 +3525,21 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ #if FF_FS_EXFAT if (fmt == 1) { QWORD maxlba; - DWORD so, cv, bcl, i; + DWORD so, cv, bcl, ncl, i; for (i = BPB_ZeroedEx; i < BPB_ZeroedEx + 53 && fs->win[i] == 0; i++) ; /* Check zero filler */ if (i < BPB_ZeroedEx + 53) return FR_NO_FILESYSTEM; - if (ld_word(fs->win + BPB_FSVerEx) != 0x100) return FR_NO_FILESYSTEM; /* Check exFAT version (must be version 1.0) */ + if (ld_16(fs->win + BPB_FSVerEx) != 0x100) return FR_NO_FILESYSTEM; /* Check exFAT version (must be version 1.0) */ if (1 << fs->win[BPB_BytsPerSecEx] != SS(fs)) { /* (BPB_BytsPerSecEx must be equal to the physical sector size) */ return FR_NO_FILESYSTEM; } - maxlba = ld_qword(fs->win + BPB_TotSecEx) + bsect; /* Last LBA of the volume + 1 */ + maxlba = ld_64(fs->win + BPB_TotSecEx) + bsect; /* Last LBA of the volume + 1 */ if (!FF_LBA64 && maxlba >= 0x100000000) return FR_NO_FILESYSTEM; /* (It cannot be accessed in 32-bit LBA) */ - fs->fsize = ld_dword(fs->win + BPB_FatSzEx); /* Number of sectors per FAT */ + fs->fsize = ld_32(fs->win + BPB_FatSzEx); /* Number of sectors per FAT */ fs->n_fats = fs->win[BPB_NumFATsEx]; /* Number of FATs */ if (fs->n_fats != 1) return FR_NO_FILESYSTEM; /* (Supports only 1 FAT) */ @@ -3486,16 +3547,16 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ fs->csize = 1 << fs->win[BPB_SecPerClusEx]; /* Cluster size */ if (fs->csize == 0) return FR_NO_FILESYSTEM; /* (Must be 1..32768 sectors) */ - nclst = ld_dword(fs->win + BPB_NumClusEx); /* Number of clusters */ - if (nclst > MAX_EXFAT) return FR_NO_FILESYSTEM; /* (Too many clusters) */ - fs->n_fatent = nclst + 2; + ncl = ld_32(fs->win + BPB_NumClusEx); /* Number of clusters */ + if (ncl < MIN_EXFAT || ncl > MAX_EXFAT) return FR_NO_FILESYSTEM; /* (Wrong cluster count) */ + fs->n_fatent = ncl + 2; /* Boundaries and Limits */ fs->volbase = bsect; - fs->database = bsect + ld_dword(fs->win + BPB_DataOfsEx); - fs->fatbase = bsect + ld_dword(fs->win + BPB_FatOfsEx); - if (maxlba < (QWORD)fs->database + nclst * fs->csize) return FR_NO_FILESYSTEM; /* (Volume size must not be smaller than the size required) */ - fs->dirbase = ld_dword(fs->win + BPB_RootClusEx); + fs->database = bsect + ld_32(fs->win + BPB_DataOfsEx); + fs->fatbase = bsect + ld_32(fs->win + BPB_FatOfsEx); + if (maxlba < (QWORD)fs->database + ncl * fs->csize) return FR_NO_FILESYSTEM; /* (Volume size must not be smaller than the size required) */ + fs->dirbase = ld_32(fs->win + BPB_RootClusEx); /* Get bitmap location and check if it is contiguous (implementation assumption) */ so = i = 0; @@ -3505,19 +3566,18 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ if (move_window(fs, clst2sect(fs, (DWORD)fs->dirbase) + so) != FR_OK) return FR_DISK_ERR; so++; } - if (fs->win[i] == ET_BITMAP) break; /* Is it a bitmap entry? */ + if (fs->win[i] == ET_BITMAP) break; /* Is it a bitmap entry? */ i = (i + SZDIRE) % SS(fs); /* Next entry */ } - bcl = ld_dword(fs->win + i + 20); /* Bitmap cluster */ + bcl = ld_32(fs->win + i + 20); /* Bitmap cluster */ if (bcl < 2 || bcl >= fs->n_fatent) return FR_NO_FILESYSTEM; /* (Wrong cluster#) */ fs->bitbase = fs->database + fs->csize * (bcl - 2); /* Bitmap sector */ for (;;) { /* Check if bitmap is contiguous */ if (move_window(fs, fs->fatbase + bcl / (SS(fs) / 4)) != FR_OK) return FR_DISK_ERR; - cv = ld_dword(fs->win + bcl % (SS(fs) / 4) * 4); + cv = ld_32(fs->win + bcl % (SS(fs) / 4) * 4); if (cv == 0xFFFFFFFF) break; /* Last link? */ if (cv != ++bcl) return FR_NO_FILESYSTEM; /* Fragmented bitmap? */ } - #if !FF_FS_READONLY fs->last_clst = fs->free_clst = 0xFFFFFFFF; /* Invalidate cluster allocation information */ fs->fsi_flag = 0; /* Enable to sync PercInUse value in VBR */ @@ -3526,10 +3586,14 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ } else #endif /* FF_FS_EXFAT */ { - if (ld_word(fs->win + BPB_BytsPerSec) != SS(fs)) return FR_NO_FILESYSTEM; /* (BPB_BytsPerSec must be equal to the physical sector size) */ + DWORD tsect, sysect, fasize, nclst, szbfat; + WORD nrsv; - fasize = ld_word(fs->win + BPB_FATSz16); /* Number of sectors per FAT */ - if (fasize == 0) fasize = ld_dword(fs->win + BPB_FATSz32); + if (ld_16(fs->win + BPB_BytsPerSec) != SS(fs)) return FR_NO_FILESYSTEM; /* (BPB_BytsPerSec must be equal to the physical sector size) */ + + fasize = ld_16(fs->win + BPB_FATSz16); /* Number of sectors per FAT */ + if (fasize == 0) fasize = ld_32(fs->win + BPB_FATSz32); + if (fasize >= 0x200000) return FR_NO_FILESYSTEM; /* (Must be smaller than max FAT size) */ fs->fsize = fasize; fs->n_fats = fs->win[BPB_NumFATs]; /* Number of FATs */ @@ -3539,13 +3603,13 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ fs->csize = fs->win[BPB_SecPerClus]; /* Cluster size */ if (fs->csize == 0 || (fs->csize & (fs->csize - 1))) return FR_NO_FILESYSTEM; /* (Must be power of 2) */ - fs->n_rootdir = ld_word(fs->win + BPB_RootEntCnt); /* Number of root directory entries */ + fs->n_rootdir = ld_16(fs->win + BPB_RootEntCnt); /* Number of root directory entries */ if (fs->n_rootdir % (SS(fs) / SZDIRE)) return FR_NO_FILESYSTEM; /* (Must be sector aligned) */ - tsect = ld_word(fs->win + BPB_TotSec16); /* Number of sectors on the volume */ - if (tsect == 0) tsect = ld_dword(fs->win + BPB_TotSec32); + tsect = ld_16(fs->win + BPB_TotSec16); /* Number of sectors on the volume */ + if (tsect == 0) tsect = ld_32(fs->win + BPB_TotSec32); - nrsv = ld_word(fs->win + BPB_RsvdSecCnt); /* Number of reserved sectors */ + nrsv = ld_16(fs->win + BPB_RsvdSecCnt); /* Number of reserved sectors */ if (nrsv == 0) return FR_NO_FILESYSTEM; /* (Must not be 0) */ /* Determine the FAT sub type */ @@ -3557,6 +3621,7 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ if (nclst <= MAX_FAT32) fmt = FS_FAT32; if (nclst <= MAX_FAT16) fmt = FS_FAT16; if (nclst <= MAX_FAT12) fmt = FS_FAT12; + if (nclst <= MIN_FAT12) fmt = 0; if (fmt == 0) return FR_NO_FILESYSTEM; /* Boundaries and Limits */ @@ -3565,9 +3630,9 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ fs->fatbase = bsect + nrsv; /* FAT start sector */ fs->database = bsect + sysect; /* Data start sector */ if (fmt == FS_FAT32) { - if (ld_word(fs->win + BPB_FSVer32) != 0) return FR_NO_FILESYSTEM; /* (Must be FAT32 revision 0.0) */ + if (ld_16(fs->win + BPB_FSVer32) != 0) return FR_NO_FILESYSTEM; /* (Must be FAT32 revision 0.0) */ if (fs->n_rootdir != 0) return FR_NO_FILESYSTEM; /* (BPB_RootEntCnt must be 0) */ - fs->dirbase = ld_dword(fs->win + BPB_RootClus32); /* Root directory start cluster */ + fs->dirbase = ld_32(fs->win + BPB_RootClus32); /* Root directory start cluster */ szbfat = fs->n_fatent * 4; /* (Needed FAT size) */ } else { if (fs->n_rootdir == 0) return FR_NO_FILESYSTEM; /* (BPB_RootEntCnt must not be 0) */ @@ -3582,19 +3647,19 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ fs->last_clst = fs->free_clst = 0xFFFFFFFF; /* Invalidate cluster allocation information */ fs->fsi_flag = 0x80; /* Disable FSInfo by default */ if (fmt == FS_FAT32 - && ld_word(fs->win + BPB_FSInfo32) == 1 /* FAT32: Enable FSInfo feature only if FSInfo sector is next to VBR */ + && ld_16(fs->win + BPB_FSInfo32) == 1 /* FAT32: Enable FSInfo feature only if FSInfo sector is next to VBR */ && move_window(fs, bsect + 1) == FR_OK) { fs->fsi_flag = 0; - if ( ld_dword(fs->win + FSI_LeadSig) == 0x41615252 /* Load FSInfo data if available */ - && ld_dword(fs->win + FSI_StrucSig) == 0x61417272 - && ld_dword(fs->win + FSI_TrailSig) == 0xAA550000) + if ( ld_32(fs->win + FSI_LeadSig) == 0x41615252 /* Load FSInfo data if available */ + && ld_32(fs->win + FSI_StrucSig) == 0x61417272 + && ld_32(fs->win + FSI_TrailSig) == 0xAA550000) { #if (FF_FS_NOFSINFO & 1) == 0 /* Get free cluster count if trust it */ - fs->free_clst = ld_dword(fs->win + FSI_Free_Count); + fs->free_clst = ld_32(fs->win + FSI_Free_Count); #endif #if (FF_FS_NOFSINFO & 2) == 0 /* Get next free cluster if rtust it */ - fs->last_clst = ld_dword(fs->win + FSI_Nxt_Free); + fs->last_clst = ld_32(fs->win + FSI_Nxt_Free); #endif } } @@ -3603,18 +3668,25 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ fs->fs_type = (BYTE)fmt;/* FAT sub-type (the filesystem object gets valid) */ fs->id = ++Fsid; /* Volume mount ID */ -#if FF_USE_LFN == 1 - fs->lfnbuf = LfnBuf; /* Static LFN working buffer */ + +#if FF_USE_LFN == 1 /* Initilize pointers to the static working buffers */ + fs->lfnbuf = LfnBuf; /* LFN working buffer */ #if FF_FS_EXFAT - fs->dirbuf = DirBuf; /* Static directory block scratchpad buuffer */ + fs->dirbuf = DirBuf; /* Directory block scratchpad buuffer */ #endif #endif -#if FF_FS_RPATH != 0 - fs->cdir = 0; /* Initialize current directory */ + +#if FF_FS_RPATH /* Set the current directory top layer (root) */ + fs->cdir = 0; +#if FF_FS_EXFAT + memset(&fs->xcwds, 0, sizeof fs->xcwds); +#endif #endif + #if FF_FS_LOCK /* Clear file lock semaphores */ clear_share(fs); #endif + return FR_OK; } @@ -3666,7 +3738,7 @@ static FRESULT validate ( /* Returns FR_OK or FR_INVALID_OBJECT */ /*-----------------------------------------------------------------------*/ -/* Mount/Unmount a Logical Drive */ +/* API: Mount/Unmount a Logical Drive */ /*-----------------------------------------------------------------------*/ FRESULT f_mount ( @@ -3684,11 +3756,11 @@ FRESULT f_mount ( /* Get volume ID (logical drive number) */ vol = get_ldnumber(&rp); if (vol < 0) return FR_INVALID_DRIVE; - cfs = FatFs[vol]; /* Pointer to the filesystem object of the volume */ - if (cfs) { /* Unregister current filesystem object if registered */ + cfs = FatFs[vol]; /* Pointer to the filesystem object of the volume */ + if (cfs) { /* Unregister current filesystem object */ FatFs[vol] = 0; -#if FF_FS_LOCK +#if FF_FS_LOCK /* Clear file lock semaphores correspond to this volume */ clear_share(cfs); #endif #if FF_FS_REENTRANT /* Discard mutex of the current volume */ @@ -3713,12 +3785,12 @@ FRESULT f_mount ( #endif #endif fs->fs_type = 0; /* Invalidate the new filesystem object */ - FatFs[vol] = fs; /* Register new fs object */ + FatFs[vol] = fs; /* Register it */ } if (opt == 0) return FR_OK; /* Do not mount now, it will be mounted in subsequent file functions */ - res = mount_volume(&path, &fs, 0); /* Force mounted the volume */ + res = mount_volume(&path, &fs, 0); /* Force mounted the volume in this function */ LEAVE_FF(fs, res); } @@ -3726,7 +3798,7 @@ FRESULT f_mount ( /*-----------------------------------------------------------------------*/ -/* Open or Create a File */ +/* API: Open or Create a File */ /*-----------------------------------------------------------------------*/ FRESULT f_open ( @@ -3738,22 +3810,19 @@ FRESULT f_open ( FRESULT res; DIR dj; FATFS *fs; -#if !FF_FS_READONLY - DWORD cl, bcs, clst, tm; - LBA_t sc; - FSIZE_t ofs; -#endif - DEF_NAMBUF + DEF_NAMEBUFF - if (!fp) return FR_INVALID_OBJECT; + if (!fp) return FR_INVALID_OBJECT; /* Reject null pointer */ - /* Get logical drive number */ + /* Get logical drive number and mount the volume if needed */ mode &= FF_FS_READONLY ? FA_READ : FA_READ | FA_WRITE | FA_CREATE_ALWAYS | FA_CREATE_NEW | FA_OPEN_ALWAYS | FA_OPEN_APPEND; res = mount_volume(&path, &fs, mode); + if (res == FR_OK) { + fp->obj.fs = fs; dj.obj.fs = fs; - INIT_NAMBUF(fs); + INIT_NAMEBUFF(fs); res = follow_path(&dj, path); /* Follow the file path */ #if !FF_FS_READONLY /* Read/Write configuration */ if (res == FR_OK) { @@ -3778,24 +3847,25 @@ FRESULT f_open ( } mode |= FA_CREATE_ALWAYS; /* File is created */ } - else { /* Any object with the same name is already existing */ - if (dj.obj.attr & (AM_RDO | AM_DIR)) { /* Cannot overwrite it (R/O or DIR) */ - res = FR_DENIED; + else { /* An object with the same name is already existing */ + if (mode & FA_CREATE_NEW) { + res = FR_EXIST; /* Cannot create as new file */ } else { - if (mode & FA_CREATE_NEW) res = FR_EXIST; /* Cannot create as new file */ + if (dj.obj.attr & (AM_RDO | AM_DIR)) res = FR_DENIED; /* Cannot overwrite it (R/O or DIR) */ } } if (res == FR_OK && (mode & FA_CREATE_ALWAYS)) { /* Truncate the file if overwrite mode */ + DWORD tm = GET_FATTIME(); #if FF_FS_EXFAT if (fs->fs_type == FS_EXFAT) { /* Get current allocation info */ - fp->obj.fs = fs; - init_alloc_info(fs, &fp->obj); - /* Set directory entry block initial state */ + init_alloc_info(&fp->obj, 0); + /* Set exFAT directory entry block initial state */ memset(fs->dirbuf + 2, 0, 30); /* Clear 85 entry except for NumSec */ memset(fs->dirbuf + 38, 0, 26); /* Clear C0 entry except for NumName and NameHash */ fs->dirbuf[XDIR_Attr] = AM_ARC; - st_dword(fs->dirbuf + XDIR_CrtTime, GET_FATTIME()); + st_32(fs->dirbuf + XDIR_CrtTime, tm); /* Set created time */ + st_32(fs->dirbuf + XDIR_ModTime, tm); /* Set modified time (tmp setting) */ fs->dirbuf[XDIR_GenFlags] = 1; res = store_xdir(&dj); if (res == FR_OK && fp->obj.sclust != 0) { /* Remove the cluster chain if exist */ @@ -3805,17 +3875,18 @@ FRESULT f_open ( } else #endif { - /* Set directory entry initial state */ - tm = GET_FATTIME(); /* Set created time */ - st_dword(dj.dir + DIR_CrtTime, tm); - st_dword(dj.dir + DIR_ModTime, tm); + DWORD cl; + /* Set FAT directory entry initial state */ + st_32(dj.dir + DIR_CrtTime, tm); /* Set created time */ + st_32(dj.dir + DIR_ModTime, tm); /* Set modified time (tmp setting) */ cl = ld_clust(fs, dj.dir); /* Get current cluster chain */ dj.dir[DIR_Attr] = AM_ARC; /* Reset attribute */ st_clust(fs, dj.dir, 0); /* Reset file allocation info */ - st_dword(dj.dir + DIR_FileSize, 0); + st_32(dj.dir + DIR_FileSize, 0); fs->wflag = 1; if (cl != 0) { /* Remove the cluster chain if exist */ - sc = fs->winsect; + LBA_t sc = fs->winsect; + res = remove_chain(&dj.obj, cl, 0); if (res == FR_OK) { res = move_window(fs, sc); @@ -3845,7 +3916,8 @@ FRESULT f_open ( if (fp->obj.lockid == 0) res = FR_INT_ERR; #endif } -#else /* R/O configuration */ + +#else /* R/O configuration */ if (res == FR_OK) { if (dj.fn[NSFLAG] & NS_NONAME) { /* Is it origin directory itself? */ res = FR_INVALID_NAME; @@ -3860,21 +3932,17 @@ FRESULT f_open ( if (res == FR_OK) { #if FF_FS_EXFAT if (fs->fs_type == FS_EXFAT) { - fp->obj.c_scl = dj.obj.sclust; /* Get containing directory info */ - fp->obj.c_size = ((DWORD)dj.obj.objsize & 0xFFFFFF00) | dj.obj.stat; - fp->obj.c_ofs = dj.blk_ofs; - init_alloc_info(fs, &fp->obj); + init_alloc_info(&fp->obj, &dj); } else #endif { fp->obj.sclust = ld_clust(fs, dj.dir); /* Get object allocation info */ - fp->obj.objsize = ld_dword(dj.dir + DIR_FileSize); + fp->obj.objsize = ld_32(dj.dir + DIR_FileSize); } #if FF_USE_FASTSEEK fp->cltbl = 0; /* Disable fast seek mode */ #endif - fp->obj.fs = fs; /* Validate the file object */ - fp->obj.id = fs->id; + fp->obj.id = fs->id; /* Set current volume mount ID */ fp->flag = mode; /* Set file access mode */ fp->err = 0; /* Clear error flag */ fp->sect = 0; /* Invalidate current data sector */ @@ -3884,6 +3952,9 @@ FRESULT f_open ( memset(fp->buf, 0, sizeof fp->buf); /* Clear sector buffer */ #endif if ((mode & FA_SEEKEND) && fp->obj.objsize > 0) { /* Seek to end of file if FA_OPEN_APPEND is specified */ + DWORD bcs, clst; + FSIZE_t ofs; + fp->fptr = fp->obj.objsize; /* Offset to seek */ bcs = (DWORD)fs->csize * SS(fs); /* Cluster size in byte */ clst = fp->obj.sclust; /* Follow the cluster chain */ @@ -3894,11 +3965,12 @@ FRESULT f_open ( } fp->clust = clst; if (res == FR_OK && ofs % SS(fs)) { /* Fill sector buffer if not on the sector boundary */ - sc = clst2sect(fs, clst); - if (sc == 0) { + LBA_t sec = clst2sect(fs, clst); + + if (sec == 0) { res = FR_INT_ERR; } else { - fp->sect = sc + (DWORD)(ofs / SS(fs)); + fp->sect = sec + (DWORD)(ofs / SS(fs)); #if !FF_FS_TINY if (disk_read(fs->pdrv, fp->buf, fp->sect, 1) != RES_OK) res = FR_DISK_ERR; #endif @@ -3911,7 +3983,7 @@ FRESULT f_open ( #endif } - FREE_NAMBUF(); + FREE_NAMEBUFF(); } if (res != FR_OK) fp->obj.fs = 0; /* Invalidate file object on error */ @@ -3923,7 +3995,7 @@ FRESULT f_open ( /*-----------------------------------------------------------------------*/ -/* Read File */ +/* API: Read File */ /*-----------------------------------------------------------------------*/ FRESULT f_read ( @@ -3935,7 +4007,6 @@ FRESULT f_read ( { FRESULT res; FATFS *fs; - DWORD clst; LBA_t sect; FSIZE_t remain; UINT rcnt, cc, csect; @@ -3953,6 +4024,8 @@ FRESULT f_read ( if (fp->fptr % SS(fs) == 0) { /* On the sector boundary? */ csect = (UINT)(fp->fptr / SS(fs) & (fs->csize - 1)); /* Sector offset in the cluster */ if (csect == 0) { /* On the cluster boundary? */ + DWORD clst; + if (fp->fptr == 0) { /* On the top of the file? */ clst = fp->obj.sclust; /* Follow cluster chain from the origin */ } else { /* Middle or end of the file */ @@ -4023,7 +4096,7 @@ FRESULT f_read ( #if !FF_FS_READONLY /*-----------------------------------------------------------------------*/ -/* Write File */ +/* API: Write File */ /*-----------------------------------------------------------------------*/ FRESULT f_write ( @@ -4144,7 +4217,7 @@ FRESULT f_write ( /*-----------------------------------------------------------------------*/ -/* Synchronize the File */ +/* API: Synchronize the File */ /*-----------------------------------------------------------------------*/ FRESULT f_sync ( @@ -4153,8 +4226,6 @@ FRESULT f_sync ( { FRESULT res; FATFS *fs; - DWORD tm; - BYTE *dir; res = validate(&fp->obj, &fs); /* Check validity of the file object */ @@ -4167,7 +4238,6 @@ FRESULT f_sync ( } #endif /* Update the directory entry */ - tm = GET_FATTIME(); /* Modified time */ #if FF_FS_EXFAT if (fs->fs_type == FS_EXFAT) { res = fill_first_frag(&fp->obj); /* Fill first fragment on the FAT if needed */ @@ -4176,40 +4246,43 @@ FRESULT f_sync ( } if (res == FR_OK) { DIR dj; - DEF_NAMBUF + DEF_NAMEBUFF - INIT_NAMBUF(fs); + INIT_NAMEBUFF(fs); res = load_obj_xdir(&dj, &fp->obj); /* Load directory entry block */ if (res == FR_OK) { - fs->dirbuf[XDIR_Attr] |= AM_ARC; /* Set archive attribute to indicate that the file has been changed */ - fs->dirbuf[XDIR_GenFlags] = fp->obj.stat | 1; /* Update file allocation information */ - st_dword(fs->dirbuf + XDIR_FstClus, fp->obj.sclust); /* Update start cluster */ - st_qword(fs->dirbuf + XDIR_FileSize, fp->obj.objsize); /* Update file size */ - st_qword(fs->dirbuf + XDIR_ValidFileSize, fp->obj.objsize); /* (FatFs does not support Valid File Size feature) */ - st_dword(fs->dirbuf + XDIR_ModTime, tm); /* Update modified time */ + fs->dirbuf[XDIR_Attr] |= AM_ARC; /* Set archive attribute to indicate that the file has been changed */ + fs->dirbuf[XDIR_GenFlags] = fp->obj.stat | 1; /* Update file allocation information */ + st_32(fs->dirbuf + XDIR_FstClus, fp->obj.sclust); /* Update start cluster */ + st_64(fs->dirbuf + XDIR_FileSize, fp->obj.objsize); /* Update file size */ + st_64(fs->dirbuf + XDIR_ValidFileSize, fp->obj.objsize); /* (FatFs does not support Valid File Size feature) */ + st_32(fs->dirbuf + XDIR_ModTime, GET_FATTIME()); /* Update modified time */ fs->dirbuf[XDIR_ModTime10] = 0; - st_dword(fs->dirbuf + XDIR_AccTime, 0); - res = store_xdir(&dj); /* Restore it to the directory */ + fs->dirbuf[XDIR_ModTZ] = 0; + st_32(fs->dirbuf + XDIR_AccTime, 0); /* Invalidate last access time */ + fs->dirbuf[XDIR_AccTZ] = 0; + res = store_xdir(&dj); /* Restore it to the directory */ if (res == FR_OK) { res = sync_fs(fs); fp->flag &= (BYTE)~FA_MODIFIED; } } - FREE_NAMBUF(); + FREE_NAMEBUFF(); } } else #endif { res = move_window(fs, fp->dir_sect); if (res == FR_OK) { - dir = fp->dir_ptr; - dir[DIR_Attr] |= AM_ARC; /* Set archive attribute to indicate that the file has been changed */ - st_clust(fp->obj.fs, dir, fp->obj.sclust); /* Update file allocation information */ - st_dword(dir + DIR_FileSize, (DWORD)fp->obj.objsize); /* Update file size */ - st_dword(dir + DIR_ModTime, tm); /* Update modified time */ - st_word(dir + DIR_LstAccDate, 0); + BYTE *dir = fp->dir_ptr; + + dir[DIR_Attr] |= AM_ARC; /* Set archive attribute to indicate that the file has been changed */ + st_clust(fp->obj.fs, dir, fp->obj.sclust); /* Update file allocation information */ + st_32(dir + DIR_FileSize, (DWORD)fp->obj.objsize); /* Update file size */ + st_32(dir + DIR_ModTime, GET_FATTIME()); /* Update modified time */ + st_16(dir + DIR_LstAccDate, 0); /* Invalidate last access date */ fs->wflag = 1; - res = sync_fs(fs); /* Restore it to the directory */ + res = sync_fs(fs); /* Restore it to the directory */ fp->flag &= (BYTE)~FA_MODIFIED; } } @@ -4225,7 +4298,7 @@ FRESULT f_sync ( /*-----------------------------------------------------------------------*/ -/* Close File */ +/* API: Close File */ /*-----------------------------------------------------------------------*/ FRESULT f_close ( @@ -4261,7 +4334,7 @@ FRESULT f_close ( #if FF_FS_RPATH >= 1 /*-----------------------------------------------------------------------*/ -/* Change Current Directory or Current Drive, Get Current Directory */ +/* API: Change Current Drive */ /*-----------------------------------------------------------------------*/ FRESULT f_chdrive ( @@ -4281,57 +4354,56 @@ FRESULT f_chdrive ( + +/*-----------------------------------------------------------------------*/ +/* API: Change Current Directory */ +/*-----------------------------------------------------------------------*/ + FRESULT f_chdir ( const TCHAR* path /* Pointer to the directory path */ ) { -#if FF_STR_VOLUME_ID == 2 - UINT i; -#endif FRESULT res; DIR dj; FATFS *fs; - DEF_NAMBUF + DEF_NAMEBUFF - /* Get logical drive */ - res = mount_volume(&path, &fs, 0); + res = mount_volume(&path, &fs, 0); /* Get logical drive and mount the volume if needed */ if (res == FR_OK) { dj.obj.fs = fs; - INIT_NAMBUF(fs); + INIT_NAMEBUFF(fs); res = follow_path(&dj, path); /* Follow the path */ if (res == FR_OK) { /* Follow completed */ if (dj.fn[NSFLAG] & NS_NONAME) { /* Is it the start directory itself? */ - fs->cdir = dj.obj.sclust; #if FF_FS_EXFAT if (fs->fs_type == FS_EXFAT) { - fs->cdc_scl = dj.obj.c_scl; - fs->cdc_size = dj.obj.c_size; - fs->cdc_ofs = dj.obj.c_ofs; + memcpy(&fs->xcwds, &fs->xcwds2, sizeof fs->xcwds); } #endif + fs->cdir = dj.obj.sclust; } else { if (dj.obj.attr & AM_DIR) { /* It is a sub-directory */ #if FF_FS_EXFAT if (fs->fs_type == FS_EXFAT) { - fs->cdir = ld_dword(fs->dirbuf + XDIR_FstClus); /* Sub-directory cluster */ - fs->cdc_scl = dj.obj.sclust; /* Save containing directory information */ - fs->cdc_size = ((DWORD)dj.obj.objsize & 0xFFFFFF00) | dj.obj.stat; - fs->cdc_ofs = dj.blk_ofs; + memcpy(&fs->xcwds, &fs->xcwds2, sizeof fs->xcwds); + fs->cdir = fs->xcwds.tbl[fs->xcwds.depth].d_scl; /* Sub-directory cluster */ } else #endif { - fs->cdir = ld_clust(fs, dj.dir); /* Sub-directory cluster */ + fs->cdir = ld_clust(fs, dj.dir); /* Sub-directory cluster */ } } else { res = FR_NO_PATH; /* Reached but a file */ } } } - FREE_NAMBUF(); + FREE_NAMEBUFF(); if (res == FR_NO_FILE) res = FR_NO_PATH; #if FF_STR_VOLUME_ID == 2 /* Also current drive is changed if in Unix style volume ID */ if (res == FR_OK) { + UINT i; + for (i = FF_VOLUMES - 1; i && fs != FatFs[i]; i--) ; /* Set current drive */ CurrVol = (BYTE)i; } @@ -4342,96 +4414,136 @@ FRESULT f_chdir ( } + + #if FF_FS_RPATH >= 2 +/*-----------------------------------------------------------------------*/ +/* API: Get Curent Directory */ +/*-----------------------------------------------------------------------*/ + FRESULT f_getcwd ( - TCHAR* buff, /* Pointer to the directory path */ + TCHAR* buff, /* Pointer to the buffer to store the current direcotry path */ UINT len /* Size of buff in unit of TCHAR */ ) { FRESULT res; DIR dj; FATFS *fs; - UINT i, n; - DWORD ccl; - TCHAR *tp = buff; #if FF_VOLUMES >= 2 UINT vl; #if FF_STR_VOLUME_ID - const char *vp; + const char *vid; #endif #endif FILINFO fno; - DEF_NAMBUF + DEF_NAMEBUFF - /* Get logical drive */ - buff[0] = 0; /* Set null string to get current volume */ - res = mount_volume((const TCHAR**)&buff, &fs, 0); /* Get current volume */ + buff[0] = 0; /* A null str to get current drive */ + res = mount_volume((const TCHAR**)&buff, &fs, 0); if (res == FR_OK) { dj.obj.fs = fs; - INIT_NAMBUF(fs); + INIT_NAMEBUFF(fs); +#if FF_FS_EXFAT + if (fs->fs_type == FS_EXFAT) { /* On the exFAT volume */ + UINT wi = 0; + UINT di, ni; + +#if FF_VOLUMES >= 2 /* Add drive prefix if needed */ +#if FF_STR_VOLUME_ID == 0 /* Numeric volume ID */ + if (wi < len) buff[wi++] = '0' + CurrVol; +#else /* String volume ID */ + if (FF_STR_VOLUME_ID == 2 && wi < len) buff[wi++] = '/'; + for (vid = (const char*)VolumeStr[CurrVol]; *vid && wi < len; buff[wi++] = *vid++) ; +#endif + if (FF_STR_VOLUME_ID <= 1 && wi < len) buff[wi++] = ':'; +#endif + if (wi < len) buff[wi++] = '/'; + for (di = 0; wi < len && di < fs->xcwds.depth; di++) { /* Follow current directory path with cwd structure */ + dj.obj.sclust = fs->xcwds.tbl[di].d_scl; /* Open this directory */ + dj.obj.stat = (BYTE)fs->xcwds.tbl[di].d_size; + dj.obj.objsize = fs->xcwds.tbl[di].d_size & 0xFFFFFF00; + res = dir_sdi(&dj, fs->xcwds.tbl[di].nxt_ofs); /* Find next directory */ + if (res != FR_OK) break; + res = DIR_READ_FILE(&dj); /* Get the directory name */ + if (res != FR_OK) break; + get_fileinfo(&dj, &fno); + if (di > 0 && wi < len) buff[wi++] = '/'; /* Add the directory name with a directory separator */ + for (ni = 0; fno.fname[ni] && wi < len; buff[wi++] = fno.fname[ni++]) ; + } + if (wi == len) { /* Buffer overflow? */ + res = FR_NOT_ENOUGH_CORE; + } else { + buff[wi] = 0; /* Terminate the string */ + } + } + else +#endif + { /* On the FAT/FAT32 volume */ + TCHAR *tp = buff; + UINT i, nl; + DWORD ccl; - /* Follow parent directories and create the path */ - i = len; /* Bottom of buffer (directory stack base) */ - if (!FF_FS_EXFAT || fs->fs_type != FS_EXFAT) { /* (Cannot do getcwd on exFAT and returns root path) */ + /* Follow parent directories toward the root directory and create the cwd path */ + i = len; /* Bottom of buffer (directory stack base) */ dj.obj.sclust = fs->cdir; /* Start to follow upper directory from current directory */ while ((ccl = dj.obj.sclust) != 0) { /* Repeat while current directory is a sub-directory */ - res = dir_sdi(&dj, 1 * SZDIRE); /* Get parent directory */ + res = dir_sdi(&dj, 1 * SZDIRE); /* Get parent directory */ if (res != FR_OK) break; res = move_window(fs, dj.sect); if (res != FR_OK) break; - dj.obj.sclust = ld_clust(fs, dj.dir); /* Goto parent directory */ + dj.obj.sclust = ld_clust(fs, dj.dir); /* Go to parent directory */ res = dir_sdi(&dj, 0); if (res != FR_OK) break; - do { /* Find the entry links to the child directory */ + do { /* Find the entry links to this sub-directory */ res = DIR_READ_FILE(&dj); if (res != FR_OK) break; if (ccl == ld_clust(fs, dj.dir)) break; /* Found the entry */ res = dir_next(&dj, 0); } while (res == FR_OK); - if (res == FR_NO_FILE) res = FR_INT_ERR;/* It cannot be 'not found'. */ + if (res == FR_NO_FILE) res = FR_INT_ERR; /* It cannot be 'not found'. */ if (res != FR_OK) break; - get_fileinfo(&dj, &fno); /* Get the directory name and push it to the buffer */ - for (n = 0; fno.fname[n]; n++) ; /* Name length */ - if (i < n + 1) { /* Insufficient space to store the path name? */ + get_fileinfo(&dj, &fno); /* Get the directory name and push it to the buffer */ + for (nl = 0; fno.fname[nl]; nl++) ; /* Name length */ + if (i < nl + 1) { /* Insufficient space to store the path name? */ res = FR_NOT_ENOUGH_CORE; break; } - while (n) buff[--i] = fno.fname[--n]; /* Stack the name */ + while (nl) buff[--i] = fno.fname[--nl]; /* Stack the name */ buff[--i] = '/'; } - } - if (res == FR_OK) { - if (i == len) buff[--i] = '/'; /* Is it the root-directory? */ + if (res == FR_OK) { + if (i == len) buff[--i] = '/'; /* Is it the root-directory? */ #if FF_VOLUMES >= 2 /* Put drive prefix */ - vl = 0; + vl = 0; #if FF_STR_VOLUME_ID >= 1 /* String volume ID */ - for (n = 0, vp = (const char*)VolumeStr[CurrVol]; vp[n]; n++) ; - if (i >= n + 2) { - if (FF_STR_VOLUME_ID == 2) *tp++ = (TCHAR)'/'; - for (vl = 0; vl < n; *tp++ = (TCHAR)vp[vl], vl++) ; - if (FF_STR_VOLUME_ID == 1) *tp++ = (TCHAR)':'; - vl++; - } + for (nl = 0, vid = (const char*)VolumeStr[CurrVol]; vid[nl]; nl++) ; /* Volume ID length */ + if (i >= nl + 2) { + if (FF_STR_VOLUME_ID == 2) *tp++ = '/'; /* Unix style */ + for (vl = 0; vl < nl; *tp++ = vid[vl], vl++) ; + if (FF_STR_VOLUME_ID == 1) *tp++ = ':'; /* DOS/Windows style */ + vl++; + } #else /* Numeric volume ID */ - if (i >= 3) { - *tp++ = (TCHAR)'0' + CurrVol; - *tp++ = (TCHAR)':'; - vl = 2; - } + if (i >= 3) { + *tp++ = '0' + CurrVol; + *tp++ = ':'; + vl = 2; + } #endif - if (vl == 0) res = FR_NOT_ENOUGH_CORE; + if (vl == 0) res = FR_NOT_ENOUGH_CORE; #endif - /* Add current directory path */ - if (res == FR_OK) { - do { /* Copy stacked path string */ - *tp++ = buff[i++]; - } while (i < len); + /* Add current directory path */ + if (res == FR_OK) { + do { /* Copy stacked path string */ + *tp++ = buff[i++]; + } while (i < len); + } } + *tp = 0; } - FREE_NAMBUF(); + FREE_NAMEBUFF(); } - *tp = 0; LEAVE_FF(fs, res); } @@ -4442,7 +4554,7 @@ FRESULT f_getcwd ( #if FF_FS_MINIMIZE <= 2 /*-----------------------------------------------------------------------*/ -/* Seek File Read/Write Pointer */ +/* API: Seek File Read/Write Pointer */ /*-----------------------------------------------------------------------*/ FRESULT f_lseek ( @@ -4455,11 +4567,7 @@ FRESULT f_lseek ( DWORD clst, bcs; LBA_t nsect; FSIZE_t ifptr; -#if FF_USE_FASTSEEK - DWORD cl, pcl, ncl, tcl, tlen, ulen; - DWORD *tbl; - LBA_t dsc; -#endif + res = validate(&fp->obj, &fs); /* Check validity of the file object */ if (res == FR_OK) res = (FRESULT)fp->err; @@ -4472,6 +4580,10 @@ FRESULT f_lseek ( #if FF_USE_FASTSEEK if (fp->cltbl) { /* Fast seek */ + DWORD cl, pcl, ncl, tcl, tlen, ulen; + DWORD *tbl; + LBA_t dsc; + if (ofs == CREATE_LINKMAP) { /* Create CLMT */ tbl = fp->cltbl; tlen = *tbl++; ulen = 2; /* Given table size and required table size */ @@ -4606,7 +4718,7 @@ FRESULT f_lseek ( #if FF_FS_MINIMIZE <= 1 /*-----------------------------------------------------------------------*/ -/* Create a Directory Object */ +/* API: Create a Directory Object */ /*-----------------------------------------------------------------------*/ FRESULT f_opendir ( @@ -4616,26 +4728,22 @@ FRESULT f_opendir ( { FRESULT res; FATFS *fs; - DEF_NAMBUF + DEF_NAMEBUFF - if (!dp) return FR_INVALID_OBJECT; + if (!dp) return FR_INVALID_OBJECT; /* Reject null pointer */ - /* Get logical drive */ - res = mount_volume(&path, &fs, 0); + res = mount_volume(&path, &fs, 0); /* Get logical drive and mount the volume if needed */ if (res == FR_OK) { dp->obj.fs = fs; - INIT_NAMBUF(fs); + INIT_NAMEBUFF(fs); res = follow_path(dp, path); /* Follow the path to the directory */ if (res == FR_OK) { /* Follow completed */ - if (!(dp->fn[NSFLAG] & NS_NONAME)) { /* It is not the origin directory itself */ + if (!(dp->fn[NSFLAG] & NS_NONAME)) { /* It is neither the origin directory itself nor dot name in exFAT */ if (dp->obj.attr & AM_DIR) { /* This object is a sub-directory */ #if FF_FS_EXFAT if (fs->fs_type == FS_EXFAT) { - dp->obj.c_scl = dp->obj.sclust; /* Get containing directory information */ - dp->obj.c_size = ((DWORD)dp->obj.objsize & 0xFFFFFF00) | dp->obj.stat; - dp->obj.c_ofs = dp->blk_ofs; - init_alloc_info(fs, &dp->obj); /* Get object allocation info */ + init_alloc_info(&dp->obj, dp); /* Get object allocation info */ } else #endif { @@ -4646,21 +4754,21 @@ FRESULT f_opendir ( } } if (res == FR_OK) { - dp->obj.id = fs->id; - res = dir_sdi(dp, 0); /* Rewind directory */ + dp->obj.id = fs->id; /* Set current volume mount ID */ + res = dir_sdi(dp, 0); /* Rewind directory */ #if FF_FS_LOCK if (res == FR_OK) { - if (dp->obj.sclust != 0) { - dp->obj.lockid = inc_share(dp, 0); /* Lock the sub directory */ + if (dp->obj.sclust) { /* Is this a sub-directory? */ + dp->obj.lockid = inc_share(dp, 0); /* Lock the sub-directory */ if (!dp->obj.lockid) res = FR_TOO_MANY_OPEN_FILES; } else { - dp->obj.lockid = 0; /* Root directory need not to be locked */ + dp->obj.lockid = 0; /* Root directory does not need to be locked */ } } #endif } } - FREE_NAMBUF(); + FREE_NAMEBUFF(); if (res == FR_NO_FILE) res = FR_NO_PATH; } if (res != FR_OK) dp->obj.fs = 0; /* Invalidate the directory object if function failed */ @@ -4672,7 +4780,7 @@ FRESULT f_opendir ( /*-----------------------------------------------------------------------*/ -/* Close Directory */ +/* API: Close Directory */ /*-----------------------------------------------------------------------*/ FRESULT f_closedir ( @@ -4702,7 +4810,7 @@ FRESULT f_closedir ( /*-----------------------------------------------------------------------*/ -/* Read Directory Entries in Sequence */ +/* API: Read Directory Entries in Sequence */ /*-----------------------------------------------------------------------*/ FRESULT f_readdir ( @@ -4712,7 +4820,7 @@ FRESULT f_readdir ( { FRESULT res; FATFS *fs; - DEF_NAMBUF + DEF_NAMEBUFF res = validate(&dp->obj, &fs); /* Check validity of the directory object */ @@ -4720,7 +4828,8 @@ FRESULT f_readdir ( if (!fno) { res = dir_sdi(dp, 0); /* Rewind the directory object */ } else { - INIT_NAMBUF(fs); + INIT_NAMEBUFF(fs); + fno->fname[0] = 0; /* Clear file information */ res = DIR_READ_FILE(dp); /* Read an item */ if (res == FR_NO_FILE) res = FR_OK; /* Ignore end of directory */ if (res == FR_OK) { /* A valid entry is found */ @@ -4728,9 +4837,11 @@ FRESULT f_readdir ( res = dir_next(dp, 0); /* Increment index for next */ if (res == FR_NO_FILE) res = FR_OK; /* Ignore end of directory now */ } - FREE_NAMBUF(); + FREE_NAMEBUFF(); } } + + if (fno && res != FR_OK) fno->fname[0] = 0; /* Clear the file information if any error occured */ LEAVE_FF(fs, res); } @@ -4738,7 +4849,7 @@ FRESULT f_readdir ( #if FF_USE_FIND /*-----------------------------------------------------------------------*/ -/* Find Next File */ +/* API: Find Next File */ /*-----------------------------------------------------------------------*/ FRESULT f_findnext ( @@ -4763,7 +4874,7 @@ FRESULT f_findnext ( /*-----------------------------------------------------------------------*/ -/* Find First File */ +/* API: Find First File */ /*-----------------------------------------------------------------------*/ FRESULT f_findfirst ( @@ -4790,7 +4901,7 @@ FRESULT f_findfirst ( #if FF_FS_MINIMIZE == 0 /*-----------------------------------------------------------------------*/ -/* Get File Status */ +/* API: Get File Status */ /*-----------------------------------------------------------------------*/ FRESULT f_stat ( @@ -4800,13 +4911,14 @@ FRESULT f_stat ( { FRESULT res; DIR dj; - DEF_NAMBUF + DEF_NAMEBUFF - /* Get logical drive */ + /* Get logical drive and mount the volume if needed */ res = mount_volume(&path, &dj.obj.fs, 0); + if (res == FR_OK) { - INIT_NAMBUF(dj.obj.fs); + INIT_NAMEBUFF(dj.obj.fs); res = follow_path(&dj, path); /* Follow the file path */ if (res == FR_OK) { /* Follow completed */ if (dj.fn[NSFLAG] & NS_NONAME) { /* It is origin directory */ @@ -4815,9 +4927,10 @@ FRESULT f_stat ( if (fno) get_fileinfo(&dj, fno); } } - FREE_NAMBUF(); + FREE_NAMEBUFF(); } + if (fno && res != FR_OK) fno->fname[0] = 0; /* Invalidate the file information if an error occured */ LEAVE_FF(dj.obj.fs, res); } @@ -4825,7 +4938,7 @@ FRESULT f_stat ( #if !FF_FS_READONLY /*-----------------------------------------------------------------------*/ -/* Get Number of Free Clusters */ +/* API: Get Number of Free Clusters */ /*-----------------------------------------------------------------------*/ FRESULT f_getfree ( @@ -4842,8 +4955,9 @@ FRESULT f_getfree ( FFOBJID obj; - /* Get logical drive */ + /* Get logical drive and mount the volume if needed */ res = mount_volume(&path, &fs, 0); + if (res == FR_OK) { *fatfs = fs; /* Return ptr to the fs object */ /* If free_clst is valid, return it without full FAT scan */ @@ -4896,10 +5010,10 @@ FRESULT f_getfree ( if (res != FR_OK) break; } if (fs->fs_type == FS_FAT16) { - if (ld_word(fs->win + i) == 0) nfree++; /* FAT16: Is this cluster free? */ + if (ld_16(fs->win + i) == 0) nfree++; /* FAT16: Is this cluster free? */ i += 2; /* Next entry */ } else { - if ((ld_dword(fs->win + i) & 0x0FFFFFFF) == 0) nfree++; /* FAT32: Is this cluster free? */ + if ((ld_32(fs->win + i) & 0x0FFFFFFF) == 0) nfree++; /* FAT32: Is this cluster free? */ i += 4; /* Next entry */ } i %= SS(fs); @@ -4921,7 +5035,7 @@ FRESULT f_getfree ( /*-----------------------------------------------------------------------*/ -/* Truncate File */ +/* API: Truncate File */ /*-----------------------------------------------------------------------*/ FRESULT f_truncate ( @@ -4933,7 +5047,8 @@ FRESULT f_truncate ( DWORD ncl; - res = validate(&fp->obj, &fs); /* Check validity of the file object */ + /* Check validity of the file object */ + res = validate(&fp->obj, &fs); if (res != FR_OK || (res = (FRESULT)fp->err) != FR_OK) LEAVE_FF(fs, res); if (!(fp->flag & FA_WRITE)) LEAVE_FF(fs, FR_DENIED); /* Check access mode */ @@ -4971,7 +5086,7 @@ FRESULT f_truncate ( /*-----------------------------------------------------------------------*/ -/* Delete a File/Directory */ +/* API: Delete a File/Directory */ /*-----------------------------------------------------------------------*/ FRESULT f_unlink ( @@ -4985,77 +5100,72 @@ FRESULT f_unlink ( #if FF_FS_EXFAT FFOBJID obj; #endif - DEF_NAMBUF - + DEF_NAMEBUFF - /* Get logical drive */ + /* Get logical drive and mount the volume if needed */ res = mount_volume(&path, &fs, FA_WRITE); if (res == FR_OK) { dj.obj.fs = fs; - INIT_NAMBUF(fs); - res = follow_path(&dj, path); /* Follow the file path */ - if (FF_FS_RPATH && res == FR_OK && (dj.fn[NSFLAG] & NS_DOT)) { - res = FR_INVALID_NAME; /* Cannot remove dot entry */ - } + INIT_NAMEBUFF(fs); + res = follow_path(&dj, path); /* Follow the path to the object */ + if (res == FR_OK) { + if (dj.fn[NSFLAG] & (NS_DOT | NS_NONAME)) { + res = FR_INVALID_NAME; /* It must be a real object */ + } else if (dj.obj.attr & AM_RDO) { + res = FR_DENIED; /* The object must not be read-only */ #if FF_FS_LOCK - if (res == FR_OK) res = chk_share(&dj, 2); /* Check if it is an open object */ -#endif - if (res == FR_OK) { /* The object is accessible */ - if (dj.fn[NSFLAG] & NS_NONAME) { - res = FR_INVALID_NAME; /* Cannot remove the origin directory */ } else { - if (dj.obj.attr & AM_RDO) { - res = FR_DENIED; /* Cannot remove R/O object */ - } + res = chk_share(&dj, 2); /* Check if the object is in use */ +#endif } - if (res == FR_OK) { + } + if (res == FR_OK) { /* The object is accessible */ #if FF_FS_EXFAT - obj.fs = fs; - if (fs->fs_type == FS_EXFAT) { - init_alloc_info(fs, &obj); - dclst = obj.sclust; + obj.fs = fs; + if (fs->fs_type == FS_EXFAT) { + init_alloc_info(&obj, 0); + dclst = obj.sclust; + } else +#endif + { + dclst = ld_clust(fs, dj.dir); + } + if (dj.obj.attr & AM_DIR) { /* Is the object a sub-directory? */ +#if FF_FS_RPATH + if (dclst == fs->cdir) { + res = FR_DENIED; /* Current directory cannot be removed */ } else #endif { - dclst = ld_clust(fs, dj.dir); - } - if (dj.obj.attr & AM_DIR) { /* Is it a sub-directory? */ -#if FF_FS_RPATH != 0 - if (dclst == fs->cdir) { /* Is it the current directory? */ - res = FR_DENIED; - } else -#endif - { - sdj.obj.fs = fs; /* Open the sub-directory */ - sdj.obj.sclust = dclst; + sdj.obj.fs = fs; /* Open the sub-directory */ + sdj.obj.sclust = dclst; #if FF_FS_EXFAT - if (fs->fs_type == FS_EXFAT) { - sdj.obj.objsize = obj.objsize; - sdj.obj.stat = obj.stat; - } + if (fs->fs_type == FS_EXFAT) { + sdj.obj.objsize = obj.objsize; + sdj.obj.stat = obj.stat; + } #endif - res = dir_sdi(&sdj, 0); - if (res == FR_OK) { - res = DIR_READ_FILE(&sdj); /* Test if the directory is empty */ - if (res == FR_OK) res = FR_DENIED; /* Not empty? */ - if (res == FR_NO_FILE) res = FR_OK; /* Empty? */ - } + res = dir_sdi(&sdj, 0); + if (res == FR_OK) { + res = DIR_READ_FILE(&sdj); /* Check if the sub-directory is empty */ + if (res == FR_OK) res = FR_DENIED; /* Not empty? */ + if (res == FR_NO_FILE) res = FR_OK; /* Empty? */ } } } - if (res == FR_OK) { - res = dir_remove(&dj); /* Remove the directory entry */ - if (res == FR_OK && dclst != 0) { /* Remove the cluster chain if exist */ + } + if (res == FR_OK) { /* It is ready to remove the object */ + res = dir_remove(&dj); /* Remove the directory entry */ + if (res == FR_OK && dclst != 0) { /* Remove the cluster chain if exist */ #if FF_FS_EXFAT - res = remove_chain(&obj, dclst, 0); + res = remove_chain(&obj, dclst, 0); #else - res = remove_chain(&dj.obj, dclst, 0); + res = remove_chain(&dj.obj, dclst, 0); #endif - } - if (res == FR_OK) res = sync_fs(fs); } + if (res == FR_OK) res = sync_fs(fs); } - FREE_NAMBUF(); + FREE_NAMEBUFF(); } LEAVE_FF(fs, res); @@ -5065,7 +5175,7 @@ FRESULT f_unlink ( /*-----------------------------------------------------------------------*/ -/* Create a Directory */ +/* API: Create a Directory */ /*-----------------------------------------------------------------------*/ FRESULT f_mkdir ( @@ -5077,20 +5187,19 @@ FRESULT f_mkdir ( DIR dj; FFOBJID sobj; DWORD dcl, pcl, tm; - DEF_NAMBUF + DEF_NAMEBUFF - res = mount_volume(&path, &fs, FA_WRITE); /* Get logical drive */ + res = mount_volume(&path, &fs, FA_WRITE); /* Get logical drive and mount the volume if needed */ if (res == FR_OK) { dj.obj.fs = fs; - INIT_NAMBUF(fs); + INIT_NAMEBUFF(fs); res = follow_path(&dj, path); /* Follow the file path */ - if (res == FR_OK) res = FR_EXIST; /* Name collision? */ - if (FF_FS_RPATH && res == FR_NO_FILE && (dj.fn[NSFLAG] & NS_DOT)) { /* Invalid name? */ - res = FR_INVALID_NAME; + if (res == FR_OK) { /* Invalid name or name collision */ + res = (dj.fn[NSFLAG] & (NS_DOT | NS_NONAME)) ? FR_INVALID_NAME : FR_EXIST; } if (res == FR_NO_FILE) { /* It is clear to create a new directory */ - sobj.fs = fs; /* New object id to create a new chain */ + sobj.fs = fs; /* New object ID to create a new chain */ dcl = create_chain(&sobj, 0); /* Allocate a cluster for the new directory */ res = FR_OK; if (dcl == 0) res = FR_DENIED; /* No space to allocate a new cluster? */ @@ -5098,13 +5207,13 @@ FRESULT f_mkdir ( if (dcl == 0xFFFFFFFF) res = FR_DISK_ERR; /* Disk error? */ tm = GET_FATTIME(); if (res == FR_OK) { - res = dir_clear(fs, dcl); /* Clean up the new table */ + res = dir_clear(fs, dcl); /* Clear the allocated cluster as new direcotry table */ if (res == FR_OK) { if (!FF_FS_EXFAT || fs->fs_type != FS_EXFAT) { /* Create dot entries (FAT only) */ memset(fs->win + DIR_Name, ' ', 11); /* Create "." entry */ fs->win[DIR_Name] = '.'; fs->win[DIR_Attr] = AM_DIR; - st_dword(fs->win + DIR_ModTime, tm); + st_32(fs->win + DIR_ModTime, tm); st_clust(fs, fs->win, dcl); memcpy(fs->win + SZDIRE, fs->win, SZDIRE); /* Create ".." entry */ fs->win[SZDIRE + 1] = '.'; pcl = dj.obj.sclust; @@ -5117,17 +5226,19 @@ FRESULT f_mkdir ( if (res == FR_OK) { #if FF_FS_EXFAT if (fs->fs_type == FS_EXFAT) { /* Initialize directory entry block */ - st_dword(fs->dirbuf + XDIR_ModTime, tm); /* Created time */ - st_dword(fs->dirbuf + XDIR_FstClus, dcl); /* Table start cluster */ - st_dword(fs->dirbuf + XDIR_FileSize, (DWORD)fs->csize * SS(fs)); /* Directory size needs to be valid */ - st_dword(fs->dirbuf + XDIR_ValidFileSize, (DWORD)fs->csize * SS(fs)); - fs->dirbuf[XDIR_GenFlags] = 3; /* Initialize the object flag */ - fs->dirbuf[XDIR_Attr] = AM_DIR; /* Attribute */ + st_32(fs->dirbuf + XDIR_CrtTime, tm); /* Created time */ + st_32(fs->dirbuf + XDIR_ModTime, tm); + st_32(fs->dirbuf + XDIR_FstClus, dcl); /* Table start cluster */ + st_32(fs->dirbuf + XDIR_FileSize, (DWORD)fs->csize * SS(fs)); /* Directory size needs to be valid */ + st_32(fs->dirbuf + XDIR_ValidFileSize, (DWORD)fs->csize * SS(fs)); + fs->dirbuf[XDIR_GenFlags] = 3; /* Initialize the object flag */ + fs->dirbuf[XDIR_Attr] = AM_DIR; /* Attribute */ res = store_xdir(&dj); } else #endif { - st_dword(dj.dir + DIR_ModTime, tm); /* Created time */ + st_32(dj.dir + DIR_CrtTime, tm); /* Created time */ + st_32(dj.dir + DIR_ModTime, tm); st_clust(fs, dj.dir, dcl); /* Table start cluster */ dj.dir[DIR_Attr] = AM_DIR; /* Attribute */ fs->wflag = 1; @@ -5139,7 +5250,7 @@ FRESULT f_mkdir ( remove_chain(&sobj, dcl, 0); /* Could not register, remove the allocated cluster */ } } - FREE_NAMBUF(); + FREE_NAMEBUFF(); } LEAVE_FF(fs, res); @@ -5149,7 +5260,7 @@ FRESULT f_mkdir ( /*-----------------------------------------------------------------------*/ -/* Rename a File/Directory */ +/* API: Rename a File/Directory */ /*-----------------------------------------------------------------------*/ FRESULT f_rename ( @@ -5161,42 +5272,55 @@ FRESULT f_rename ( FATFS *fs; DIR djo, djn; BYTE buf[FF_FS_EXFAT ? SZDIRE * 2 : SZDIRE], *dir; - LBA_t sect; - DEF_NAMBUF + DEF_NAMEBUFF - get_ldnumber(&path_new); /* Snip the drive number of new name off */ + get_ldnumber(&path_new); /* Snip the drive number of new name off */ res = mount_volume(&path_old, &fs, FA_WRITE); /* Get logical drive of the old object */ if (res == FR_OK) { djo.obj.fs = fs; - INIT_NAMBUF(fs); - res = follow_path(&djo, path_old); /* Check old object */ - if (res == FR_OK && (djo.fn[NSFLAG] & (NS_DOT | NS_NONAME))) res = FR_INVALID_NAME; /* Check validity of name */ -#if FF_FS_LOCK + INIT_NAMEBUFF(fs); + res = follow_path(&djo, path_old); /* Check old object */ if (res == FR_OK) { - res = chk_share(&djo, 2); - } + if (djo.fn[NSFLAG] & (NS_DOT | NS_NONAME)) { + res = FR_INVALID_NAME; /* Object must not be a dot name or blank name */ +#if FF_FS_LOCK + } else { + res = chk_share(&djo, 2); /* Check if the object is in use */ #endif - if (res == FR_OK) { /* Object to be renamed is found */ + } + } + if (res == FR_OK) { /* It is ready to rename the object */ #if FF_FS_EXFAT if (fs->fs_type == FS_EXFAT) { /* At exFAT volume */ - BYTE nf, nn; - WORD nh; +#if FF_FS_RPATH + UINT i; + DWORD dscl = ld_32(fs->dirbuf + XDIR_FstClus); - memcpy(buf, fs->dirbuf, SZDIRE * 2); /* Save 85+C0 entry of old object */ - memcpy(&djn, &djo, sizeof djo); - res = follow_path(&djn, path_new); /* Make sure if new object name is not in use */ - if (res == FR_OK) { /* Is new name already in use by any other object? */ + for (i = 1; i <= fs->xcwds.depth && dscl != fs->xcwds.tbl[i].d_scl; i++) ; /* Check if the object is a sub-dir in the current dir path */ + if (i <= fs->xcwds.depth) { + res = FR_DENIED; /* Reject to rename a sub-dir in the current dir path */ + } else +#endif + { + memcpy(buf, fs->dirbuf, SZDIRE * 2); /* Save 85+C0 entry of old object */ + memcpy(&djn, &djo, sizeof djn); + res = follow_path(&djn, path_new); /* Check if new object name collides with an existing one */ + } + if (res == FR_OK) { /* Is new name already in use by another object? */ res = (djn.obj.sclust == djo.obj.sclust && djn.dptr == djo.dptr) ? FR_NO_FILE : FR_EXIST; } - if (res == FR_NO_FILE) { /* It is a valid path and no name collision */ - res = dir_register(&djn); /* Register the new entry */ + if (res == FR_NO_FILE) { /* It is a valid path and no name collision */ + res = dir_register(&djn); /* Register the new entry */ if (res == FR_OK) { - nf = fs->dirbuf[XDIR_NumSec]; nn = fs->dirbuf[XDIR_NumName]; - nh = ld_word(fs->dirbuf + XDIR_NameHash); + BYTE nf, nn; + WORD nh; + + nf = fs->dirbuf[XDIR_NumSec]; nn = fs->dirbuf[XDIR_NumName]; /* Save name length and hash */ + nh = ld_16(fs->dirbuf + XDIR_NameHash); memcpy(fs->dirbuf, buf, SZDIRE * 2); /* Restore 85+C0 entry */ - fs->dirbuf[XDIR_NumSec] = nf; fs->dirbuf[XDIR_NumName] = nn; - st_word(fs->dirbuf + XDIR_NameHash, nh); + fs->dirbuf[XDIR_NumSec] = nf; fs->dirbuf[XDIR_NumName] = nn; /* Restore name length and hash */ + st_16(fs->dirbuf + XDIR_NameHash, nh); if (!(fs->dirbuf[XDIR_Attr] & AM_DIR)) fs->dirbuf[XDIR_Attr] |= AM_ARC; /* Set archive attribute if it is a file */ /* Start of critical section where an interruption can cause a cross-link */ res = store_xdir(&djn); @@ -5206,9 +5330,9 @@ FRESULT f_rename ( #endif { /* At FAT/FAT32 volume */ memcpy(buf, djo.dir, SZDIRE); /* Save directory entry of the object */ - memcpy(&djn, &djo, sizeof (DIR)); /* Duplicate the directory object */ + memcpy(&djn, &djo, sizeof djn); /* Duplicate the directory object */ res = follow_path(&djn, path_new); /* Make sure if new object name is not in use */ - if (res == FR_OK) { /* Is new name already in use by any other object? */ + if (res == FR_OK) { /* Is new name already in use by another object? */ res = (djn.obj.sclust == djo.obj.sclust && djn.dptr == djo.dptr) ? FR_NO_FILE : FR_EXIST; } if (res == FR_NO_FILE) { /* It is a valid path and no name collision */ @@ -5219,8 +5343,9 @@ FRESULT f_rename ( dir[DIR_Attr] = buf[DIR_Attr]; if (!(dir[DIR_Attr] & AM_DIR)) dir[DIR_Attr] |= AM_ARC; /* Set archive attribute if it is a file */ fs->wflag = 1; - if ((dir[DIR_Attr] & AM_DIR) && djo.obj.sclust != djn.obj.sclust) { /* Update .. entry in the sub-directory if needed */ - sect = clst2sect(fs, ld_clust(fs, dir)); + if ((dir[DIR_Attr] & AM_DIR) && djo.obj.sclust != djn.obj.sclust) { /* Update .. entry in the sub-directory being moved if needed */ + LBA_t sect = clst2sect(fs, ld_clust(fs, dir)); + if (sect == 0) { res = FR_INT_ERR; } else { @@ -5236,15 +5361,15 @@ FRESULT f_rename ( } } } - if (res == FR_OK) { - res = dir_remove(&djo); /* Remove old entry */ + if (res == FR_OK) { /* New entry has been created */ + res = dir_remove(&djo); /* Remove old entry */ if (res == FR_OK) { res = sync_fs(fs); } } /* End of the critical section */ } - FREE_NAMBUF(); + FREE_NAMEBUFF(); } LEAVE_FF(fs, res); @@ -5259,25 +5384,27 @@ FRESULT f_rename ( #if FF_USE_CHMOD && !FF_FS_READONLY /*-----------------------------------------------------------------------*/ -/* Change Attribute */ +/* API: Change Attribute */ /*-----------------------------------------------------------------------*/ FRESULT f_chmod ( const TCHAR* path, /* Pointer to the file path */ - BYTE attr, /* Attribute bits */ + BYTE attr, /* Attribute bits to set/clear */ BYTE mask /* Attribute mask to change */ ) { FRESULT res; FATFS *fs; DIR dj; - DEF_NAMBUF + DEF_NAMEBUFF - res = mount_volume(&path, &fs, FA_WRITE); /* Get logical drive */ + /* Get logical drive and mount the volume if needed */ + res = mount_volume(&path, &fs, FA_WRITE); + if (res == FR_OK) { dj.obj.fs = fs; - INIT_NAMBUF(fs); + INIT_NAMEBUFF(fs); res = follow_path(&dj, path); /* Follow the file path */ if (res == FR_OK && (dj.fn[NSFLAG] & (NS_DOT | NS_NONAME))) res = FR_INVALID_NAME; /* Check object validity */ if (res == FR_OK) { @@ -5296,7 +5423,7 @@ FRESULT f_chmod ( res = sync_fs(fs); } } - FREE_NAMBUF(); + FREE_NAMEBUFF(); } LEAVE_FF(fs, res); @@ -5306,42 +5433,63 @@ FRESULT f_chmod ( /*-----------------------------------------------------------------------*/ -/* Change Timestamp */ +/* API: Change Timestamp */ /*-----------------------------------------------------------------------*/ FRESULT f_utime ( const TCHAR* path, /* Pointer to the file/directory name */ - const FILINFO* fno /* Pointer to the timestamp to be set */ + const FILINFO* fno /* Timestamp to be set */ ) { FRESULT res; FATFS *fs; DIR dj; - DEF_NAMBUF + DEF_NAMEBUFF - res = mount_volume(&path, &fs, FA_WRITE); /* Get logical drive */ + /* Get logical drive and mount the volume if needed */ + res = mount_volume(&path, &fs, FA_WRITE); + if (res == FR_OK) { dj.obj.fs = fs; - INIT_NAMBUF(fs); + INIT_NAMEBUFF(fs); res = follow_path(&dj, path); /* Follow the file path */ if (res == FR_OK && (dj.fn[NSFLAG] & (NS_DOT | NS_NONAME))) res = FR_INVALID_NAME; /* Check object validity */ if (res == FR_OK) { #if FF_FS_EXFAT - if (fs->fs_type == FS_EXFAT) { - st_dword(fs->dirbuf + XDIR_ModTime, (DWORD)fno->fdate << 16 | fno->ftime); + if (fs->fs_type == FS_EXFAT) { /* On the exFAT volume */ + if (fno->fdate) { /* Change last modified time if needed */ + st_32(fs->dirbuf + XDIR_ModTime, (DWORD)fno->fdate << 16 | fno->ftime); + fs->dirbuf[XDIR_ModTime10] = 0; + fs->dirbuf[XDIR_ModTZ] = 0; + } +#if FF_FS_CRTIME + if (fno->crdate) { /* Change created time if needed */ + st_32(fs->dirbuf + XDIR_CrtTime, (DWORD)fno->crdate << 16 | fno->crtime); + fs->dirbuf[XDIR_CrtTime10] = 0; + fs->dirbuf[XDIR_CrtTZ] = 0; + } +#endif res = store_xdir(&dj); } else #endif - { - st_dword(dj.dir + DIR_ModTime, (DWORD)fno->fdate << 16 | fno->ftime); + { /* On the FAT volume */ + if (fno->fdate) { /* Change last modified time if needed */ + st_32(dj.dir + DIR_ModTime, (DWORD)fno->fdate << 16 | fno->ftime); + } +#if FF_FS_CRTIME + if (fno->crdate) { /* Change created time if needed */ + st_32(dj.dir + DIR_CrtTime, (DWORD)fno->crdate << 16 | fno->crtime); + dj.dir[DIR_CrtTime10] = 0; + } +#endif fs->wflag = 1; } if (res == FR_OK) { res = sync_fs(fs); } } - FREE_NAMBUF(); + FREE_NAMEBUFF(); } LEAVE_FF(fs, res); @@ -5353,7 +5501,7 @@ FRESULT f_utime ( #if FF_USE_LABEL /*-----------------------------------------------------------------------*/ -/* Get Volume Label */ +/* API: Get Volume Label */ /*-----------------------------------------------------------------------*/ FRESULT f_getlabel ( @@ -5368,8 +5516,8 @@ FRESULT f_getlabel ( UINT si, di; WCHAR wc; - /* Get logical drive */ - res = mount_volume(&path, &fs, 0); + + res = mount_volume(&path, &fs, 0); /* Get logical drive and mount the volume if needed */ /* Get volume label */ if (res == FR_OK && label) { @@ -5383,8 +5531,8 @@ FRESULT f_getlabel ( WCHAR hs; UINT nw; - for (si = di = hs = 0; si < dj.dir[XDIR_NumLabel]; si++) { /* Extract volume label from 83 entry */ - wc = ld_word(dj.dir + XDIR_Label + si * 2); + for (si = di = hs = 0; si < dj.dir[XDIR_NumLabel] && si < 11; si++) { /* Extract volume label from 83 entry */ + wc = ld_16(dj.dir + XDIR_Label + si * 2); if (hs == 0 && IsSurrogate(wc)) { /* Is the code a surrogate? */ hs = wc; continue; } @@ -5443,7 +5591,7 @@ FRESULT f_getlabel ( default: /* FAT12/16 */ di = fs->win[BS_BootSig] == 0x29 ? BS_VolID : 0; } - *vsn = di ? ld_dword(fs->win + di) : 0; /* Get VSN in the VBR */ + *vsn = di ? ld_32(fs->win + di) : 0; /* Get VSN in the VBR */ } } @@ -5454,7 +5602,7 @@ FRESULT f_getlabel ( #if !FF_FS_READONLY /*-----------------------------------------------------------------------*/ -/* Set Volume Label */ +/* API: Set Volume Label */ /*-----------------------------------------------------------------------*/ FRESULT f_setlabel ( @@ -5472,7 +5620,7 @@ FRESULT f_setlabel ( DWORD dc; #endif - /* Get logical drive */ + /* Get logical drive and mount the volume if needed */ res = mount_volume(&label, &fs, FA_WRITE); if (res != FR_OK) LEAVE_FF(fs, res); #if FF_STR_VOLUME_ID == 2 @@ -5489,13 +5637,13 @@ FRESULT f_setlabel ( if (dc == 0xFFFFFFFF || di >= 10) { /* Wrong surrogate or buffer overflow */ dc = 0; } else { - st_word(dirvn + di * 2, (WCHAR)(dc >> 16)); di++; + st_16(dirvn + di * 2, (WCHAR)(dc >> 16)); di++; } } if (dc == 0 || strchr(&badchr[7], (int)dc) || di >= 11) { /* Check validity of the volume label */ LEAVE_FF(fs, FR_INVALID_NAME); } - st_word(dirvn + di * 2, (WCHAR)dc); di++; + st_16(dirvn + di * 2, (WCHAR)dc); di++; } } else #endif @@ -5577,7 +5725,7 @@ FRESULT f_setlabel ( #if FF_USE_EXPAND && !FF_FS_READONLY /*-----------------------------------------------------------------------*/ -/* Allocate a Contiguous Blocks to the File */ +/* API: Allocate a Contiguous Blocks to the File */ /*-----------------------------------------------------------------------*/ FRESULT f_expand ( @@ -5673,7 +5821,7 @@ FRESULT f_expand ( #if FF_USE_FORWARD /*-----------------------------------------------------------------------*/ -/* Forward Data to the Stream Directly */ +/* API: Forward Data to the Stream Directly */ /*-----------------------------------------------------------------------*/ FRESULT f_forward ( @@ -5744,12 +5892,12 @@ FRESULT f_forward ( #if !FF_FS_READONLY && FF_USE_MKFS /*-----------------------------------------------------------------------*/ -/* Create FAT/exFAT volume (with sub-functions) */ +/* API: Create FAT/exFAT volume (with a sub-function) */ /*-----------------------------------------------------------------------*/ #define N_SEC_TRACK 63 /* Sectors per track for determination of drive CHS */ #define GPT_ALIGN 0x100000 /* Alignment of partitions in GPT [byte] (>=128KB) */ -#define GPT_ITEMS 128 /* Number of GPT table size (>=128, sector aligned) */ +#define GPT_ITEMS 128 /* Number of GPT table items (>=128, sector aligned) */ /* Create partitions on the physical drive in format of MBR or GPT */ @@ -5809,8 +5957,8 @@ static FRESULT create_partition ( ofs = pi * SZ_GPTE % ss; memcpy(buf + ofs + GPTE_PtGuid, GUID_MS_Basic, 16); /* Set partition GUID (Microsoft Basic Data) */ rnd = make_rand(rnd, buf + ofs + GPTE_UpGuid, 16); /* Set unique partition GUID */ - st_qword(buf + ofs + GPTE_FstLba, nxt_alloc); /* Set partition start LBA */ - st_qword(buf + ofs + GPTE_LstLba, nxt_alloc + sz_part - 1); /* Set partition end LBA */ + st_64(buf + ofs + GPTE_FstLba, nxt_alloc); /* Set partition start LBA */ + st_64(buf + ofs + GPTE_LstLba, nxt_alloc + sz_part - 1); /* Set partition end LBA */ nxt_alloc += sz_part; /* Next allocatable LBA */ } if ((pi + 1) * SZ_GPTE % ss == 0) { /* Write the sector buffer if it is filled up */ @@ -5823,32 +5971,32 @@ static FRESULT create_partition ( /* Create primary GPT header */ memset(buf, 0, ss); memcpy(buf + GPTH_Sign, "EFI PART" "\0\0\1\0" "\x5C\0\0", 16); /* Signature, version (1.0) and size (92) */ - st_dword(buf + GPTH_PtBcc, ~bcc); /* Table check sum */ - st_qword(buf + GPTH_CurLba, 1); /* LBA of this header */ - st_qword(buf + GPTH_BakLba, sz_drv - 1); /* LBA of secondary header */ - st_qword(buf + GPTH_FstLba, 2 + sz_ptbl); /* LBA of first allocatable sector */ - st_qword(buf + GPTH_LstLba, top_bpt - 1); /* LBA of last allocatable sector */ - st_dword(buf + GPTH_PteSize, SZ_GPTE); /* Size of a table entry */ - st_dword(buf + GPTH_PtNum, GPT_ITEMS); /* Number of table entries */ - st_dword(buf + GPTH_PtOfs, 2); /* LBA of this table */ + st_32(buf + GPTH_PtBcc, ~bcc); /* Table check sum */ + st_64(buf + GPTH_CurLba, 1); /* LBA of this header */ + st_64(buf + GPTH_BakLba, sz_drv - 1); /* LBA of secondary header */ + st_64(buf + GPTH_FstLba, 2 + sz_ptbl); /* LBA of first allocatable sector */ + st_64(buf + GPTH_LstLba, top_bpt - 1); /* LBA of last allocatable sector */ + st_32(buf + GPTH_PteSize, SZ_GPTE); /* Size of a table entry */ + st_32(buf + GPTH_PtNum, GPT_ITEMS); /* Number of table entries */ + st_32(buf + GPTH_PtOfs, 2); /* LBA of this table */ rnd = make_rand(rnd, buf + GPTH_DskGuid, 16); /* Disk GUID */ for (i = 0, bcc= 0xFFFFFFFF; i < 92; bcc = crc32(bcc, buf[i++])) ; /* Calculate header check sum */ - st_dword(buf + GPTH_Bcc, ~bcc); /* Header check sum */ + st_32(buf + GPTH_Bcc, ~bcc); /* Header check sum */ if (disk_write(drv, buf, 1, 1) != RES_OK) return FR_DISK_ERR; /* Create secondary GPT header */ - st_qword(buf + GPTH_CurLba, sz_drv - 1); /* LBA of this header */ - st_qword(buf + GPTH_BakLba, 1); /* LBA of primary header */ - st_qword(buf + GPTH_PtOfs, top_bpt); /* LBA of this table */ - st_dword(buf + GPTH_Bcc, 0); + st_64(buf + GPTH_CurLba, sz_drv - 1); /* LBA of this header */ + st_64(buf + GPTH_BakLba, 1); /* LBA of primary header */ + st_64(buf + GPTH_PtOfs, top_bpt); /* LBA of this table */ + st_32(buf + GPTH_Bcc, 0); for (i = 0, bcc= 0xFFFFFFFF; i < 92; bcc = crc32(bcc, buf[i++])) ; /* Calculate header check sum */ - st_dword(buf + GPTH_Bcc, ~bcc); /* Header check sum */ + st_32(buf + GPTH_Bcc, ~bcc); /* Header check sum */ if (disk_write(drv, buf, sz_drv - 1, 1) != RES_OK) return FR_DISK_ERR; /* Create protective MBR */ memset(buf, 0, ss); - memcpy(buf + MBR_Table, gpt_mbr, 16); /* Create a GPT partition */ - st_word(buf + BS_55AA, 0xAA55); + memcpy(buf + MBR_Table, gpt_mbr, 16); /* Create a GPT partition */ + st_16(buf + BS_55AA, 0xAA55); if (disk_write(drv, buf, 0, 1) != RES_OK) return FR_DISK_ERR; } else @@ -5867,8 +6015,8 @@ static FRESULT create_partition ( if (nxt_alloc32 + sz_part32 > sz_drv32 || nxt_alloc32 + sz_part32 < nxt_alloc32) sz_part32 = sz_drv32 - nxt_alloc32; /* Clip at drive size */ if (sz_part32 == 0) break; /* End of table or no sector to allocate? */ - st_dword(pte + PTE_StLba, nxt_alloc32); /* Partition start LBA sector */ - st_dword(pte + PTE_SizLba, sz_part32); /* Size of partition [sector] */ + st_32(pte + PTE_StLba, nxt_alloc32); /* Partition start LBA sector */ + st_32(pte + PTE_SizLba, sz_part32); /* Size of partition [sector] */ pte[PTE_System] = sys; /* System type */ cy = (UINT)(nxt_alloc32 / n_sc / n_hd); /* Partitio start CHS cylinder */ @@ -5888,7 +6036,7 @@ static FRESULT create_partition ( pte += SZ_PTE; /* Next entry */ } - st_word(buf + BS_55AA, 0xAA55); /* MBR signature */ + st_16(buf + BS_55AA, 0xAA55); /* MBR signature */ if (disk_write(drv, buf, 0, 1) != RES_OK) return FR_DISK_ERR; /* Write it to the MBR */ } @@ -5922,7 +6070,7 @@ FRESULT f_mkfs ( /* Check mounted drive and clear work area */ - vol = get_ldnumber(&path); /* Get target logical drive */ + vol = get_ldnumber(&path); /* Get logical drive number to be formatted */ if (vol < 0) return FR_INVALID_DRIVE; if (FatFs[vol]) FatFs[vol]->fs_type = 0; /* Clear the fs object if mounted */ pdrv = LD2PD(vol); /* Hosting physical drive */ @@ -5966,7 +6114,7 @@ FRESULT f_mkfs ( if (FF_MULTI_PARTITION && ipart != 0) { /* Is the volume associated with any specific partition? */ /* Get partition location from the existing partition table */ if (disk_read(pdrv, buf, 0, 1) != RES_OK) LEAVE_MKFS(FR_DISK_ERR); /* Load MBR */ - if (ld_word(buf + BS_55AA) != 0xAA55) LEAVE_MKFS(FR_MKFS_ABORTED); /* Check if MBR is valid */ + if (ld_16(buf + BS_55AA) != 0xAA55) LEAVE_MKFS(FR_MKFS_ABORTED); /* Check if MBR is valid */ #if FF_LBA64 if (buf[MBR_Table + PTE_System] == 0xEE) { /* GPT protective MBR? */ DWORD n_ent, ofs; @@ -5975,14 +6123,14 @@ FRESULT f_mkfs ( /* Get the partition location from GPT */ if (disk_read(pdrv, buf, 1, 1) != RES_OK) LEAVE_MKFS(FR_DISK_ERR); /* Load GPT header sector (next to MBR) */ if (!test_gpt_header(buf)) LEAVE_MKFS(FR_MKFS_ABORTED); /* Check if GPT header is valid */ - n_ent = ld_dword(buf + GPTH_PtNum); /* Number of entries */ - pt_lba = ld_qword(buf + GPTH_PtOfs); /* Table start sector */ + n_ent = ld_32(buf + GPTH_PtNum); /* Number of entries */ + pt_lba = ld_64(buf + GPTH_PtOfs); /* Table start sector */ ofs = i = 0; while (n_ent) { /* Find MS Basic partition with order of ipart */ if (ofs == 0 && disk_read(pdrv, buf, pt_lba++, 1) != RES_OK) LEAVE_MKFS(FR_DISK_ERR); /* Get PT sector */ if (!memcmp(buf + ofs + GPTE_PtGuid, GUID_MS_Basic, 16) && ++i == ipart) { /* MS basic data partition? */ - b_vol = ld_qword(buf + ofs + GPTE_FstLba); - sz_vol = ld_qword(buf + ofs + GPTE_LstLba) - b_vol + 1; + b_vol = ld_64(buf + ofs + GPTE_FstLba); + sz_vol = ld_64(buf + ofs + GPTE_LstLba) - b_vol + 1; break; } n_ent--; ofs = (ofs + SZ_GPTE) % ss; /* Next entry */ @@ -5994,8 +6142,8 @@ FRESULT f_mkfs ( { /* Get the partition location from MBR partition table */ pte = buf + (MBR_Table + (ipart - 1) * SZ_PTE); if (ipart > 4 || pte[PTE_System] == 0) LEAVE_MKFS(FR_MKFS_ABORTED); /* No partition? */ - b_vol = ld_dword(pte + PTE_StLba); /* Get volume start sector */ - sz_vol = ld_dword(pte + PTE_SizLba); /* Get volume size */ + b_vol = ld_32(pte + PTE_StLba); /* Get volume start sector */ + sz_vol = ld_32(pte + PTE_SizLba); /* Get volume size */ } } else { /* The volume is associated with a physical drive */ if (disk_ioctl(pdrv, GET_SECTOR_COUNT, &sz_vol) != RES_OK) LEAVE_MKFS(FR_DISK_ERR); @@ -6014,13 +6162,13 @@ FRESULT f_mkfs ( } } } - if (sz_vol < 128) LEAVE_MKFS(FR_MKFS_ABORTED); /* Check if volume size is >=128 sectors */ + if (sz_vol < MIN_VOLUME) LEAVE_MKFS(FR_MKFS_ABORTED); /* Check if volume size is not too small */ /* Now start to create an FAT volume at b_vol and sz_vol */ do { /* Pre-determine the FAT type */ if (FF_FS_EXFAT && (fsopt & FM_EXFAT)) { /* exFAT possible? */ - if ((fsopt & FM_ANY) == FM_EXFAT || sz_vol >= 0x4000000 || sz_au > 128) { /* exFAT only, vol >= 64M sectors or sz_au > 128 sectors ? */ + if ((fsopt & FM_ANY) == FM_EXFAT || sz_vol >= 0x4000000 || sz_au > 128) { /* exFAT only, vol >= 64M sectors or sz_au > 128 sectors? */ fsty = FS_EXFAT; break; } } @@ -6122,13 +6270,13 @@ FRESULT f_mkfs ( do { memset(buf, 0, sz_buf * ss); i = 0; /* Clear work area and reset write offset */ if (clu == 0) { /* Initialize FAT [0] and FAT[1] */ - st_dword(buf + i, 0xFFFFFFF8); i += 4; clu++; - st_dword(buf + i, 0xFFFFFFFF); i += 4; clu++; + st_32(buf + i, 0xFFFFFFF8); i += 4; clu++; + st_32(buf + i, 0xFFFFFFFF); i += 4; clu++; } do { /* Create chains of bitmap, up-case and root directory */ while (nbit != 0 && i < sz_buf * ss) { /* Create a chain */ - st_dword(buf + i, (nbit > 1) ? clu + 1 : 0xFFFFFFFF); + st_32(buf + i, (nbit > 1) ? clu + 1 : 0xFFFFFFFF); i += 4; clu++; nbit--; } if (nbit == 0 && j < 3) nbit = clen[j++]; /* Get next chain length */ @@ -6140,14 +6288,14 @@ FRESULT f_mkfs ( /* Initialize the root directory */ memset(buf, 0, sz_buf * ss); - buf[SZDIRE * 0 + 0] = ET_VLABEL; /* Volume label entry (no label) */ - buf[SZDIRE * 1 + 0] = ET_BITMAP; /* Bitmap entry */ - st_dword(buf + SZDIRE * 1 + 20, 2); /* cluster */ - st_dword(buf + SZDIRE * 1 + 24, szb_bit); /* size */ - buf[SZDIRE * 2 + 0] = ET_UPCASE; /* Up-case table entry */ - st_dword(buf + SZDIRE * 2 + 4, sum); /* sum */ - st_dword(buf + SZDIRE * 2 + 20, 2 + clen[0]); /* cluster */ - st_dword(buf + SZDIRE * 2 + 24, szb_case); /* size */ + buf[SZDIRE * 0 + 0] = ET_VLABEL; /* Volume label entry (no label) */ + buf[SZDIRE * 1 + 0] = ET_BITMAP; /* Bitmap entry */ + st_32(buf + SZDIRE * 1 + 20, 2); /* cluster */ + st_32(buf + SZDIRE * 1 + 24, szb_bit); /* size */ + buf[SZDIRE * 2 + 0] = ET_UPCASE; /* Up-case table entry */ + st_32(buf + SZDIRE * 2 + 4, sum); /* sum */ + st_32(buf + SZDIRE * 2 + 20, 2 + clen[0]); /* cluster */ + st_32(buf + SZDIRE * 2 + 24, szb_case); /* size */ sect = b_data + sz_au * (clen[0] + clen[1]); nsect = sz_au; /* Start of the root directory and number of sectors */ do { /* Fill root directory sectors */ n = (nsect > sz_buf) ? sz_buf : nsect; @@ -6162,28 +6310,28 @@ FRESULT f_mkfs ( /* Main record (+0) */ memset(buf, 0, ss); memcpy(buf + BS_JmpBoot, "\xEB\x76\x90" "EXFAT ", 11); /* Boot jump code (x86), OEM name */ - st_qword(buf + BPB_VolOfsEx, b_vol); /* Volume offset in the physical drive [sector] */ - st_qword(buf + BPB_TotSecEx, sz_vol); /* Volume size [sector] */ - st_dword(buf + BPB_FatOfsEx, (DWORD)(b_fat - b_vol)); /* FAT offset [sector] */ - st_dword(buf + BPB_FatSzEx, sz_fat); /* FAT size [sector] */ - st_dword(buf + BPB_DataOfsEx, (DWORD)(b_data - b_vol)); /* Data offset [sector] */ - st_dword(buf + BPB_NumClusEx, n_clst); /* Number of clusters */ - st_dword(buf + BPB_RootClusEx, 2 + clen[0] + clen[1]); /* Root directory cluster number */ - st_dword(buf + BPB_VolIDEx, vsn); /* VSN */ - st_word(buf + BPB_FSVerEx, 0x100); /* Filesystem version (1.00) */ + st_64(buf + BPB_VolOfsEx, b_vol); /* Volume offset in the physical drive [sector] */ + st_64(buf + BPB_TotSecEx, sz_vol); /* Volume size [sector] */ + st_32(buf + BPB_FatOfsEx, (DWORD)(b_fat - b_vol)); /* FAT offset [sector] */ + st_32(buf + BPB_FatSzEx, sz_fat); /* FAT size [sector] */ + st_32(buf + BPB_DataOfsEx, (DWORD)(b_data - b_vol)); /* Data offset [sector] */ + st_32(buf + BPB_NumClusEx, n_clst); /* Number of clusters */ + st_32(buf + BPB_RootClusEx, 2 + clen[0] + clen[1]); /* Root directory cluster number */ + st_32(buf + BPB_VolIDEx, vsn); /* VSN */ + st_16(buf + BPB_FSVerEx, 0x100); /* Filesystem version (1.00) */ for (buf[BPB_BytsPerSecEx] = 0, i = ss; i >>= 1; buf[BPB_BytsPerSecEx]++) ; /* Log2 of sector size [byte] */ for (buf[BPB_SecPerClusEx] = 0, i = sz_au; i >>= 1; buf[BPB_SecPerClusEx]++) ; /* Log2 of cluster size [sector] */ buf[BPB_NumFATsEx] = 1; /* Number of FATs */ buf[BPB_DrvNumEx] = 0x80; /* Drive number (for int13) */ - st_word(buf + BS_BootCodeEx, 0xFEEB); /* Boot code (x86) */ - st_word(buf + BS_55AA, 0xAA55); /* Signature (placed here regardless of sector size) */ + st_16(buf + BS_BootCodeEx, 0xFEEB); /* Boot code (x86) */ + st_16(buf + BS_55AA, 0xAA55); /* Signature (placed here regardless of sector size) */ for (i = sum = 0; i < ss; i++) { /* VBR checksum */ if (i != BPB_VolFlagEx && i != BPB_VolFlagEx + 1 && i != BPB_PercInUseEx) sum = xsum32(buf[i], sum); } if (disk_write(pdrv, buf, sect++, 1) != RES_OK) LEAVE_MKFS(FR_DISK_ERR); /* Extended bootstrap record (+1..+8) */ memset(buf, 0, ss); - st_word(buf + ss - 2, 0xAA55); /* Signature (placed at end of sector) */ + st_16(buf + ss - 2, 0xAA55); /* Signature (placed at end of sector) */ for (j = 1; j < 9; j++) { for (i = 0; i < ss; sum = xsum32(buf[i++], sum)) ; /* VBR checksum */ if (disk_write(pdrv, buf, sect++, 1) != RES_OK) LEAVE_MKFS(FR_DISK_ERR); @@ -6195,7 +6343,7 @@ FRESULT f_mkfs ( if (disk_write(pdrv, buf, sect++, 1) != RES_OK) LEAVE_MKFS(FR_DISK_ERR); } /* Sum record (+11) */ - for (i = 0; i < ss; i += 4) st_dword(buf + i, sum); /* Fill with checksum value */ + for (i = 0; i < ss; i += 4) st_32(buf + i, sum); /* Fill with checksum value */ if (disk_write(pdrv, buf, sect++, 1) != RES_OK) LEAVE_MKFS(FR_DISK_ERR); } @@ -6246,7 +6394,7 @@ FRESULT f_mkfs ( } /* Determine number of clusters and final check of validity of the FAT sub-type */ - if (sz_vol < b_data + pau * 16 - b_vol) LEAVE_MKFS(FR_MKFS_ABORTED); /* Too small volume? */ + if (sz_vol < b_data + pau * MIN_FAT12 - b_vol) LEAVE_MKFS(FR_MKFS_ABORTED); /* Too small volume for this configuration? */ n_clst = ((DWORD)sz_vol - sz_rsv - sz_fat * n_fat - sz_dir) / pau; if (fsty == FS_FAT32) { if (n_clst <= MAX_FAT16) { /* Too few clusters for FAT32? */ @@ -6283,48 +6431,48 @@ FRESULT f_mkfs ( /* Create FAT VBR */ memset(buf, 0, ss); memcpy(buf + BS_JmpBoot, "\xEB\xFE\x90" "MSDOS5.0", 11); /* Boot jump code (x86), OEM name */ - st_word(buf + BPB_BytsPerSec, ss); /* Sector size [byte] */ + st_16(buf + BPB_BytsPerSec, ss); /* Sector size [byte] */ buf[BPB_SecPerClus] = (BYTE)pau; /* Cluster size [sector] */ - st_word(buf + BPB_RsvdSecCnt, (WORD)sz_rsv); /* Size of reserved area */ + st_16(buf + BPB_RsvdSecCnt, (WORD)sz_rsv); /* Size of reserved area */ buf[BPB_NumFATs] = (BYTE)n_fat; /* Number of FATs */ - st_word(buf + BPB_RootEntCnt, (WORD)((fsty == FS_FAT32) ? 0 : n_root)); /* Number of root directory entries */ + st_16(buf + BPB_RootEntCnt, (WORD)((fsty == FS_FAT32) ? 0 : n_root)); /* Number of root directory entries */ if (sz_vol < 0x10000) { - st_word(buf + BPB_TotSec16, (WORD)sz_vol); /* Volume size in 16-bit LBA */ + st_16(buf + BPB_TotSec16, (WORD)sz_vol); /* Volume size in 16-bit LBA */ } else { - st_dword(buf + BPB_TotSec32, (DWORD)sz_vol); /* Volume size in 32-bit LBA */ + st_32(buf + BPB_TotSec32, (DWORD)sz_vol); /* Volume size in 32-bit LBA */ } buf[BPB_Media] = 0xF8; /* Media descriptor byte */ - st_word(buf + BPB_SecPerTrk, 63); /* Number of sectors per track (for int13) */ - st_word(buf + BPB_NumHeads, 255); /* Number of heads (for int13) */ - st_dword(buf + BPB_HiddSec, (DWORD)b_vol); /* Volume offset in the physical drive [sector] */ + st_16(buf + BPB_SecPerTrk, 63); /* Number of sectors per track (for int13) */ + st_16(buf + BPB_NumHeads, 255); /* Number of heads (for int13) */ + st_32(buf + BPB_HiddSec, (DWORD)b_vol); /* Volume offset in the physical drive [sector] */ if (fsty == FS_FAT32) { - st_dword(buf + BS_VolID32, vsn); /* VSN */ - st_dword(buf + BPB_FATSz32, sz_fat); /* FAT size [sector] */ - st_dword(buf + BPB_RootClus32, 2); /* Root directory cluster # (2) */ - st_word(buf + BPB_FSInfo32, 1); /* Offset of FSINFO sector (VBR + 1) */ - st_word(buf + BPB_BkBootSec32, 6); /* Offset of backup VBR (VBR + 6) */ + st_32(buf + BS_VolID32, vsn); /* VSN */ + st_32(buf + BPB_FATSz32, sz_fat); /* FAT size [sector] */ + st_32(buf + BPB_RootClus32, 2); /* Root directory cluster # (2) */ + st_16(buf + BPB_FSInfo32, 1); /* Offset of FSINFO sector (VBR + 1) */ + st_16(buf + BPB_BkBootSec32, 6); /* Offset of backup VBR (VBR + 6) */ buf[BS_DrvNum32] = 0x80; /* Drive number (for int13) */ buf[BS_BootSig32] = 0x29; /* Extended boot signature */ memcpy(buf + BS_VolLab32, "NO NAME " "FAT32 ", 19); /* Volume label, FAT signature */ } else { - st_dword(buf + BS_VolID, vsn); /* VSN */ - st_word(buf + BPB_FATSz16, (WORD)sz_fat); /* FAT size [sector] */ + st_32(buf + BS_VolID, vsn); /* VSN */ + st_16(buf + BPB_FATSz16, (WORD)sz_fat); /* FAT size [sector] */ buf[BS_DrvNum] = 0x80; /* Drive number (for int13) */ buf[BS_BootSig] = 0x29; /* Extended boot signature */ memcpy(buf + BS_VolLab, "NO NAME " "FAT ", 19); /* Volume label, FAT signature */ } - st_word(buf + BS_55AA, 0xAA55); /* Signature (offset is fixed here regardless of sector size) */ + st_16(buf + BS_55AA, 0xAA55); /* Signature (offset is fixed here regardless of sector size) */ if (disk_write(pdrv, buf, b_vol, 1) != RES_OK) LEAVE_MKFS(FR_DISK_ERR); /* Write it to the VBR sector */ /* Create FSINFO record if needed */ if (fsty == FS_FAT32) { disk_write(pdrv, buf, b_vol + 6, 1); /* Write backup VBR (VBR + 6) */ memset(buf, 0, ss); - st_dword(buf + FSI_LeadSig, 0x41615252); - st_dword(buf + FSI_StrucSig, 0x61417272); - st_dword(buf + FSI_Free_Count, n_clst - 1); /* Number of free clusters */ - st_dword(buf + FSI_Nxt_Free, 2); /* Last allocated cluster# */ - st_word(buf + BS_55AA, 0xAA55); + st_32(buf + FSI_LeadSig, 0x41615252); + st_32(buf + FSI_StrucSig, 0x61417272); + st_32(buf + FSI_Free_Count, n_clst - 1); /* Number of free clusters */ + st_32(buf + FSI_Nxt_Free, 2); /* Last allocated cluster# */ + st_16(buf + BS_55AA, 0xAA55); disk_write(pdrv, buf, b_vol + 7, 1); /* Write backup FSINFO (VBR + 7) */ disk_write(pdrv, buf, b_vol + 1, 1); /* Write original FSINFO (VBR + 1) */ } @@ -6334,11 +6482,11 @@ FRESULT f_mkfs ( sect = b_fat; /* FAT start sector */ for (i = 0; i < n_fat; i++) { /* Initialize FATs each */ if (fsty == FS_FAT32) { - st_dword(buf + 0, 0xFFFFFFF8); /* FAT[0] */ - st_dword(buf + 4, 0xFFFFFFFF); /* FAT[1] */ - st_dword(buf + 8, 0x0FFFFFFF); /* FAT[2] (root directory at cluster# 2) */ + st_32(buf + 0, 0xFFFFFFF8); /* FAT[0] */ + st_32(buf + 4, 0xFFFFFFFF); /* FAT[1] */ + st_32(buf + 8, 0x0FFFFFFF); /* FAT[2] (root directory at cluster# 2) */ } else { - st_dword(buf + 0, (fsty == FS_FAT12) ? 0xFFFFF8 : 0xFFFFFFF8); /* FAT[0] and FAT[1] */ + st_32(buf + 0, (fsty == FS_FAT12) ? 0xFFFFF8 : 0xFFFFFFF8); /* FAT[0] and FAT[1] */ } nsect = sz_fat; /* Number of FAT sectors */ do { /* Fill FAT sectors */ @@ -6399,7 +6547,7 @@ FRESULT f_mkfs ( #if FF_MULTI_PARTITION /*-----------------------------------------------------------------------*/ -/* Create Partition Table on the Physical Drive */ +/* API: Create Partition Table on the Physical Drive */ /*-----------------------------------------------------------------------*/ FRESULT f_fdisk ( @@ -6439,7 +6587,7 @@ FRESULT f_fdisk ( #error Wrong FF_STRF_ENCODE setting #endif /*-----------------------------------------------------------------------*/ -/* Get a String from the File */ +/* API: Get a String from the File */ /*-----------------------------------------------------------------------*/ TCHAR* f_gets ( @@ -6480,12 +6628,12 @@ TCHAR* f_gets ( #elif FF_STRF_ENCODE == 1 || FF_STRF_ENCODE == 2 /* Read a character in UTF-16LE/BE */ f_read(fp, s, 2, &rc); /* Get a code unit */ if (rc != 2) break; /* EOF? */ - dc = (FF_STRF_ENCODE == 1) ? ld_word(s) : s[0] << 8 | s[1]; + dc = (FF_STRF_ENCODE == 1) ? ld_16(s) : s[0] << 8 | s[1]; if (IsSurrogateL(dc)) continue; /* Broken surrogate pair? */ if (IsSurrogateH(dc)) { /* High surrogate? */ f_read(fp, s, 2, &rc); /* Get low surrogate */ if (rc != 2) break; /* EOF? */ - wc = (FF_STRF_ENCODE == 1) ? ld_word(s) : s[0] << 8 | s[1]; + wc = (FF_STRF_ENCODE == 1) ? ld_16(s) : s[0] << 8 | s[1]; if (!IsSurrogateL(wc)) continue; /* Broken surrogate pair? */ dc = ((dc & 0x3FF) + 0x40) << 10 | (wc & 0x3FF); /* Merge surrogate pair */ } @@ -6574,7 +6722,7 @@ TCHAR* f_gets ( #define SZ_NUM_BUF 32 /*-----------------------------------------------------------------------*/ -/* Put a Character to the File (with sub-functions) */ +/* API: Put a Character to the File (with sub-functions) */ /*-----------------------------------------------------------------------*/ /* Output buffer and work area */ @@ -6663,11 +6811,11 @@ static void putc_bfd (putbuff* pb, TCHAR c) #if FF_STRF_ENCODE == 1 /* Write a code point in UTF-16LE */ if (hs != 0) { /* Surrogate pair? */ - st_word(&pb->buf[i], hs); + st_16(&pb->buf[i], hs); i += 2; nc++; } - st_word(&pb->buf[i], wc); + st_16(&pb->buf[i], wc); i += 2; #elif FF_STRF_ENCODE == 2 /* Write a code point in UTF-16BE */ if (hs != 0) { /* Surrogate pair? */ @@ -6765,7 +6913,7 @@ int f_putc ( /*-----------------------------------------------------------------------*/ -/* Put a String to the File */ +/* API: Put a String to the File */ /*-----------------------------------------------------------------------*/ int f_puts ( @@ -6785,7 +6933,7 @@ int f_puts ( /*-----------------------------------------------------------------------*/ -/* Put a Formatted String to the File (with sub-functions) */ +/* API: Put a Formatted String to the File (with sub-functions) */ /*-----------------------------------------------------------------------*/ #if FF_PRINT_FLOAT && FF_INTDEF == 2 #include @@ -7077,7 +7225,7 @@ int f_printf ( #if FF_CODE_PAGE == 0 /*-----------------------------------------------------------------------*/ -/* Set Active Codepage for the Path Name */ +/* API: Set Active Codepage for the Path Name */ /*-----------------------------------------------------------------------*/ FRESULT f_setcp ( diff --git a/src/fatfs/ff.h b/src/fatfs/ff.h index e39df1d03..aae57e53f 100644 --- a/src/fatfs/ff.h +++ b/src/fatfs/ff.h @@ -1,8 +1,8 @@ /*----------------------------------------------------------------------------/ -/ FatFs - Generic FAT Filesystem module R0.15a / +/ FatFs - Generic FAT Filesystem module R0.16 / /-----------------------------------------------------------------------------/ / -/ Copyright (C) 2024, ChaN, all right reserved. +/ Copyright (C) 2025, ChaN, all right reserved. / / FatFs module is an open source software. Redistribution and use of FatFs in / source and binary forms, with or without modification, are permitted provided @@ -20,7 +20,7 @@ #ifndef FF_DEFINED -#define FF_DEFINED 5380 /* Revision ID */ +#define FF_DEFINED 80386 /* Revision ID */ #ifdef __cplusplus extern "C" { @@ -127,51 +127,65 @@ extern const char* VolumeStr[FF_VOLUMES]; /* User defined volume ID table */ #endif +/* Current working directory structure (FFXCWDS) */ + +#if FF_FS_EXFAT && FF_FS_RPATH +#if FF_PATH_DEPTH < 1 +#error FF_PATH_DEPTH must not be zero +#endif +typedef struct { + DWORD d_scl; /* Directory start cluster (0:root dir) */ + DWORD d_size; /* Size of directory (b7-b0: cluster chain status) (invalid if d_scl == 0) */ + DWORD nxt_ofs; /* Offset of entry of next dir in this directory (invalid if last link) */ +} FFXCWDL; +typedef struct { + UINT depth; /* Current directory depth (0:root dir) */ + FFXCWDL tbl[FF_PATH_DEPTH + 1]; /* Directory chain of current working directory path */ +} FFXCWDS; +#endif + /* Filesystem object structure (FATFS) */ typedef struct { - BYTE fs_type; /* Filesystem type (0:blank filesystem object) */ - BYTE pdrv; /* Volume hosting physical drive */ - BYTE ldrv; /* Logical drive number (used only when FF_FS_REENTRANT) */ - BYTE n_fats; /* Number of FATs (1 or 2) */ - BYTE wflag; /* win[] status (1:dirty) */ - BYTE fsi_flag; /* Allocation information control (b7:disabled, b0:dirty) */ - WORD id; /* Volume mount ID */ - WORD n_rootdir; /* Number of root directory entries (FAT12/16) */ - WORD csize; /* Cluster size [sectors] */ + BYTE fs_type; /* Filesystem type (0:not mounted) */ + BYTE pdrv; /* Physical drive that holds this volume */ + BYTE ldrv; /* Logical drive number (used only when FF_FS_REENTRANT) */ + BYTE n_fats; /* Number of FATs (1 or 2) */ + BYTE wflag; /* win[] status (b0:dirty) */ + BYTE fsi_flag; /* Allocation information control (b7:disabled, b0:dirty) */ + WORD id; /* Volume mount ID */ + WORD n_rootdir; /* Number of root directory entries (FAT12/16) */ + WORD csize; /* Cluster size [sectors] */ #if FF_MAX_SS != FF_MIN_SS - WORD ssize; /* Sector size (512, 1024, 2048 or 4096) */ + WORD ssize; /* Sector size (512, 1024, 2048 or 4096) */ #endif #if FF_USE_LFN - WCHAR* lfnbuf; /* LFN working buffer */ -#endif -#if FF_FS_EXFAT - BYTE* dirbuf; /* Directory entry block scratch pad buffer for exFAT */ + WCHAR* lfnbuf; /* Pointer to LFN working buffer */ #endif #if !FF_FS_READONLY - DWORD last_clst; /* Last allocated cluster (Unknown if >= n_fatent) */ - DWORD free_clst; /* Number of free clusters (Unknown if >= n_fatent-2) */ + DWORD last_clst; /* Last allocated cluster (invalid if >=n_fatent) */ + DWORD free_clst; /* Number of free clusters (invalid if >=fs->n_fatent-2) */ #endif #if FF_FS_RPATH - DWORD cdir; /* Current directory start cluster (0:root) */ + DWORD cdir; /* Current directory start cluster (0:root) */ +#endif + DWORD n_fatent; /* Number of FAT entries (number of clusters + 2) */ + DWORD fsize; /* Number of sectors per FAT */ + LBA_t winsect; /* Current sector appearing in the win[] */ + LBA_t volbase; /* Volume base sector */ + LBA_t fatbase; /* FAT base sector */ + LBA_t dirbase; /* Root directory base sector (FAT12/16) or cluster (FAT32/exFAT) */ + LBA_t database; /* Data base sector */ #if FF_FS_EXFAT - DWORD cdc_scl; /* Containing directory start cluster (invalid when cdir is 0) */ - DWORD cdc_size; /* b31-b8:Size of containing directory, b7-b0: Chain status */ - DWORD cdc_ofs; /* Offset in the containing directory (invalid when cdir is 0) */ -#endif + LBA_t bitbase; /* Allocation bitmap base sector */ + BYTE* dirbuf; /* Pointer to directory entry block buffer */ +#if FF_FS_RPATH + FFXCWDS xcwds; /* Crrent working directory structure */ + FFXCWDS xcwds2; /* Working buffer to follow the path */ #endif - DWORD n_fatent; /* Number of FAT entries (number of clusters + 2) */ - DWORD fsize; /* Number of sectors per FAT */ - LBA_t volbase; /* Volume base sector */ - LBA_t fatbase; /* FAT base sector */ - LBA_t dirbase; /* Root directory base sector (FAT12/16) or cluster (FAT32/exFAT) */ - LBA_t database; /* Data base sector */ -#if FF_FS_EXFAT - LBA_t bitbase; /* Allocation bitmap base sector */ #endif - LBA_t winsect; /* Current sector appearing in the win[] */ - BYTE win[FF_MAX_SS]; /* Disk access window for Directory, FAT (and file data at tiny cfg) */ + BYTE win[FF_MAX_SS]; /* Disk access window for directory, FAT (and file data in tiny cfg) */ } FATFS; @@ -179,21 +193,21 @@ typedef struct { /* Object ID and allocation information (FFOBJID) */ typedef struct { - FATFS* fs; /* Pointer to the hosting volume of this object */ - WORD id; /* Hosting volume's mount ID */ - BYTE attr; /* Object attribute */ - BYTE stat; /* Object chain status (b1-0: =0:not contiguous, =2:contiguous, =3:fragmented in this session, b2:sub-directory stretched) */ - DWORD sclust; /* Object data start cluster (0:no cluster or root directory) */ - FSIZE_t objsize; /* Object size (valid when sclust != 0) */ + FATFS* fs; /* Pointer to the volume holding this object */ + WORD id; /* Volume mount ID when this object was opened */ + BYTE attr; /* Object attribute */ + BYTE stat; /* Object chain status (exFAT: b1-0: =0:not contiguous, =2:contiguous, =3:fragmented in this session, b2:sub-directory stretched) */ + DWORD sclust; /* Object data cluster (0:no data or root directory) */ + FSIZE_t objsize; /* Object size (valid when sclust != 0) */ #if FF_FS_EXFAT - DWORD n_cont; /* Size of first fragment - 1 (valid when stat == 3) */ - DWORD n_frag; /* Size of last fragment needs to be written to FAT (valid when not zero) */ - DWORD c_scl; /* Containing directory start cluster (valid when sclust != 0) */ - DWORD c_size; /* b31-b8:Size of containing directory, b7-b0: Chain status (valid when c_scl != 0) */ - DWORD c_ofs; /* Offset in the containing directory (valid when file object and sclust != 0) */ + DWORD n_cont; /* Size of first fragment - 1 (valid when stat == 3) */ + DWORD n_frag; /* Size of last fragment needs to be written to FAT (valid when not zero) */ + DWORD c_scl; /* Cluster of directory holding this object (valid when sclust != 0) */ + DWORD c_size; /* Size of directory holding this object (b7-b0: allocation status, valid when c_scl != 0) */ + DWORD c_ofs; /* Offset of entry in the holding directory */ #endif #if FF_FS_LOCK - UINT lockid; /* File lock ID origin from 1 (index of file semaphore table Files[]) */ + UINT lockid; /* File lock ID origin from 1 (index of file semaphore table Files[]) */ #endif } FFOBJID; @@ -202,18 +216,18 @@ typedef struct { /* File object structure (FIL) */ typedef struct { - FFOBJID obj; /* Object identifier (must be the 1st member to detect invalid object pointer) */ - BYTE flag; /* File status flags */ - BYTE err; /* Abort flag (error code) */ - FSIZE_t fptr; /* File read/write pointer (Zeroed on file open) */ - DWORD clust; /* Current cluster of fpter (invalid when fptr is 0) */ - LBA_t sect; /* Sector number appearing in buf[] (0:invalid) */ + FFOBJID obj; /* Object identifier (must be the 1st member to detect invalid object pointer) */ + BYTE flag; /* File status flags */ + BYTE err; /* Abort flag (error code) */ + FSIZE_t fptr; /* File read/write pointer (0 on open) */ + DWORD clust; /* Current cluster of fptr (invalid when fptr is 0) */ + LBA_t sect; /* Sector number appearing in buf[] (0:invalid) */ #if !FF_FS_READONLY - LBA_t dir_sect; /* Sector number containing the directory entry (not used at exFAT) */ - BYTE* dir_ptr; /* Pointer to the directory entry in the win[] (not used at exFAT) */ + LBA_t dir_sect; /* Sector number containing the directory entry (not used in exFAT) */ + BYTE* dir_ptr; /* Pointer to the directory entry in the win[] (not used in exFAT) */ #endif #if FF_USE_FASTSEEK - DWORD* cltbl; /* Pointer to the cluster link map table (nulled on open, set by application) */ + DWORD* cltbl; /* Pointer to the cluster link map table (nulled on open; set by application) */ #endif #if !FF_FS_TINY BYTE buf[FF_MAX_SS]; /* File private data read/write window */ @@ -225,40 +239,44 @@ typedef struct { /* Directory object structure (DIR) */ typedef struct { - FFOBJID obj; /* Object identifier */ - DWORD dptr; /* Current read/write offset */ - DWORD clust; /* Current cluster */ - LBA_t sect; /* Current sector (0:Read operation has terminated) */ - BYTE* dir; /* Pointer to the directory item in the win[] */ - BYTE fn[12]; /* SFN (in/out) {body[8],ext[3],status[1]} */ + FFOBJID obj; /* Object identifier (must be the 1st member to detect invalid object pointer) */ + DWORD dptr; /* Current read/write offset */ + DWORD clust; /* Current cluster */ + LBA_t sect; /* Current sector (0:no more item to read) */ + BYTE* dir; /* Pointer to the directory item in the win[] in filesystem object */ + BYTE fn[12]; /* SFN (in/out) {body[0-7],ext[8-10],status[11]} */ #if FF_USE_LFN - DWORD blk_ofs; /* Offset of current entry block being processed (0xFFFFFFFF:Invalid) */ + DWORD blk_ofs; /* Offset of current entry block being processed (0xFFFFFFFF:invalid) */ #endif #if FF_USE_FIND - const TCHAR* pat; /* Pointer to the name matching pattern */ + const TCHAR *pat; /* Pointer to the name matching pattern */ #endif } DIR; -/* File information structure (FILINFO) */ +/* File/directory information structure (FILINFO) */ typedef struct { - FSIZE_t fsize; /* File size */ - WORD fdate; /* Modified date */ - WORD ftime; /* Modified time */ - BYTE fattrib; /* File attribute */ + FSIZE_t fsize; /* File size (invalid for directory) */ + WORD fdate; /* Date of file modification or directory creation */ + WORD ftime; /* Time of file modification or directory creation */ +#if FF_FS_CRTIME + WORD crdate; /* Date of object createion */ + WORD crtime; /* Time of object createion */ +#endif + BYTE fattrib; /* Object attribute */ #if FF_USE_LFN - TCHAR altname[FF_SFN_BUF + 1];/* Alternative file name */ - TCHAR fname[FF_LFN_BUF + 1]; /* Primary file name */ + TCHAR altname[FF_SFN_BUF + 1];/* Alternative object name */ + TCHAR fname[FF_LFN_BUF + 1]; /* Primary object name */ #else - TCHAR fname[12 + 1]; /* File name */ + TCHAR fname[12 + 1]; /* Object name */ #endif } FILINFO; -/* Format parameter structure (MKFS_PARM) */ +/* Format parameter structure (MKFS_PARM) used for f_mkfs() */ typedef struct { BYTE fmt; /* Format option (FM_FAT, FM_FAT32, FM_EXFAT and FM_SFD) */ @@ -290,7 +308,7 @@ typedef enum { FR_MKFS_ABORTED, /* (14) The f_mkfs function aborted due to some problem */ FR_TIMEOUT, /* (15) Could not take control of the volume within defined period */ FR_LOCKED, /* (16) The operation is rejected according to the file sharing policy */ - FR_NOT_ENOUGH_CORE, /* (17) LFN working buffer could not be allocated or given buffer is insufficient in size */ + FR_NOT_ENOUGH_CORE, /* (17) LFN working buffer could not be allocated, given buffer size is insufficient or too deep path */ FR_TOO_MANY_OPEN_FILES, /* (18) Number of open files > FF_FS_LOCK */ FR_INVALID_PARAMETER /* (19) Given parameter is invalid */ } FRESULT; diff --git a/src/fatfs/ffconf.h b/src/fatfs/ffconf.h index 04f94a074..93829dfb5 100644 --- a/src/fatfs/ffconf.h +++ b/src/fatfs/ffconf.h @@ -5,7 +5,7 @@ / Configurations of FatFs Module /---------------------------------------------------------------------------*/ -#define FFCONF_DEF 5380 /* Revision ID */ +#define FFCONF_DEF 80386 /* Revision ID */ /*---------------------------------------------------------------------------/ / Function Configurations @@ -254,6 +254,13 @@ / These options have no effect in read-only configuration (FF_FS_READONLY = 1). */ +#define FF_FS_CRTIME 1 +/* This option switches support for creation time of files and directories. +/ (0:Disable or 1:Enable) +/ When enabled, crdate and crtime members are added to the FILINFO structure +/ and creation timestamps are managed in addition to modification timestamps. */ + + #define FF_FS_NOFSINFO 0 /* If you need to know correct free space on the FAT32 volume, set bit 0 of this / option, and f_getfree() at the first time after volume mount will force diff --git a/src/flashram.c b/src/flashram.c new file mode 100644 index 000000000..c5813a438 --- /dev/null +++ b/src/flashram.c @@ -0,0 +1,534 @@ +/** + * @file flashram.c + * @brief FlashRAM access functions for N64 cartridges + * + * FlashRAM is a 1 Mibit (128 KiB) Macronix- or Matsushita-family NOR flash used + * as save storage on some N64 cartridges. It shares the PI DOM2 window with + * cartridge SRAM (a cart has one or the other, never both), but is driven + * through a command state machine rather than being flat memory: + * + * - Command register at #FLASHRAM_ADDRESS | 0x10000: 32-bit command words are + * written here as (opcode << 24) | argument, where the argument (for + * erase/program) is a *page number*, not a byte offset. + * - Data window at #FLASHRAM_ADDRESS: reads back the status/ID latch or the + * array, depending on the current mode -- a CPU read returns the latch in + * status/identify mode and array data in read-array mode. This driver moves + * array data with DMA throughout, which uniformly handles the word-index + * address halving and the 2-byte transfer unit. + * + * Two families of chip exist and differ in how they address their array: + * "byte-indexed" parts address individual bytes (like SRAM), while "older" + * word-indexed parts (e.g. MX29L1100) address 16-bit words, so a logical byte + * offset must be halved to reach the right word. The addressing mode is a fixed + * property of the silicon; flashram_init() looks it up from the silicon ID + * and caches it, and the read path adapts accordingly. Because PGS cannot + * express the word-indexed "divide by 2", we set PGS to its maximum (never + * auto-split) and split every DMA manually at the 256-page boundary. + */ + +#include "flashram.h" +#include "flashram_internal.h" +#include "debug.h" +#include "dma.h" +#include "interrupt.h" +#include "n64sys.h" +#include +#include + +/// @cond +#define PI_BSD_DOM2_LAT ((volatile uint32_t*) 0xA4600024) +#define PI_BSD_DOM2_PWD ((volatile uint32_t*) 0xA4600028) +#define PI_BSD_DOM2_PGS ((volatile uint32_t*) 0xA460002C) +#define PI_BSD_DOM2_RLS ((volatile uint32_t*) 0xA4600030) + +/// Command register address (offset 0x10000 into the FlashRAM window). +#define FLASHRAM_COMMAND_ADDRESS (FLASHRAM_ADDRESS | 0x00010000) + +/// FLASH_TYPE_ID word (silicon ID bits [63:32]) expected on all known FlashRAM parts. +#define FLASHRAM_IDENTIFIER 0x11118001 + +// Command opcodes (written as (opcode) or (opcode | page_number)). +#define FLASHRAM_CMD_STATUS_MODE 0xD2000000 ///< Enter status mode. +#define FLASHRAM_CMD_SILICON_ID_MODE 0xE1000000 ///< Enter SiliconID mode. +#define FLASHRAM_CMD_READ_MODE 0xF0000000 ///< Enter array read mode. +#define FLASHRAM_CMD_SECTOR_ERASE 0x4B000000 ///< Arm sector erase at page (| page). +#define FLASHRAM_CMD_CHIP_ERASE 0x3C000000 ///< Arm whole-chip erase. +#define FLASHRAM_CMD_EXECUTE_ERASE 0x78000000 ///< Execute the armed (sector or chip) erase. +#define FLASHRAM_CMD_LOAD_BYTE_PAGE 0xB4000000 ///< Enter load-byte-page mode (fill the page buffer). +#define FLASHRAM_CMD_PROGRAM_PAGE 0xA5000000 ///< Program the page buffer into page (| page). + +// Status register bits (the register is 8-bit; the window returns 00 ). +#define FLASHRAM_STATUS_PROGRAM_BUSY 0x01 ///< A page program is in progress. +#define FLASHRAM_STATUS_ERASE_BUSY 0x02 ///< A sector/chip erase is in progress. +#define FLASHRAM_STATUS_PROGRAM_OK 0x04 ///< The last page program succeeded. +#define FLASHRAM_STATUS_ERASE_OK 0x08 ///< The last erase succeeded. + +// Blocking poll timeouts (milliseconds). +#define FLASHRAM_PROGRAM_TIMEOUT_MS 1000 +#define FLASHRAM_ERASE_TIMEOUT_MS 3000 + +/// Largest page the fixed-size program buffer can hold. All known parts use +/// 128-byte pages; a detected layout with a larger page is not supported. +#define FLASHRAM_MAX_PAGE_SIZE 128 + +/// @endcond + +// Canonical layouts for the two known 1 Mibit geometries (128-byte pages, +// 128 pages/sector, 8 sectors, 256-page read boundary), differing only in the +// addressable unit: byte-indexed vs 16-bit-word-indexed. + +/// Byte-indexed 1 Mibit layout (newer MX29L1101 / MN63F8MPN parts). +#define FLASHRAM_LAYOUT_BYTE \ + { .unit_bits = 0, .offset_bits = 7, .page_bits = 7, .sector_bits = 3, .read_page_bits = 8 } +/// Word-indexed 1 Mibit layout (older MX29L0000 / MX29L1100 parts). +#define FLASHRAM_LAYOUT_WORD \ + { .unit_bits = 1, .offset_bits = 6, .page_bits = 7, .sector_bits = 3, .read_page_bits = 8 } + +/// One row of the silicon-ID lookup table. +typedef struct +{ + uint16_t manufacturer_id; + uint16_t device_id; + const char* name; + flashram_layout_t layout; +} flashram_model_t; + +/// Known FlashRAM parts. The layout (in particular byte- vs word-addressing) is +/// a fixed property we cannot probe at runtime, so it must be looked up by +/// manufacturer/device ID. +static const flashram_model_t FLASHRAM_MODELS[] = { + { 0x00C2, 0x0000, "MX29L0000", FLASHRAM_LAYOUT_WORD }, // Macronix + { 0x00C2, 0x0001, "MX29L0001", FLASHRAM_LAYOUT_WORD }, + { 0x00C2, 0x001E, "MX29L1100", FLASHRAM_LAYOUT_WORD }, + { 0x00C2, 0x001D, "MX29L1101_A", FLASHRAM_LAYOUT_BYTE }, + { 0x00C2, 0x0084, "MX29L1101_B", FLASHRAM_LAYOUT_BYTE }, + { 0x00C2, 0x008E, "MX29L1101_C", FLASHRAM_LAYOUT_BYTE }, + { 0x0032, 0x00F1, "MN63F8MPN", FLASHRAM_LAYOUT_BYTE }, // Matsushita +}; + +/// Fill the byte-size geometry fields of @p info from its bit-encoded layout. +static void flashram_derive_geometry(flashram_info_t* info) +{ + const flashram_layout_t* l = &info->layout; + info->page_size = (size_t) 1 << (l->unit_bits + l->offset_bits); + info->sector_size = (size_t) 1 << (l->unit_bits + l->offset_bits + l->page_bits); + info->total_size = (size_t) 1 << (l->unit_bits + l->offset_bits + l->page_bits + l->sector_bits); + info->num_sectors = 1u << l->sector_bits; + info->num_pages = 1u << (l->page_bits + l->sector_bits); + + // Sector-erase/page-program commands OR the page number into a 32-bit opcode + // word whose opcode occupies bits [31:24], so a page number must fit in the + // low 24 bits. No real part comes close, but a bad table entry would silently + // corrupt the opcode -- assert the invariant where the geometry is adopted. + assertf(info->num_pages <= (1u << 24), + "flashram: layout has %u pages; a page number would overwrite the " + "command opcode (max 2^24 pages)", info->num_pages); +} + +/// True if flashram_init() has been called. +static bool __flashram_inited = false; + +/// True once flashram_init() has probed and found a FlashRAM chip present. +static bool __flashram_present = false; + +/// Assert that flashram_init() ran and found a chip -- otherwise the cached +/// layout is only a default guess and reads/writes cannot address the array. +#define flashram_assert_ready() do { \ + assertf(__flashram_inited, "flashram accessed before flashram_init() was called"); \ + assertf(__flashram_present, "flashram accessed, but no FlashRAM chip was detected"); \ +} while (0) + +/// Cached identity/layout of the detected chip, fully populated by the probe in +/// flashram_init(). It is only ever read after a successful probe (every +/// read/write asserts a chip was detected), so it needs no meaningful default. +static flashram_info_t __flashram_info; + +/// Logical-byte span a single DMA must not cross, derived from the current +/// layout's read boundary. For every known part this is 0x8000 logical bytes. +static inline size_t flashram_dma_boundary(void) +{ + const flashram_layout_t* l = &__flashram_info.layout; + return (size_t) 1 << (l->unit_bits + l->offset_bits + l->read_page_bits); +} + +/// Number of program pages per erase sector for the detected layout. +static inline unsigned int flashram_pages_per_sector(void) +{ + return __flashram_info.num_pages / __flashram_info.num_sectors; +} + +/// Write a single command word to the FlashRAM command register. +static inline void flashram_command(uint32_t command) +{ + io_write(FLASHRAM_COMMAND_ADDRESS, command); +} + +/// Write a command word twice. Some parts (notably MX29L1100) need two CIR +/// writes to actually switch to status or silicon-ID mode; a single write can +/// leave the previous mode latched. Read-array/erase/program need only one. +static inline void flashram_command_twice(uint32_t command) +{ + io_write(FLASHRAM_COMMAND_ADDRESS, command); + io_write(FLASHRAM_COMMAND_ADDRESS, command); +} + +/// PI-bus address of logical byte @p offset for the detected addressing mode. +/// unit_bits is the log2 of the unit size, so it is exactly the shift that maps +/// a byte offset onto a unit (word) address: 0 for byte-indexed, 1 for word. +static inline uint32_t flashram_pi_address(size_t offset) +{ + return FLASHRAM_ADDRESS + (uint32_t) (offset >> __flashram_info.layout.unit_bits); +} + +uint8_t flashram_status(void) +{ + flashram_assert_ready(); + flashram_command_twice(FLASHRAM_CMD_STATUS_MODE); + // The window returns the pattern 00 ; the upper bits are meaningless + // (and on MX29L1100 hold leftovers from the previous mode), so keep the low byte. + return (uint8_t) io_read(FLASHRAM_ADDRESS); +} + +void flashram_clear_status(void) +{ + flashram_assert_ready(); + flashram_command_twice(FLASHRAM_CMD_STATUS_MODE); + // Reset the ERASE_OK / PROGRAM_OK latch by writing 0 at the array origin. + io_write(FLASHRAM_ADDRESS, 0); +} + +/// Poll the status byte until @p busy_mask clears; returns whether @p ok_mask is then set. +static bool flashram_wait_ready(uint8_t busy_mask, uint8_t ok_mask, uint32_t timeout_ms) +{ + // The erase/program that preceded this call left the chip in status mode + // automatically, so read the status byte directly. Re-issuing the status + // command each poll is unnecessary and can clear the OK latch on some parts. + uint64_t start_ms = get_ticks_ms(); + while (true) + { + uint8_t status = (uint8_t) io_read(FLASHRAM_ADDRESS); + if ((status & busy_mask) == 0) + { + return (status & ok_mask) == ok_mask; + } + if ((get_ticks_ms() - start_ms) > timeout_ms) + { + return false; + } + wait_ms(1); + } +} + +/// Standard PI DOM2 bus timings for FlashRAM (https://n64brew.dev/wiki/Flash). +/// PGS is left at its maximum so the PI never auto-splits DMAs -- the driver +/// splits them manually, which is required for word-indexed parts. +static const pi_dom_timings_t FLASHRAM_TIMINGS_DEFAULT = { + .latency = 0x05, + .pulse_width = 0x0C, + .page_size = 0x0F, + .release = 0x02, +}; + +/// Read the silicon ID, identify the chip, and cache its layout/geometry. +/// Returns whether a known FlashRAM identifier was found. Leaves read mode. +static bool flashram_probe(void) +{ + // Enter SiliconID mode and DMA out the two silicon-ID words. DMA is mandatory + // here: a CPU read of the ID window returns only the first 32-bit word twice. + uint32_t id[2] __attribute__((aligned(16))) = {0}; + flashram_command_twice(FLASHRAM_CMD_SILICON_ID_MODE); + data_cache_hit_writeback_invalidate(id, sizeof(id)); + dma_read_raw_async(id, FLASHRAM_ADDRESS, sizeof(id)); + dma_wait(); + + // Restore read mode so a later CPU/DMA read returns array data. + flashram_command(FLASHRAM_CMD_READ_MODE); + + // SILICON_ID: [63:32] FLASH_TYPE_ID, [31:16] MANUFACTURER_ID, [15:0] DEVICE_ID. + uint32_t type_id = id[0]; + if (type_id != FLASHRAM_IDENTIFIER) + return false; + + uint16_t manufacturer_id = (uint16_t) (id[1] >> 16); + uint16_t device_id = (uint16_t) (id[1] & 0xFFFF); + + // Look up the model to learn its layout. Default to a byte-indexed 1 Mibit + // part (the common flashcart/emulator convention) for parts not in the table. + const char* name = "unknown"; + flashram_layout_t layout = (flashram_layout_t) FLASHRAM_LAYOUT_BYTE; + for (size_t i = 0; i < sizeof(FLASHRAM_MODELS) / sizeof(FLASHRAM_MODELS[0]); i++) + { + if (FLASHRAM_MODELS[i].manufacturer_id == manufacturer_id && + FLASHRAM_MODELS[i].device_id == device_id) + { + name = FLASHRAM_MODELS[i].name; + layout = FLASHRAM_MODELS[i].layout; + break; + } + } + + __flashram_info.type_id = type_id; + __flashram_info.manufacturer_id = manufacturer_id; + __flashram_info.device_id = device_id; + __flashram_info.layout = layout; + __flashram_info.name = name; + flashram_derive_geometry(&__flashram_info); + return true; +} + +bool flashram_init(const pi_dom_timings_t* timings, flashram_info_t* info) +{ + if (!__flashram_inited) + { + if (timings == NULL) + timings = &FLASHRAM_TIMINGS_DEFAULT; + + // Configure PI DOM2 registers to enable access to FlashRAM. + disable_interrupts(); + *PI_BSD_DOM2_LAT = timings->latency; + *PI_BSD_DOM2_PWD = timings->pulse_width; + *PI_BSD_DOM2_PGS = timings->page_size; + *PI_BSD_DOM2_RLS = timings->release; + enable_interrupts(); + + __flashram_inited = true; + + // Probe the chip so its layout is cached before any read/write. Leaves + // the chip in read mode whether or not a chip was found. + __flashram_present = flashram_probe(); + } + + if (info && __flashram_present) + *info = __flashram_info; + return __flashram_present; +} + +int flashram_read(void* dst, size_t offset, size_t len) +{ + flashram_assert_ready(); + + size_t total_size = __flashram_info.total_size; + assertf(offset + len <= total_size, + "flashram_read out of range: offset=0x%X len=0x%X (size=0x%X)", + (unsigned) offset, (unsigned) len, (unsigned) total_size); + if (len == 0) + return 0; + // PI DMA moves the array over the 2-byte bus, so the offset must be even. + assertf((offset & 1) == 0, "flashram_read requires an even offset, got 0x%X", (unsigned) offset); + + flashram_command(FLASHRAM_CMD_READ_MODE); + + uint8_t bounce[512] __attribute__((aligned(16))); + uint8_t* out = (uint8_t*) dst; + size_t done = 0; + size_t boundary = flashram_dma_boundary(); // 0x8000 logical bytes for known parts + + while (done < len) + { + size_t abs_off = offset + done; + // Clamp this DMA so it neither exceeds the request nor crosses the + // 256-page (0x8000 logical byte) boundary that a single DMA cannot span. + size_t to_boundary = boundary - (abs_off & (boundary - 1)); + size_t chunk = len - done; + if (chunk > to_boundary) + chunk = to_boundary; + + uint32_t pi = flashram_pi_address(abs_off); + + // Fast path: cacheline-aligned destination and whole-cacheline length + // -> DMA straight in. Must be 16-byte (not just 8-byte/DMA) granular: + // the writeback-invalidate below operates on whole 16-byte D-cache + // lines, so a merely 8-aligned edge would extend the op into the + // adjacent allocation's shared line — flushing stale CPU bytes over + // (or discarding cached state of) up to 8 neighboring bytes. Anything + // less aligned takes the bounce path below, which is edge-safe. + if ((((uintptr_t) (out + done) & 15) == 0) && ((chunk & 15) == 0)) + { + data_cache_hit_writeback_invalidate(out + done, chunk); + dma_read_raw_async(out + done, pi, chunk); + dma_wait(); + done += chunk; + continue; + } + + // Slow path (unaligned dst or odd length): DMA into an aligned bounce, + // then copy out. We keep the whole array path on DMA -- a CPU read would + // return a big-endian 32-bit word needing manual byte assembly here. + if (chunk > sizeof(bounce)) + chunk = sizeof(bounce); + size_t dma_len = (chunk + 1) & ~(size_t) 1; // round up to the 2-byte DMA unit + if (abs_off + dma_len > total_size) + dma_len = total_size - abs_off; + + data_cache_hit_writeback_invalidate(bounce, dma_len); + dma_read_raw_async(bounce, pi, dma_len); + dma_wait(); + + memcpy(out + done, bounce, chunk); + done += chunk; + } + return (int) len; +} + +/// True if all @p page_size bytes at @p page are 0xFF (already erased). +static bool flashram_page_is_erased(const uint8_t* page, size_t page_size) +{ + for (size_t i = 0; i < page_size; i++) + { + if (page[i] != 0xFF) + return false; + } + return true; +} + +bool flashram_erase_sector_at_page(unsigned int page) +{ + flashram_assert_ready(); + + assertf(page < __flashram_info.num_pages, + "flashram_erase_sector_at_page: page %u out of range (0..%u)", + page, __flashram_info.num_pages - 1); + + // The sector-erase command is addressed by page number; the chip derives the + // enclosing sector from it. + flashram_command(FLASHRAM_CMD_SECTOR_ERASE | page); + flashram_command(FLASHRAM_CMD_EXECUTE_ERASE); + bool ok = flashram_wait_ready(FLASHRAM_STATUS_ERASE_BUSY, FLASHRAM_STATUS_ERASE_OK, + FLASHRAM_ERASE_TIMEOUT_MS); + // Clear the OK/error latch so the next operation's result is not masked. + flashram_clear_status(); + return ok; +} + +bool flashram_erase_chip(void) +{ + flashram_assert_ready(); + + flashram_command(FLASHRAM_CMD_CHIP_ERASE); + flashram_command(FLASHRAM_CMD_EXECUTE_ERASE); + bool ok = flashram_wait_ready(FLASHRAM_STATUS_ERASE_BUSY, FLASHRAM_STATUS_ERASE_OK, + FLASHRAM_ERASE_TIMEOUT_MS); + // Clear the OK/error latch so the next operation's result is not masked. + flashram_clear_status(); + return ok; +} + +bool flashram_program_page(unsigned int page, const void* data) +{ + flashram_assert_ready(); + + assertf(page < __flashram_info.num_pages, + "flashram_program_page: page %u out of range (0..%u)", + page, __flashram_info.num_pages - 1); + + // Copy into an aligned bounce so callers can pass any alignment. The bounce + // is sized to the standard page (128 B), which caps the layouts we support. + size_t page_size = __flashram_info.page_size; + uint8_t buffer[FLASHRAM_MAX_PAGE_SIZE] __attribute__((aligned(16))); + assertf(page_size <= sizeof(buffer), + "flashram_program_page: page size 0x%X exceeds the buffer", (unsigned) page_size); + memcpy(buffer, data, page_size); + + // Load the page buffer, then program it into the target page. + flashram_command(FLASHRAM_CMD_LOAD_BYTE_PAGE); + data_cache_hit_writeback(buffer, page_size); + dma_write_raw_async(buffer, FLASHRAM_ADDRESS, page_size); + dma_wait(); + flashram_command(FLASHRAM_CMD_PROGRAM_PAGE | page); + bool ok = flashram_wait_ready(FLASHRAM_STATUS_PROGRAM_BUSY, FLASHRAM_STATUS_PROGRAM_OK, + FLASHRAM_PROGRAM_TIMEOUT_MS); + // Clear the OK/error latch so the next operation's result is not masked. + flashram_clear_status(); + return ok; +} + +/// Erase the sector starting at @p base_page, then program its pages from the +/// one-sector @p content buffer. Works in page numbers throughout. +static bool flashram_write_sector(unsigned int base_page, const uint8_t* content) +{ + if (!flashram_erase_sector_at_page(base_page)) + return false; + + size_t page_size = __flashram_info.page_size; + unsigned int pages_per_sector = flashram_pages_per_sector(); + for (unsigned int i = 0; i < pages_per_sector; i++) + { + const uint8_t* page = content + (i * page_size); + // Pages that end up fully erased need no programming; the erase set them to 0xFF. + if (flashram_page_is_erased(page, page_size)) + continue; + if (!flashram_program_page(base_page + i, page)) + return false; + } + return true; +} + +int flashram_write(const void* src, size_t offset, size_t len) +{ + flashram_assert_ready(); + + size_t total_size = __flashram_info.total_size; + size_t sector_size = __flashram_info.sector_size; + unsigned int pages_per_sector = flashram_pages_per_sector(); + assertf(offset + len <= total_size, + "flashram_write out of range: offset=0x%X len=0x%X (size=0x%X)", + (unsigned) offset, (unsigned) len, (unsigned) total_size); + if (len == 0) + return 0; + + const uint8_t* in = (const uint8_t*) src; + size_t pos = offset; + size_t remaining = len; + uint8_t* sector_buf = NULL; // one-sector scratch, allocated lazily for partial sectors only + int result = (int) len; + + while (remaining > 0) + { + unsigned int sector = pos / sector_size; + unsigned int base_page = sector * pages_per_sector; + size_t sector_start = (size_t) sector * sector_size; + size_t in_sector = pos - sector_start; + size_t n = sector_size - in_sector; + if (n > remaining) + n = remaining; + + if (in_sector == 0 && n == sector_size) + { + // Whole sector overwritten: erase and program straight from the source. + if (!flashram_write_sector(base_page, in)) + { + result = -1; + break; + } + } + else + { + // Partial sector: read-modify-write to preserve untouched bytes. + if (sector_buf == NULL) + { + sector_buf = memalign(16, sector_size); + assertf(sector_buf != NULL, + "flashram_write: out of memory for the 0x%X-byte sector buffer", + (unsigned) sector_size); + } + flashram_read(sector_buf, sector_start, sector_size); + memcpy(sector_buf + in_sector, in, n); + if (!flashram_write_sector(base_page, sector_buf)) + { + result = -1; + break; + } + } + + in += n; + pos += n; + remaining -= n; + } + + if (sector_buf != NULL) + free(sector_buf); + + // Return the chip to read mode for subsequent reads. + flashram_command(FLASHRAM_CMD_READ_MODE); + return result; +} diff --git a/src/flashram_internal.h b/src/flashram_internal.h new file mode 100644 index 000000000..d2c016853 --- /dev/null +++ b/src/flashram_internal.h @@ -0,0 +1,72 @@ +/** + * @file flashram_internal.h + * @brief Internal low-level FlashRAM page/sector interface + * @ingroup flashram + * + * These operations map directly onto the FlashRAM command state machine. They + * are kept internal for now: the public API (flashram.h) exposes only the + * SRAM-like byte-range interface (#flashram_read / #flashram_write). + */ + +#ifndef LIBDRAGON_FLASHRAM_INTERNAL_H +#define LIBDRAGON_FLASHRAM_INTERNAL_H + +#include "flashram.h" +#include +#include + +/** + * @brief Read the 8-bit FlashRAM status register. + * + * Enters status mode and returns the status byte. The low bits report + * program/erase busy and last-operation success. + * + * @return The 8-bit status byte. + */ +uint8_t flashram_status(void); + +/** + * @brief Clear the FlashRAM status register. + * + * Resets the ERASE_OK / PROGRAM_OK latch by writing 0 at the array origin while + * in status mode. Should be called after a failed erase or program so a later + * operation's success flag is not masked by the previous result. + */ +void flashram_clear_status(void); + +/** + * @brief Erase the 16 KiB sector that contains a page. + * + * The hardware sector-erase command is addressed by page number; the chip + * derives the enclosing sector from it. Sets every byte of that sector to 0xFF. + * Blocking. + * + * @param page Any page number within the sector to erase (0 to num_pages - 1). + * @return true on success, false on erase timeout/failure. An out-of-range page asserts. + */ +bool flashram_erase_sector_at_page(unsigned int page); + +/** + * @brief Erase the entire chip. + * + * Sets every byte of every sector to 0xFF in a single operation. Blocking. + * Currently unused by the high-level driver (which erases per sector) but kept + * for completeness of the low-level protocol. + * + * @return true on success, false on erase timeout/failure. + */ +bool flashram_erase_chip(void); + +/** + * @brief Program a single 128-byte page. + * + * The page's sector must have been erased first; programming can only clear + * bits (1 -> 0). Blocking. + * + * @param page Page number (0 to num_pages - 1). + * @param data Pointer to one page (page_size bytes) of data (any alignment). + * @return true on success, false on program timeout/failure. An out-of-range page asserts. + */ +bool flashram_program_page(unsigned int page, const void* data); + +#endif diff --git a/src/graphics.c b/src/graphics.c index 1c0df95ce..386b1bcc2 100644 --- a/src/graphics.c +++ b/src/graphics.c @@ -1,6 +1,7 @@ /** * @file graphics.c * @author Jennifer Taylor + * @author Giovanni Bajo * @brief 2D Graphics * @ingroup graphics */ @@ -8,7 +9,8 @@ #include #include #include -#include "display.h" +#include "vi.h" +#include "n64sys.h" #include "graphics.h" #include "sprite.h" #include "font.h" @@ -98,7 +100,7 @@ uint32_t graphics_make_color( int r, int g, int b, int a ) uint32_t graphics_convert_color( color_t color ) { - if( display_get_bitdepth() == 2 ) + if( vi_get_bpp() == 16 ) { // Pack twice for compatibility with RDP packed colors and the old deprecated RDP API. uint32_t conv = color_to_packed16(color); @@ -143,6 +145,76 @@ static int __is_transparent( int bitdepth, uint32_t color ) return 0; } +/** + * @brief Clip a source rectangle blitted at (x,y) to the surface bounds. + * + * @param[in] disp Destination surface + * @param[in] x Destination X coordinate of the source origin (sx, sy) + * @param[in] y Destination Y coordinate of the source origin (sx, sy) + * @param[in,out] sx Source X start (inclusive), advanced if clipped on the left + * @param[in,out] sy Source Y start (inclusive), advanced if clipped on the top + * @param[in,out] ex Source X end (exclusive), reduced if clipped on the right + * @param[in,out] ey Source Y end (exclusive), reduced if clipped on the bottom + * @param[out] tx Destination X offset such that dest = (tx + xp, ty + yp) + * @param[out] ty Destination Y offset such that dest = (tx + xp, ty + yp) + * + * @retval 1 if any portion of the blit is visible + * @retval 0 if the blit is fully outside the surface + */ +static int __clip_blit( surface_t *disp, int x, int y, int *sx, int *sy, int *ex, int *ey, int *tx, int *ty ) +{ + *tx = x - *sx; + *ty = y - *sy; + + if( (*tx + *ex) <= 0 ) { return 0; } + if( (*ty + *ey) <= 0 ) { return 0; } + if( *tx >= (int)disp->width ) { return 0; } + if( *ty >= (int)disp->height ) { return 0; } + + if( x < 0 ) { *sx += -x; } + if( y < 0 ) { *sy += -y; } + if( (*tx + *ex) >= (int)disp->width ) { *ex = disp->width - *tx; } + if( (*ty + *ey) >= (int)disp->height ) { *ey = disp->height - *ty; } + + return 1; +} + +/** + * @brief Clip a filled rectangle to the surface bounds. + * + * @param[in] disp Destination surface + * @param[in] x Top-left X coordinate + * @param[in] y Top-left Y coordinate + * @param[in] width Rectangle width in pixels + * @param[in] height Rectangle height in pixels + * @param[out] x0 Clipped top-left X (inclusive) + * @param[out] y0 Clipped top-left Y (inclusive) + * @param[out] x1 Clipped bottom-right X (exclusive) + * @param[out] y1 Clipped bottom-right Y (exclusive) + * + * @retval 1 if any portion of the rectangle is visible + * @retval 0 if the rectangle is fully outside the surface + */ +static int __clip_box( surface_t *disp, int x, int y, int width, int height, int *x0, int *y0, int *x1, int *y1 ) +{ + if( width <= 0 || height <= 0 ) { return 0; } + + *x0 = x; + *y0 = y; + *x1 = x + width; + *y1 = y + height; + + if( *x1 <= 0 || *y1 <= 0 ) { return 0; } + if( *x0 >= (int)disp->width || *y0 >= (int)disp->height ) { return 0; } + + if( *x0 < 0 ) { *x0 = 0; } + if( *y0 < 0 ) { *y0 = 0; } + if( *x1 > (int)disp->width ) { *x1 = disp->width; } + if( *y1 > (int)disp->height ) { *y1 = disp->height; } + + return 1; +} + void graphics_draw_pixel( surface_t* disp, int x, int y, uint32_t color ) { if( disp == 0 ) { return; } @@ -328,28 +400,42 @@ void graphics_draw_box( surface_t* disp, int x, int y, int width, int height, ui { if( disp == 0 ) { return; } - int pix_stride = TEX_FORMAT_BYTES2PIX(surface_get_format(disp), disp->stride); - if( TEX_FORMAT_BITDEPTH(surface_get_format( disp )) == 16 ) - { - uint16_t *buffer16 = (uint16_t *)__get_buffer( disp ); + int x0, y0, x1, y1; + if( !__clip_box( disp, x, y, width, height, &x0, &y0, &x1, &y1 ) ) { return; } - for(int j = y; j < y + height; j++) + tex_format_t format = surface_get_format(disp); + size_t row_bytes = TEX_FORMAT_PIX2BYTES(format, x1 - x0); + size_t box_bytes = row_bytes * (size_t)(y1 - y0); + uint8_t *row_ptr = (uint8_t *)__get_buffer(disp) + y0 * disp->stride + TEX_FORMAT_PIX2BYTES(format, x0); + + if( TEX_FORMAT_BITDEPTH(format) == 16 ) + { + uint16_t color16 = (uint16_t)color; + if( row_bytes == disp->stride ) { - for(int i = x; i < x + width; i++) + sys_hw_memset16(row_ptr, color16, box_bytes); + } + else + { + for(int j = y0; j < y1; j++) { - __set_pixel( buffer16, i, j, color ); + sys_hw_memset16(row_ptr, color16, row_bytes); + row_ptr += disp->stride; } } } else { - uint32_t *buffer32 = (uint32_t *)__get_buffer( disp ); - - for(int j = y; j < y + height; j++) + if( row_bytes == disp->stride ) + { + sys_hw_memset32(row_ptr, color, box_bytes); + } + else { - for(int i = x; i < x + width; i++) + for(int j = y0; j < y1; j++) { - __set_pixel( buffer32, i, j, color ); + sys_hw_memset32(row_ptr, color, row_bytes); + row_ptr += disp->stride; } } } @@ -359,14 +445,17 @@ void graphics_draw_box_trans( surface_t* disp, int x, int y, int width, int heig { if( disp == 0 ) { return; } + int x0, y0, x1, y1; + if( !__clip_box( disp, x, y, width, height, &x0, &y0, &x1, &y1 ) ) { return; } + int pix_stride = TEX_FORMAT_BYTES2PIX(surface_get_format(disp), disp->stride); if( TEX_FORMAT_BITDEPTH(surface_get_format( disp )) == 16 ) { uint16_t *buffer16 = (uint16_t *)__get_buffer( disp ); - for(int j = y; j < y + height; j++) + for(int j = y0; j < y1; j++) { - for(int i = x; i < x + width; i++) + for(int i = x0; i < x1; i++) { /* Only display the pixel if alpha bit is set */ if( !__is_transparent( 2, color ) ) @@ -380,9 +469,9 @@ void graphics_draw_box_trans( surface_t* disp, int x, int y, int width, int heig { uint32_t *buffer32 = (uint32_t *)__get_buffer( disp ); - for(int j = y; j < y + height; j++) + for(int j = y0; j < y1; j++) { - for(int i = x; i < x + width; i++) + for(int i = x0; i < x1; i++) { /* Get 32bit representations */ uint32_t cur_color = __get_pixel( buffer32, i, j ); @@ -422,12 +511,14 @@ void graphics_fill_screen( surface_t* disp, uint32_t c ) { if( disp == 0 ) { return; } - int len = TEX_FORMAT_PIX2BYTES(surface_get_format(disp), disp->width * disp->height) / 8; - - uint64_t c64 = ((uint64_t)c << 32) | c; - uint64_t *buffer = (uint64_t *)__get_buffer(disp); - for( int i = 0; i < len; i++ ) - buffer[i] = c64; + int width_bytes = TEX_FORMAT_PIX2BYTES(surface_get_format(disp), disp->width); + if (width_bytes == disp->stride) { + sys_hw_memset32(disp->buffer, c, width_bytes * disp->height); + } else { + for (int y = 0; y < disp->height; y++) { + sys_hw_memset32(disp->buffer + y * disp->stride, c, width_bytes); + } + } } void graphics_set_default_font( void ) @@ -449,7 +540,7 @@ void graphics_draw_character( surface_t* disp, int x, int y, char ch ) if( disp == 0 ) { return; } int pix_stride = TEX_FORMAT_BYTES2PIX(surface_get_format(disp), disp->stride); - int depth = display_get_bitdepth(); + int depth = TEX_FORMAT_BITDEPTH(surface_get_format(disp)) >> 3; // resetting to default font if bit depth has been changed if( sprite_font.sprite != NULL && depth*8 != TEX_FORMAT_BITDEPTH(sprite_get_format(sprite_font.sprite)) ) @@ -463,13 +554,13 @@ void graphics_draw_character( surface_t* disp, int x, int y, char ch ) if ( sprite_font.sprite != NULL ) { // Use custom font - const int sx = ( ch % sprite_font.sprite->hslices ) * sprite_font.font_width; - const int sy = ( ch / sprite_font.sprite->hslices ) * sprite_font.font_height; - const int ex = sx + sprite_font.font_width; - const int ey = sy + sprite_font.font_height; + int sx = ( ch % sprite_font.sprite->hslices ) * sprite_font.font_width; + int sy = ( ch / sprite_font.sprite->hslices ) * sprite_font.font_height; + int ex = sx + sprite_font.font_width; + int ey = sy + sprite_font.font_height; + int tx, ty; - const int tx = x - sx; - const int ty = y - sy; + if( !__clip_blit( disp, x, y, &sx, &sy, &ex, &ey, &tx, &ty ) ) { return; } if( depth == 2 ) { @@ -531,34 +622,38 @@ void graphics_draw_character( surface_t* disp, int x, int y, char ch ) } } else { // Use 1bpp default font - int trans = __is_transparent( depth, b_color ); + int sx = 0; + int sy = 0; + int ex = 8; + int ey = 8; + int tx, ty; + + if( !__clip_blit( disp, x, y, &sx, &sy, &ex, &ey, &tx, &ty ) ) { return; } if( depth == 2 ) { uint16_t *buffer = (uint16_t *)__get_buffer( disp ); - for( int row = 0; row < 8; row++ ) + for( int yp = sy; yp < ey; yp++ ) { - unsigned char c = __font_data[(ch * 8) + row]; + unsigned char c = __font_data[(ch * 8) + yp]; - for( int col = 0; col < 8; col++ ) + for( int xp = sx; xp < ex; xp++ ) { - int active = (c & 0x80) != 0; + int active = (c & (0x80 >> xp)) != 0; if( trans ) { if( active ) { /* Only draw it if it is active */ - __set_pixel( buffer, x + col, y + row, f_color ); + __set_pixel( buffer, tx + xp, ty + yp, f_color ); } } else { /* Display foreground or background depending on font data */ - __set_pixel( buffer, x + col, y + row, active ? f_color : b_color ); + __set_pixel( buffer, tx + xp, ty + yp, active ? f_color : b_color ); } - - c <<= 1; } } } @@ -566,28 +661,26 @@ void graphics_draw_character( surface_t* disp, int x, int y, char ch ) { uint32_t *buffer = (uint32_t *)__get_buffer( disp ); - for( int row = 0; row < 8; row++ ) + for( int yp = sy; yp < ey; yp++ ) { - unsigned char c = __font_data[(ch * 8) + row]; + unsigned char c = __font_data[(ch * 8) + yp]; - for( int col = 0; col < 8; col++ ) + for( int xp = sx; xp < ex; xp++ ) { - int active = (c & 0x80) != 0; + int active = (c & (0x80 >> xp)) != 0; if( trans ) { if( active ) { /* Only draw it if it is active */ - __set_pixel( buffer, x + col, y + row, f_color ); + __set_pixel( buffer, tx + xp, ty + yp, f_color ); } } else { /* Display foreground or background depending on font data */ - __set_pixel( buffer, x + col, y + row, active ? f_color : b_color ); + __set_pixel( buffer, tx + xp, ty + yp, active ? f_color : b_color ); } - - c <<= 1; } } } @@ -642,8 +735,7 @@ void graphics_draw_sprite_stride( surface_t* disp, int x, int y, sprite_t *sprit __sprite_upgrade(sprite); /* For spritemaps */ - int tx = x; - int ty = y; + int tx, ty; /* Calculate the location size of the actual sprite we will be blitting */ int sx, sy, ex, ey; @@ -658,9 +750,6 @@ void graphics_draw_sprite_stride( surface_t* disp, int x, int y, sprite_t *sprit sy = (offset / sprite->hslices) * theight; ex = sx + twidth; ey = sy + theight; - - tx -= sx; - ty -= sy; } else { @@ -671,41 +760,7 @@ void graphics_draw_sprite_stride( surface_t* disp, int x, int y, sprite_t *sprit ey = sprite->height; } - /* Too far left */ - if( (tx + ex) <= 0 ) { return; } - - /* Too far up */ - if( (ty + ey) <= 0 ) { return; } - - /* Too far right */ - if( tx >= (int)disp->width ) { return; } - - /* Too far down */ - if( ty >= (int)disp->height ) { return; } - - /* Clipping left */ - if( x < 0 ) - { - sx += (x * -1); - } - - /* Clipping top */ - if( y < 0 ) - { - sy += (y * -1); - } - - /* Clipping right */ - if( (tx + ex) >= (int)disp->width ) - { - ex = disp->width - tx; - } - - /* Clipping bottom */ - if( (ty + ey) >= disp->height ) - { - ey = disp->height - ty; - } + if( !__clip_blit( disp, x, y, &sx, &sy, &ex, &ey, &tx, &ty ) ) { return; } int pix_stride = TEX_FORMAT_BYTES2PIX(surface_get_format(disp), disp->stride); int depth = TEX_FORMAT_BITDEPTH(surface_get_format( disp )); @@ -757,8 +812,7 @@ void graphics_draw_sprite_trans_stride( surface_t* disp, int x, int y, sprite_t __sprite_upgrade(sprite); /* For spritemaps */ - int tx = x; - int ty = y; + int tx, ty; /* Calculate the location size of the actual sprite we will be blitting */ int sx, sy, ex, ey; @@ -773,9 +827,6 @@ void graphics_draw_sprite_trans_stride( surface_t* disp, int x, int y, sprite_t sy = (offset / sprite->hslices) * theight; ex = sx + twidth; ey = sy + theight; - - tx -= sx; - ty -= sy; } else { @@ -786,41 +837,7 @@ void graphics_draw_sprite_trans_stride( surface_t* disp, int x, int y, sprite_t ey = sprite->height; } - /* Too far left */ - if( (tx + ex) <= 0 ) { return; } - - /* Too far up */ - if( (ty + ey) <= 0 ) { return; } - - /* Too far right */ - if( tx >= (int)disp->width ) { return; } - - /* Too far down */ - if( ty >= (int)disp->height ) { return; } - - /* Clipping left */ - if( x < 0 ) - { - sx += (x * -1); - } - - /* Clipping top */ - if( y < 0 ) - { - sy += (y * -1); - } - - /* Clipping right */ - if( (tx + ex) >= (int)disp->width ) - { - ex = disp->width - tx; - } - - /* Clipping bottom */ - if( (ty + ey) >= disp->height ) - { - ey = disp->height - ty; - } + if( !__clip_blit( disp, x, y, &sx, &sy, &ex, &ey, &tx, &ty ) ) { return; } int pix_stride = TEX_FORMAT_BYTES2PIX(surface_get_format(disp), disp->stride); int depth = TEX_FORMAT_BITDEPTH(surface_get_format( disp )); diff --git a/src/hashtable.c b/src/hashtable.c new file mode 100644 index 000000000..bc4e8bf6b --- /dev/null +++ b/src/hashtable.c @@ -0,0 +1,171 @@ +/** + * @file hashtable.c + * @author Giovanni Bajo + */ +#include +#include +#include +#include +#include "hashtable_internal.h" + +#ifdef N64 +#include "n64sys.h" +#else +/// @cond +#define PhysicalAddr(p) ((uint32_t)(uintptr_t)(p) & 0xFFFFFF) +#define VirtualCachedAddr(v) ((void*)(uintptr_t)((v) & 0xFFFFFF)) +/// @endcond +#endif + +#define EMPTY_KEY 0 ///< Key that represents an empty slot +#define TOMBSTONE_KEY 0xFFFFFFFF ///< Key that represents a tombstone (deleted key) + +/** Tagged pointer containing also a 8-bit reference counter */ +typedef uint32_t counted_ptr_t; // [31..24]=refcount, [23..0]=PhysicalAddr + +static inline uint8_t refcount(counted_ptr_t v) { return v >> 24; } +static inline void set_refcount(counted_ptr_t *v, uint8_t r) { + *v = (*v & 0xFFFFFF) | ((uint32_t)r << 24); +} +static inline void* cached_addr(counted_ptr_t v) { + return VirtualCachedAddr(v & 0xFFFFFF); +} + +static inline uint32_t hash32(uint32_t x) { + x ^= x >> 16; x *= 0x7feb352d; x ^= x >> 15; x *= 0x846ca68b; x ^= x >> 16; + return x; +} + +/* Returns pointer to key slot: existing key or first free/tombstone slot */ +static uint32_t* hashtable_lookup_slot(hashtable_t *h, uint32_t k) { + size_t mask = h->capacity - 1; // capacity is power of 2, so this works + size_t hash = hash32(k); + uint32_t *tomb_key = NULL; + + for (size_t i = 0; i < h->capacity; i++) { + size_t idx = ((hash + i) & mask) * 2; // Each slot is 2 elements apart + uint32_t *kk = &h->entries[idx]; + if (*kk == k) return kk; + if (*kk == TOMBSTONE_KEY && !tomb_key) tomb_key = kk; + if (*kk == EMPTY_KEY) return tomb_key ? tomb_key : kk; + } + + // If no EMPTY_KEY was found but we saw a tombstone, reuse it. + if (tomb_key) return tomb_key; + + // Should never reach here if load factor is kept reasonable + assert(0 && "hashtable full"); + return NULL; +} + +int hashtable_init(hashtable_t *h, size_t initial_entries, hashtable_loader_fn loader) { + // Round up to next power of 2, ensuring capacity is at least initial_entries + size_t capacity = 1; + while (capacity < initial_entries) capacity <<= 1; + + h->capacity = capacity; + // Allocate interleaved key/value array: capacity pairs = capacity * 2 uint32_t + h->entries = aligned_alloc(16, sizeof(uint32_t) * capacity * 2); + assert(h->entries); + memset(h->entries, 0, sizeof(uint32_t) * capacity * 2); + h->size = 0; + h->loader = loader; + return 1; +} + +void hashtable_free(hashtable_t *h) { + free(h->entries); + h->entries = NULL; +} + +static void hashtable_resize(hashtable_t *h, size_t new_capacity) { + uint32_t *newb = aligned_alloc(16, sizeof(uint32_t) * new_capacity * 2); + assert(newb); + memset(newb, 0, sizeof(uint32_t) * new_capacity * 2); + + uint32_t *old = h->entries; + size_t old_capacity = h->capacity; + + h->entries = newb; + h->capacity = new_capacity; + h->size = 0; + + // Rehash all existing entries + for (size_t i = 0; i < old_capacity * 2; i += 2) { + uint32_t k = old[i]; // key + if (k == EMPTY_KEY || k == TOMBSTONE_KEY) continue; + counted_ptr_t v = old[i + 1]; // value + + uint32_t *kk = hashtable_lookup_slot(h, k); + *kk = k; + kk[1] = v; // value is immediately after key + h->size++; + } + free(old); +} + +void* hashtable_insert(hashtable_t *h, uint32_t k, void *value) { + assert(k != EMPTY_KEY && k != TOMBSTONE_KEY); + + // Resize when load factor exceeds 75% + size_t threshold = h->capacity * 3 / 4; + if (h->size > threshold) hashtable_resize(h, h->capacity * 2); + + uint32_t *kk = hashtable_lookup_slot(h, k); + counted_ptr_t *vv = (counted_ptr_t*)(kk + 1); // value is immediately after key + + if (*kk == k) { + uint8_t r = refcount(*vv); assert(r < 255); + if (value) assert(PhysicalAddr(value) == (*vv & 0xFFFFFF)); + set_refcount(vv, r + 1); + } else { + void *p = value ? value : (h->loader ? h->loader(k) : NULL); + *kk = k; + *vv = (1u << 24) | PhysicalAddr(p); + h->size++; + } + return cached_addr(*vv); +} + +void* hashtable_lookup(hashtable_t *h, uint32_t k) { + assert(k != EMPTY_KEY && k != TOMBSTONE_KEY); + uint32_t *kk = hashtable_lookup_slot(h, k); + return (*kk == k) ? cached_addr(kk[1]) : NULL; // value is immediately after key +} + +void* hashtable_remove(hashtable_t *h, uint32_t k) { + assert(k != EMPTY_KEY && k != TOMBSTONE_KEY); + size_t mask = h->capacity - 1; // capacity is power of 2, so this works + size_t hash = hash32(k); + + for (size_t i = 0; i < h->capacity; i++) { + size_t idx = ((hash + i) & mask) * 2; // Each slot is 2 elements apart + uint32_t *kk = &h->entries[idx]; + if (*kk == k) { + uint8_t r = refcount(kk[1]); // value immediately after key + void *v = cached_addr(kk[1]); + if (--r > 0) { set_refcount(&kk[1], r); return NULL; } + kk[0] = TOMBSTONE_KEY; // mark key as tombstone + kk[1] = 0; // clear value + h->size--; + return v; + } + if (*kk == EMPTY_KEY) return NULL; // Key not found + } + return NULL; // Key not found +} + +void hashtable_visit(hashtable_t *h, void (*visitor)(uint32_t key, void *value, int refcount)) { + for (size_t i = 0; i < h->capacity * 2; i += 2) { + uint32_t k = h->entries[i]; + if (k == EMPTY_KEY || k == TOMBSTONE_KEY) continue; + counted_ptr_t v = h->entries[i + 1]; + visitor(k, cached_addr(v), refcount(v)); + } +} + +void hashtable_clear(hashtable_t *h) +{ + memset(h->entries, 0, sizeof(uint32_t) * h->capacity * 2); + h->size = 0; +} diff --git a/src/hashtable_internal.h b/src/hashtable_internal.h new file mode 100644 index 000000000..86a0433c9 --- /dev/null +++ b/src/hashtable_internal.h @@ -0,0 +1,127 @@ +/** + * @file hashtable_internal.h + * @author Giovanni Bajo + * @brief Simple cache-friendly open addressing hashtable with reference counting. + * + * Keys are 32-bit integers. Values are stored as pointers (void*) internally converted + * to 24-bit physical addresses via PhysicalAddr()/CachedAddr() macros (user must ensure validity). + * + * Each key has an 8-bit reference count (1..255). + * When inserting an existing key, the reference count is incremented. + * Removing decrements the reference count, and the key is deleted when it reaches zero. + */ +#ifndef LIBDRAGON_HASHTABLE_INTERNAL_H +#define LIBDRAGON_HASHTABLE_INTERNAL_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Loader callback used to lazily create a value for a given key. + * + * This function is called by hashtable_insert() when a key is inserted + * for the first time and no value is provided. + * + * @param key The 32-bit key for which the value should be created. + * @return A pointer to the created value (may be NULL if no value). + */ +typedef void* (*hashtable_loader_fn)(uint32_t key); + +/** @brief A hashtable structure */ +typedef struct hashtable_s { + uint32_t *entries; ///< Pointer to the interleaved key/value array + size_t capacity; ///< Total capacity (number of key/value pairs) + size_t size; ///< Current number of entries in the hashtable + hashtable_loader_fn loader; ///< Loader function to lazily create values +} hashtable_t; + +/** + * @brief Initialize a hashtable. + * + * @param h Pointer to a hashtable_t structure. + * @param initial_entries Suggested initial number of entries (will be rounded up). + * @param loader A loader function to lazily create values, or NULL if not used. + * @return 1 on success, 0 on failure. + */ +int hashtable_init(hashtable_t *h, size_t initial_entries, hashtable_loader_fn loader); + +/** + * @brief Destroy a hashtable and free all associated memory. + * + * @param h Pointer to a hashtable_t structure. + */ +void hashtable_free(hashtable_t *h); + +/** + * @brief Insert an object into the hashtable. + * + * The function expects a 32-bit *unique* key as input. The caller must ensure + * that the key is unique for each possible value. Eg: you can use the + * value itself as key (the 32-bit pointer). + * + * Notice that the special values 0x0000000 and 0xFFFFFFFF are reserved and + * cannot be used as keys. + * + * If the specified key is not found in the table: + * - If @p value is not NULL, it is stored directly, with reference count 1. + * - If @p value is NULL, the loader (specified in #hashtable_init, if any) + * is called to create it. + * + * It is possible to insert the same object multiple times. In this case, the + * reference count of the value is incremented. You will then need to call + * #hashtable_remove the same number of times to actually remove the object. + * + * @param h Pointer to a hashtable_t. + * @param key 32-bit key to insert. + * @param value Optional value to associate with the key, or NULL to use the loader. + * @return The stored value (void*). + */ +void* hashtable_insert(hashtable_t *h, uint32_t key, void *value); + +/** + * @brief Lookup a value associated with a key. + * + * @param h Pointer to a hashtable. + * @param key 32-bit key to search. + * @return The stored value (void*), or NULL if the key does not exist. + */ +void* hashtable_lookup(hashtable_t *h, uint32_t key); + +/** + * @brief Remove an object from the hashtable (or decrement its reference count). + * + * Decrements the reference count. If the reference count reaches 0, + * the object is removed from the hashtable and returned back to the caller + * (in case it needs to be freed). + * + * @param h Pointer to a hashtable_t. + * @param key 32-bit key to remove. + * @return The value associated with the key before removal, or NULL + * if the key did not exist or its reference count did not reach zero. + */ +void* hashtable_remove(hashtable_t *h, uint32_t key); + +/** + * @brief Visit all objects in the hashtable. + * + * @param h Pointer to a hashtable_t. + * @param visitor Callback function to call for each key/value pair. + */ +void hashtable_visit(hashtable_t *h, void (*visitor)(uint32_t key, void *value, int refcount)); + +/** + * @brief Remove all objects from the hashtable, regardless of their reference count. + * + * @param h Pointer to a hashtable_t. + */ +void hashtable_clear(hashtable_t *h); + +#ifdef __cplusplus +} +#endif + +#endif /* LIBDRAGON_HASHTABLE_INTERNAL_H */ diff --git a/src/i2c_internal.h b/src/i2c_internal.h new file mode 100644 index 000000000..6d4530dab --- /dev/null +++ b/src/i2c_internal.h @@ -0,0 +1,134 @@ +/** + * @file i2c_internal.h + * @author Giovanni Bajo + */ +#ifndef LIBDRAGON_I2C_INTERNAL_H +#define LIBDRAGON_I2C_INTERNAL_H + +#include +#include +#include "debug.h" +#include "n64sys.h" + +// Simple implementation of 2-wire I2C protocol +// Before including this file, define the following macros: +// - I2C_INIT() : Initialize a I2C transaction +// - I2C_WRITE(scl, sda) : Write values to the SCL (clock) and SDA (data) lines +// - I2C_READ(scl) : Read the value of the SDA (data) line, while setting the SCL (clock) line + +#if !defined(I2C_INIT) || !defined(I2C_WRITE) || !defined(I2C_READ) +#error "I2C_WRITE, I2C_READ, and I2C_INIT must be defined before including i2c_internal.h" +#endif + +static inline void i2c_write_start(void) +{ + I2C_WRITE(0, 1); + I2C_WRITE(1, 1); + I2C_WRITE(1, 0); +} + +static inline void i2c_write_stop(void) +{ + I2C_WRITE(1, 0); + I2C_WRITE(1, 1); + I2C_WRITE(0, 1); +} + +static inline void i2c_write_bit(bool data) +{ + I2C_WRITE(0, data); + I2C_WRITE(1, data); + I2C_WRITE(0, data); +} + +static inline bool i2c_read_bit(void) +{ + I2C_WRITE(0, 1); + I2C_WRITE(1, 1); + bool data = I2C_READ(1); + I2C_WRITE(0, 1); + return data; +} + +static inline bool i2c_read_ack(int ackid) +{ + I2C_WRITE(1, 1); + bool ack = !I2C_READ(1); + I2C_WRITE(0, 1); + assertf(ack, "I2C ACK%d failed: expected 0, got 1", ackid); + return ack; +} + +static inline void i2c_write_ack(void) +{ + i2c_write_bit(0); +} + +static inline void i2c_write_nack(void) +{ + i2c_write_bit(1); +} + +static inline void i2c_write_byte(uint8_t data) +{ + for (int i=7; i>=0; i--) { + i2c_write_bit((data >> i) & 1); + } +} + +static inline uint8_t i2c_read_byte(void) +{ + uint8_t data = 0; + for (int i=7; i>=0; i--) { + data |= (i2c_read_bit() << i); + } + return data; +} + +__attribute__((used)) +static bool i2c_read_data(uint8_t slave_addr, uint8_t addr, int len, uint8_t *data) +{ + bool ok = true; + I2C_INIT(); + i2c_write_start(); + i2c_write_byte(slave_addr << 1); + if (!i2c_read_ack(0)) { ok = false; goto finish; } + i2c_write_byte(addr); + if (!i2c_read_ack(1)) { ok = false; goto finish; } + i2c_write_start(); + i2c_write_byte((slave_addr << 1) | 1); // read mode + if (!i2c_read_ack(2)) { ok = false; goto finish; } + for (int i=0; i */ +#ifndef NDEBUG #include "graphics.h" +#include "vi_internal.h" +#include "display.h" #include "debug.h" -#include "controller.h" +#include "surface.h" +#include "joypad.h" +#include "rompak_internal.h" +#include "joybus/joypad_internal.h" #include "exception_internal.h" #include "system.h" #include "utils.h" #include "backtrace.h" #include "backtrace_internal.h" -#include "dlfcn_internal.h" +#include "kernel/kernel_internal.h" #include "cop0.h" #include "n64sys.h" -#include "display.h" +#include "mi.h" +#include "emux.h" #include #include #include @@ -27,10 +34,13 @@ enum Mode { }; enum { + XTITLE = 64, + YTITLE = 2, XSTART = 48, XEND = 640-48, YSTART = 16, YEND = 240-8-8, + YBODY_END = 240-8-8-8, }; /** @brief Pack a 16-bit color into a 32-bit word. */ @@ -53,13 +63,19 @@ enum { /// @endcond static int cursor_x, cursor_y, cursor_columns, cursor_wordwrap; +/** Extra indent (pixels) for wrapped lines; set by \aZ, reset on \n */ +static int cursor_wrap_indent; +static enum Mode inspector_mode; static surface_t *disp; static int fpr_show_mode = 1; static int disasm_bt_idx = 0; static int disasm_max_frames = 0; static int disasm_offset = 0; -static int module_offset = 0; -static bool first_backtrace = true; +static int thread_offset = 0; +static int num_threads = 0; +static int backtrace_count = 0; +static int page_scroll_y = 0, page_scroll_max = 0; +static bool page_scroll_on; const char *__mips_gpr[34] = { "zr", "at", "v0", "v1", "a0", "a1", "a2", "a3", @@ -206,14 +222,48 @@ static bool disasm_valid_pc(uint32_t pc) { return pc >= 0x80000000 && pc < 0x80800000 && (pc & 3) == 0; } +/* Good breakpoints for wrapping C++-style symbols and signatures. */ +static bool is_wrap_break_char(char c) +{ + switch (c) { + case ' ': case ',': case '/': + case '<': case '>': case ':': case '&': + case '*': case '[': case ']': + return true; + default: + return false; + } +} + +/* Measure next printable token width in pixels, skipping escape controls. */ +static int next_token_width_px(const char *buf, unsigned int len, int start) +{ + int px = 0; + for (int j = start; j < (int)len; j++) { + char c = buf[j]; + if (c == '\n' || c == '\t' || is_wrap_break_char(c)) + break; + if (c == '\a') { + if (++j >= (int)len) break; + if (buf[j] == 'Z' && ++j >= (int)len) break; + continue; + } + if (c == '\b') + continue; + px += 8; + } + return px; +} + static int inspector_stdout(char *buf, unsigned int len) { for (int i=0; i= 640) break; - switch (buf[i]) { case '\a': { uint32_t color = COLOR_TEXT; - switch (buf[++i]) { + bool apply_color = true; + if (++i >= (int)len) break; + int esc = buf[i]; + switch (esc) { case 'T': color = COLOR_TEXT; break; case 'E': color = COLOR_EMPHASIS; break; case 'O': color = COLOR_ORANGE; break; @@ -221,8 +271,20 @@ static int inspector_stdout(char *buf, unsigned int len) { case 'M': color = COLOR_MAGENTA; break; case 'G': color = COLOR_GREEN; break; case 'W': color = COLOR_WHITE; break; + case 'Z': + apply_color = false; + if (++i < (int)len) { + int d = buf[i]; + cursor_wrap_indent = (d >= '0' && d <= '9') ? (d - '0') * 8 : 0; + } else { + cursor_wrap_indent = 0; + } + break; + default: + break; } - graphics_set_color(color, COLOR_BACKGROUND); + if (apply_color) + graphics_set_color(color, COLOR_BACKGROUND); } break; case '\b': cursor_wordwrap = true; @@ -230,35 +292,46 @@ static int inspector_stdout(char *buf, unsigned int len) { case '\t': cursor_x = ROUND_UP(cursor_x+1, cursor_columns); if (cursor_wordwrap && cursor_x >= XEND) { - cursor_x = XSTART; + cursor_x = XSTART + cursor_wrap_indent; cursor_y += 8; + if (page_scroll_on && cursor_y > page_scroll_max) page_scroll_max = cursor_y; } break; case '\n': + cursor_wrap_indent = 0; cursor_x = XSTART; - cursor_y += 8; + if (cursor_y == YTITLE) { + cursor_y = YSTART; + } else { + cursor_y += 8; + } + if (page_scroll_on && cursor_y > page_scroll_max) page_scroll_max = cursor_y; cursor_wordwrap = false; graphics_set_color(COLOR_TEXT, COLOR_BACKGROUND); break; default: + if (cursor_wordwrap && cursor_x >= XEND) { + cursor_x = XSTART + cursor_wrap_indent; + cursor_y += 8; + if (page_scroll_on && cursor_y > page_scroll_max) page_scroll_max = cursor_y; + } if (cursor_x < XEND) { - graphics_draw_character(disp, cursor_x, cursor_y, buf[i]); + if (page_scroll_on && cursor_y >= YSTART) { + int y = cursor_y - page_scroll_y; + if (y >= YSTART && y <= YBODY_END) + graphics_draw_character(disp, cursor_x, y, buf[i]); + } else { + graphics_draw_character(disp, cursor_x, cursor_y, buf[i]); + } cursor_x += 8; - if (cursor_wordwrap && buf[i] == ' ') { - // Check if we can fit the next word - int j = i+1; - while (j < len && buf[j] != ' ' && buf[j] != '\n') - j++; - // If it doesn't fit, wrap - if (cursor_x + (j-i)*8 >= XEND) { - cursor_x = XSTART; + if (cursor_wordwrap && is_wrap_break_char(buf[i])) { + int next_word_px = next_token_width_px(buf, len, i + 1); + if (next_word_px > 0 && cursor_x + next_word_px > XEND) { + cursor_x = XSTART + cursor_wrap_indent; cursor_y += 8; + if (page_scroll_on && cursor_y > page_scroll_max) page_scroll_max = cursor_y; } } - if (cursor_wordwrap && cursor_x >= XEND) { - cursor_x = XSTART; - cursor_y += 8; - } } break; } @@ -266,19 +339,49 @@ static int inspector_stdout(char *buf, unsigned int len) { return len; } -static void title(const char *title) { - graphics_draw_box(disp, 0, 0, 640, 12, COLOR_TEXT); - graphics_set_color(COLOR_BACKGROUND, COLOR_TEXT); - graphics_draw_text(disp, 64, 2, title); - graphics_set_color(COLOR_TEXT, COLOR_BACKGROUND); +static void inspector_print_backtrace(void *bt, int n, int bt_skip, bool also_debugf) +{ + printf("\aWBacktrace:\n"); + if (also_debugf) debugf("Backtrace:\n"); + bool skip = true; + void cb(void *arg, backtrace_frame_t *frame) { + if (also_debugf) { debugf(" "); backtrace_frame_print(frame, stderr); debugf("\n"); } + if (skip) { + if (strstr(frame->func, "")) + skip = false; + return; + } + if (bt_skip > 0) { + bt_skip--; + return; + } + const char *source_file = frame->source_file; + bool have_source = source_file && source_file[0] && strcmp(source_file, "???") != 0; + char file_part[96]; + if (have_source) + snprintf(file_part, sizeof(file_part), "(%s:%d)", source_file, frame->source_line); + else + snprintf(file_part, sizeof(file_part), "[0x%08lx]", (unsigned long)frame->addr); + + // Print function with arbitrary word-wrapped multiline layout. + printf("%*s\aZ%d\b\aG%s\aT", 4, "", 5, frame->func); + printf(" %s\n", file_part); + } + backtrace_symbols_cb(bt, n, 0, cb, NULL); + if (skip) { + // we didn't find the exception handler for some reason (eg: missing symbols) + // so just print the whole thing + skip = false; + backtrace_symbols_cb(bt, n, 0, cb, NULL); + } } -static void inspector_page_exception(surface_t *disp, exception_t* ex, enum Mode mode, bool with_backtrace) { +static void inspector_page_exception(surface_t *disp, exception_t* ex, joypad_buttons_t *key_pressed) { int bt_skip = 0; - switch (mode) { + switch (inspector_mode) { case MODE_EXCEPTION: - title("CPU Exception"); + printf("CPU Exception\n"); printf("\aO"); __exception_dump_header(stdout, ex); printf("\n"); @@ -292,10 +395,14 @@ static void inspector_page_exception(surface_t *disp, exception_t* ex, enum Mode } else { printf(" \n\n", epc); } + + if (__kernel) { + printf("\aWThread:\n %s\n\n", kthread_current()->name); + } break; case MODE_ASSERTION: { - title("CPU Assertion"); + printf("CPU Assertion\n"); const char *failedexpr = (const char*)(uint32_t)ex->regs->gpr[4]; const char *msg = (const char*)(uint32_t)ex->regs->gpr[5]; va_list args = (va_list)(uint32_t)ex->regs->gpr[6]; @@ -308,11 +415,14 @@ static void inspector_page_exception(surface_t *disp, exception_t* ex, enum Mode } else { printf("\b\aOASSERTION FAILED: %s\n\n", failedexpr); } + if (__kernel) { + printf("\aWThread:\n %s\n\n", kthread_current()->name); + } bt_skip = 2; break; } case MODE_CPP_EXCEPTION: { - title("Uncaught C++ Exception"); + printf("Uncaught C++ Exception\n"); const char *exctype = (const char*)(uint32_t)ex->regs->gpr[4]; const char *what = (const char*)(uint32_t)ex->regs->gpr[5]; printf("\b\aOC++ Exception: %s\n\n", what); @@ -320,49 +430,24 @@ static void inspector_page_exception(surface_t *disp, exception_t* ex, enum Mode printf("\aWException type:\n"); printf(" "); printf("\b%s", exctype); printf("\n\n"); } + if (__kernel) { + printf("\aWThread:\n %s\n\n", kthread_current()->name); + } bt_skip = 5; break; } } - if (!with_backtrace) + if (backtrace_count++ == 0) return; void *bt[32]; int n = backtrace(bt, 32); - - printf("\aWBacktrace:\n"); - if (first_backtrace) debugf("Backtrace:\n"); - char func[128]; - bool skip = true; - void cb(void *arg, backtrace_frame_t *frame) { - if (first_backtrace) { debugf(" "); backtrace_frame_print(frame, stderr); debugf("\n"); } - if (skip) { - if (strstr(frame->func, "")) - skip = false; - return; - } - if (bt_skip > 0) { - bt_skip--; - return; - } - printf(" "); - snprintf(func, sizeof(func), "\aG%s\aT", frame->func); - frame->func = func; - backtrace_frame_print_compact(frame, stdout, 60); - } - backtrace_symbols_cb(bt, n, 0, cb, NULL); - if (skip) { - // we didn't find the exception handler for some reason (eg: missing symbols) - // so just print the whole thing - skip = false; - backtrace_symbols_cb(bt, n, 0, cb, NULL); - } - first_backtrace = false; + inspector_print_backtrace(bt, n, bt_skip, backtrace_count == 2); } -static void inspector_page_gpr(surface_t *disp, exception_t* ex) { - title("CPU Registers"); +static void inspector_page_gpr(surface_t *disp, exception_t* ex, joypad_buttons_t *key_pressed) { + printf("CPU Registers\n"); cursor_columns = 92; int c = 0; @@ -375,13 +460,13 @@ static void inspector_page_gpr(surface_t *disp, exception_t* ex) { __exception_dump_gpr(ex, cb, NULL); } -static void inspector_page_fpr(surface_t *disp, exception_t* ex, struct controller_data *key_pressed) { - if (key_pressed->c[0].A) +static void inspector_page_fpr(surface_t *disp, exception_t* ex, joypad_buttons_t *key_pressed) { + if (key_pressed->a) fpr_show_mode = (fpr_show_mode + 1) % 3; - title(fpr_show_mode == 0 ? "CPU Floating Point Registers (Hex)" : - fpr_show_mode == 1 ? "CPU Floating Point Registers (Single)" : - "CPU Floating Point Registers (Double)"); + printf(fpr_show_mode == 0 ? "CPU Floating Point Registers (Hex)\n" : + fpr_show_mode == 1 ? "CPU Floating Point Registers (Single)\n" : + "CPU Floating Point Registers (Double)"); int c = 0; void cb(void *arg, const char *name, char *hexvalue, char *singlevalue, char *doublevalue) { @@ -392,23 +477,23 @@ static void inspector_page_fpr(surface_t *disp, exception_t* ex, struct controll __exception_dump_fpr(ex, cb, NULL); } -static void inspector_page_disasm(surface_t *disp, exception_t* ex, struct controller_data *key_pressed) { - if (key_pressed->c[0].up && disasm_bt_idx > 0) { +static void inspector_page_disasm(surface_t *disp, exception_t* ex, joypad_buttons_t *key_pressed) { + if (key_pressed->d_up && disasm_bt_idx > 0) { disasm_bt_idx--; disasm_offset = 0; } - if (key_pressed->c[0].down && disasm_bt_idx < disasm_max_frames-1) { + if (key_pressed->d_down && disasm_bt_idx < disasm_max_frames-1) { disasm_bt_idx++; disasm_offset = 0; } - if (key_pressed->c[0].C_up) { + if (key_pressed->c_up) { disasm_offset -= 4*6; } - if (key_pressed->c[0].C_down) { + if (key_pressed->c_down) { disasm_offset += 4*6; } - title("Disassembly"); + printf("Disassembly\n"); void *bt[32]; int n = backtrace(bt, 32); @@ -466,122 +551,276 @@ static void inspector_page_disasm(surface_t *disp, exception_t* ex, struct contr } } -static void inspector_page_modules(surface_t *disp, exception_t* ex, struct controller_data *key_pressed) +static void inspector_page_threads(surface_t *disp, exception_t* ex, joypad_buttons_t *key_pressed) { - dl_module_t *curr_module = __dl_list_head; - size_t module_idx = 0; - if(key_pressed->c[0].up && module_offset > 0) { - module_offset--; - } - if(key_pressed->c[0].down && module_offset+18 < __dl_num_loaded_modules) { - module_offset++; + const int THREAD_LEN = 12; + const int THREADS_PER_LINE = 5; + kthread_t *th_sel = NULL; + + if (key_pressed->d_left && thread_offset > 0) { thread_offset--; } + if (key_pressed->d_right && thread_offset < num_threads-1) { thread_offset++; } + if (key_pressed->d_up && thread_offset >= THREADS_PER_LINE) { thread_offset -= THREADS_PER_LINE; } + if (key_pressed->d_down && thread_offset+THREADS_PER_LINE < num_threads) { thread_offset += THREADS_PER_LINE; } + + printf("Threads\n"); + if (!__kernel) { + printf("\aWkernel not initialized\n"); + return; } - title("Loaded modules"); - while(curr_module) { - if(module_idx >= module_offset && module_idx < module_offset+18) { - void *module_min = curr_module->prog_base; - void *module_max = ((uint8_t *)module_min)+curr_module->prog_size; - printf("%s (%p-%p)\n", curr_module->filename, module_min, module_max); + + int i = 0; + for (kthread_t *th = __kernel_all_threads; th; th = th->all_next) { + char buf[THREAD_LEN+4]; + + if (i == thread_offset) { + sprintf(buf, "[%.*s]", THREAD_LEN, th->name); + printf("\aW%-*s\aT", THREAD_LEN+2, buf); + th_sel = th; + } else { + sprintf(buf, " %.*s ", THREAD_LEN, th->name); + printf("%-*s", THREAD_LEN+2, buf); } - curr_module = curr_module->next; - module_idx++; + + if ((i+1)%THREADS_PER_LINE == 0) printf("\n"); + i++; + } + if (i % THREADS_PER_LINE != 0) printf("\n"); + printf("\n"); + + char type[64] = {0}; + + if (th_sel->flags & TH_FLAG_INSPECTOR1) + strcat(type, "suspended "); + if (th_sel->flags & TH_FLAG_DETACHED) { + strcat(type, "detached "); + if (th_sel->flags & TH_FLAG_ZOMBIE) + strcat(type, "zombie "); + } else if (th_sel->joiner) { + strcat(type, "joined("); + strcat(type, th_sel->joiner->name); + strcat(type, ") "); + } else { + strcat(type, "joinable "); + if (th_sel->flags & TH_FLAG_WAITFORJOIN) + strcat(type, "joinable finished "); + } + + printf(" \aWPriority: \aT%-30d\aWStack size: \aT%d KiB\n", th_sel->pri, th_sel->stack_size / 1024); + printf(" \aWType: \aT%-34.34s\aWStack: \aT%p\n", type, th_sel->stack); + printf(" \aWPC: \aT%p\n", th_sel == kthread_current() ? (void*)ex->regs->epc : (void*)th_sel->stack_state->epc); + printf("\n"); + + void *bt[32]; int n; + n = kthread_backtrace(th_sel, bt, 32); + inspector_print_backtrace(bt, n, 0, false); +} + +static void version_callback(void *ctx, char *key, char *value) +{ + printf(" \aG%s: \aT%s\n", key, value); +} + +static bool version_walk(void *ctx, const char *name, pi_addr_t address, size_t size) +{ + if (strstr(name, ".version")) { + printf("\aW%s:\n", name); + rompak_version_parse(address, size, version_callback, ctx); + printf("\n"); } + return true; +} + +static void inspector_page_version(surface_t *disp, exception_t* ex, joypad_buttons_t *key_pressed) +{ + printf("Versions\n"); + rompak_walk(version_walk, NULL); } +/** @brief The inspector pages */ +inspector_page_t inspector_pages[16] = { + inspector_page_exception, + inspector_page_gpr, + inspector_page_fpr, + inspector_page_disasm, + inspector_page_version, + inspector_page_threads, +}; + __attribute__((noreturn)) static void inspector(exception_t* ex, enum Mode mode) { static bool in_inspector = false; if (in_inspector) abort(); in_inspector = true; + inspector_mode = mode; + + if (__kernel) { + // Reverse the order of the thread list, so that we show the oldest threads + // first in the list + kthread_t *prev = NULL; + kthread_t *th = __kernel_all_threads; + while (th) { + kthread_t *next = th->all_next; + th->all_next = prev; + prev = th; + th = next; + } + __kernel_all_threads = prev; + + // Suspend all threads but idle. This avoids the inpsector to be interrupted + // by other threads, and also allows to consistently inspect the state of the threads + kthread_t *current = kthread_current(); + for (kthread_t *th = __kernel_all_threads; th; th = th->all_next) { + if (th != current && strcmp(th->name, "idle") != 0) { + // Copy the current suspended flag into the inspector flag, + // so that we can later dump the state of the thread correctly + if (th->flags & TH_FLAG_SUSPENDED) + th->flags |= TH_FLAG_INSPECTOR1; + else + th->flags &= ~TH_FLAG_INSPECTOR1; + kthread_suspend(th); + } + ++num_threads; + } + + // Mask interrupts that we don't need during the inspector, so that + // their callbacks aren't called, which might cause crashes now. + // When the kernel is running, interrupts can always be activated in + // case of thread switch (eg: when using kirq), so to completely + // disable them we need to mask them. + *MI_MASK = MI_WMASK_CLR_DP | MI_WMASK_CLR_AI | MI_WMASK_CLR_VI; + } + // Call vi_write_end_forced in case we crashed within vi_begin() block, + // otherwise all VI register changes would not be applied. + vi_write_end_forced(); + + // Close display if it was open. This is useful mainly to free the framebuffers + // and hopefully be able to allocate the inspector's framebuffers even in + // low memory conditions. display_close(); - display_init(RESOLUTION_640x240, DEPTH_16_BPP, 2, GAMMA_NONE, FILTERS_RESAMPLE); - - enum Page { - PAGE_EXCEPTION, - PAGE_GPR, - PAGE_FPR, - PAGE_CODE, - PAGE_MODULES - }; - enum { PAGE_COUNT = PAGE_MODULES+1 }; + + // Reset the VI to default state, so that we don't inherit any weird + // configuration from the crashed program. + vi_init(); + vi_reset(); + + // Try to allocate two framebuffers. We might be out of memory though, + // so be happy with a single framebuffer if that fails. + int fbidx = 0; + surface_t fb[2] = {0}; + fb[0] = surface_alloc(FMT_RGBA16, 640, 240); + if (fb[0].buffer != NULL) { + fb[1] = surface_alloc(FMT_RGBA16, 640, 240); + } else { + // If we couldn't allocate any framebuffer, try to use the memory at + // the end of RAM. There's no guarantee this won't corrupt the inspector, + // but at least we try something. + int mem_size = get_memory_size(); + void *end_mem = (void*)(0x80000000 + mem_size); + fb[0] = surface_make_linear(end_mem - 64*1024 - 640*240*2, FMT_RGBA16, 640, 240); + } hook_stdio_calls(&(stdio_t){ NULL, inspector_stdout, NULL }); - static bool backtrace = false; - struct controller_data key_old = {0}; - struct controller_data key_pressed = {0}; - enum Page page = PAGE_EXCEPTION; + bool first_frame = true; + sys_version_t version = {0}; + joypad_buttons_t key_old = {0}; + joypad_buttons_t key_pressed = {0}; + int prevPad = -1; + int page = 0; + int prev_page = -1; + int page_count = sizeof(inspector_pages) / sizeof(inspector_pages[0]); + while (inspector_pages[page_count-1] == NULL) { + page_count--; + } + while (1) { - if (key_pressed.c[0].Z || key_pressed.c[0].R) { - //Do page wrapping logic from left - if(page == PAGE_COUNT-1) { - page = 0; - } else { - page++; - } + if (key_pressed.z || key_pressed.r) { + // Do page wrapping logic from left + page++; + if (page >= page_count) page = 0; } - if (key_pressed.c[0].L) { - //Do page wrapping logic from right - if(page == 0) { - page = PAGE_COUNT-1; - } else { - page--; - } + if (key_pressed.l) { + // Do page wrapping logic from right + page--; + if (page < 0) page = page_count - 1; } - disp = display_get(); + if (page != prev_page) { + page_scroll_y = 0; + prev_page = page; + } else if (page == 0) { + if (key_pressed.d_up && page_scroll_y >= 8) page_scroll_y -= 8; + if (key_pressed.d_down) page_scroll_y += 8; + } + page_scroll_max = 0; + page_scroll_on = (page == 0); + disp = &fb[fbidx]; - cursor_x = XSTART; - cursor_y = YSTART; + // Clear the screen, initialize printf cursor position + cursor_x = XTITLE; + cursor_y = YTITLE; cursor_columns = 8*8; graphics_set_color(COLOR_TEXT, COLOR_BACKGROUND); graphics_fill_screen(disp, COLOR_BACKGROUND); - - switch (page) { - case PAGE_EXCEPTION: - inspector_page_exception(disp, ex, mode, backtrace); - break; - case PAGE_GPR: - inspector_page_gpr(disp, ex); - break; - case PAGE_FPR: - inspector_page_fpr(disp, ex, &key_pressed); - break; - case PAGE_CODE: - inspector_page_disasm(disp, ex, &key_pressed); - break; - case PAGE_MODULES: - inspector_page_modules(disp, ex, &key_pressed); - break; - } - + graphics_draw_box(disp, 0, 0, 640, 12, COLOR_TEXT); + graphics_set_color(COLOR_BACKGROUND, COLOR_TEXT); + + // Draw the current page + inspector_pages[page](disp, ex, &key_pressed); fflush(stdout); + if (page == 0) { + int max = page_scroll_max - YBODY_END; + if (page_scroll_y < 0) page_scroll_y = 0; + else if (max > 0 && page_scroll_y > max) page_scroll_y = max; + } + page_scroll_on = false; + // Draw the footer cursor_x = XSTART; cursor_y = YEND + 2; cursor_columns = 64; graphics_draw_box(disp, 0, YEND, 640, 240-YEND, COLOR_TEXT); graphics_set_color(COLOR_BACKGROUND, COLOR_TEXT); - printf("\t\t\tLibDragon Inspector | Page %d/%d", page+1, PAGE_COUNT); + int indent = 10 - (strlen(version.branch) + (version.dirty ? 1 : 0) + strlen(version.commit_date))/2; + for (int i = 0; i < indent; i++) putc(' ', stdout); + printf("LibDragon Inspector | %s%s (%s, %.7s) | Page %d/%d", + version.branch, version.dirty ? "*" : "", version.commit_date, version.hash, + page+1, page_count); fflush(stdout); - extern void display_show_force(display_context_t disp); - display_show_force(disp); + // Show the screen + vi_show(disp); + vi_wait_vblank(); + if (fb[1].buffer != NULL) { + fbidx ^= 1; + } + + // If we drew the first frame, we skipped the backtrace to make sure at least + // basic crash information is shown in case the backtrace crashes. + // So redraw immediately to attemp displaying the backtrace now. + if (first_frame) { + first_frame = false; + // parse version information once + sys_get_version(&version); + continue; + } // Loop until a keypress while (1) { - // Read controller using controller_read, that works also when the - // interrupts are disabled and when controller_init has not been called. - struct controller_data key_new; - controller_read(&key_new); - if (key_new.c->data != key_old.c->data) { - key_pressed.c->data = key_new.c->data & ~key_old.c->data; - key_old = key_new; - break; - }; - // If we draw the first frame, turn on backtrace and redraw immediately - if (!backtrace) { - backtrace = true; + // Read controller using #joypad_read_n64_inputs, which works also when + // the interrupts are disabled and when #joypad_init has not been called. + int keyPress = false; + for (int i = 0; i < 4; i++) { + joypad_buttons_t key_new = joypad_read_n64_inputs(i).btn; + if ((key_new.raw || prevPad == i) && key_new.raw != key_old.raw) { + key_pressed = (joypad_buttons_t){ .raw = key_new.raw & ~key_old.raw }; + key_old = key_new; + prevPad = i; + keyPress = true; + break; + }; + } + if (keyPress) { break; } // Avoid constantly banging the PIF with controller reads, that @@ -593,6 +832,16 @@ static void inspector(exception_t* ex, enum Mode mode) { abort(); } +void __inspector_add_page(inspector_page_t page) { + for (int i = 0; i < sizeof(inspector_pages) / sizeof(inspector_pages[0]); i++) { + if (inspector_pages[i] == NULL) { + inspector_pages[i] = page; + return; + } + } + assertf(0, "Too many inspector pages"); +} + __attribute__((noreturn)) void __inspector_exception(exception_t* ex) { inspector(ex, MODE_EXCEPTION); @@ -630,4 +879,11 @@ void __inspector_init(void) { if (code == 2) inspector(ex, MODE_CPP_EXCEPTION); } register_syscall_handler(handler, 0x00001, 0x00002); + + if (emux_detect(1) & EMUX_FEAT1_EXCEPTION) + { + // Activate exceptions on console freeze + emux_exception_set_mask(EMUX_EXCEPTION_ERR_MASK); + } } +#endif \ No newline at end of file diff --git a/src/interrupt.c b/src/interrupt.c index 699cc7c62..42c59cd25 100644 --- a/src/interrupt.c +++ b/src/interrupt.c @@ -9,6 +9,8 @@ #include #include "mi.h" #include "regsinternal.h" +#include "kernel/kernel_internal.h" +#include "kernel/ktls_internal.h" #include "n64sys.h" #include "interrupt.h" #include "debug.h" @@ -32,12 +34,15 @@ * interrupt enable calls that need to be made to re-enable interrupts. A negative * number means that the interrupt system hasn't been initialized yet. */ -static int __interrupt_depth = -1; +int __interrupt_depth = -1; /** @brief Value of the status register at the moment interrupts * got disabled. */ -static int __interrupt_sr = 0; +int __interrupt_sr = 0; + +/** @brief Number of interrupts that have been triggered since boot */ +volatile int64_t __interrupt_counter = 0; /** * @brief Structure of an interrupt callback @@ -111,6 +116,10 @@ static uint32_t __prenmi_tick; */ static void __call_callback( struct callback_link * head ) { + /* Invalidate TP Value: if TLS data is accessed under interrupt, + an exception is raised. */ + void *tp = __th_cur_tp; + __th_cur_tp = KERNEL_TP_INVALID; /* Call each registered callback */ while( head ) { @@ -122,6 +131,8 @@ static void __call_callback( struct callback_link * head ) /* Go to next */ head=head->next; } + /* Restore TP Value */ + __th_cur_tp = tp; } /** @@ -139,12 +150,9 @@ static void __register_callback( struct callback_link ** head, void (*callback)( /* Add to beginning of linked list */ struct callback_link *next = *head; (*head) = malloc(sizeof(struct callback_link)); - - if( *head ) - { - (*head)->next=next; - (*head)->callback=callback; - } + assertf(*head, "Out of memory"); + (*head)->next=next; + (*head)->callback=callback; } } @@ -203,12 +211,16 @@ static void __unregister_callback( struct callback_link ** head, void (*callback void __MI_handler(void) { unsigned long status = *MI_INTERRUPT & *MI_MASK; + ++__interrupt_counter; if( status & MI_INTERRUPT_SP ) { /* Clear interrupt */ SP_regs->status=SP_CLEAR_INTERRUPT; + /* Trigger kernel event */ + if (__kernel) __kcond_broadcast_isr(&__kirq_cond_sp); + __call_callback(SP_callback); } @@ -217,6 +229,9 @@ void __MI_handler(void) /* Clear interrupt */ SI_regs->status=SI_CLEAR_INTERRUPT; + /* Trigger kernel event */ + if (__kernel) __kcond_broadcast_isr(&__kirq_cond_si); + __call_callback(SI_callback); } @@ -225,6 +240,9 @@ void __MI_handler(void) /* Clear interrupt */ AI_regs->status=AI_CLEAR_INTERRUPT; + /* Trigger kernel event */ + if (__kernel) __kcond_broadcast_isr(&__kirq_cond_ai); + __call_callback(AI_callback); } @@ -233,6 +251,9 @@ void __MI_handler(void) /* Clear interrupt */ VI_regs->cur_line=VI_regs->cur_line; + /* Trigger kernel event */ + if (__kernel) __kcond_broadcast_isr(&__kirq_cond_vi); + __call_callback(VI_callback); } @@ -241,6 +262,9 @@ void __MI_handler(void) /* Clear interrupt */ PI_regs->status=PI_CLEAR_INTERRUPT; + /* Trigger kernel event */ + if (__kernel) __kcond_broadcast_isr(&__kirq_cond_pi); + __call_callback(PI_callback); } @@ -249,6 +273,9 @@ void __MI_handler(void) /* Clear interrupt */ *MI_MODE = MI_WMODE_CLR_DPINT; + /* Trigger kernel event */ + if (__kernel) __kcond_broadcast_isr(&__kirq_cond_dp); + __call_callback(DP_callback); } } @@ -262,6 +289,7 @@ void __MI_handler(void) void __MI_BB_handler(void) { unsigned long status = *MI_BB_INTERRUPT & *MI_BB_MASK; + ++__interrupt_counter; if( status & MI_BB_INTERRUPT_FLASH ) { @@ -699,7 +727,7 @@ void set_BB_MD_interrupt(int active) /** * @brief Initialize the interrupt controller */ -__attribute__((constructor)) void __init_interrupts() +__attribute__((constructor(120))) void __init_interrupts(void) { /* Make sure that we aren't initializing interrupts when they are already enabled */ if( __interrupt_depth < 0 ) diff --git a/src/interrupt_internal.h b/src/interrupt_internal.h new file mode 100644 index 000000000..c5852edd0 --- /dev/null +++ b/src/interrupt_internal.h @@ -0,0 +1,50 @@ +/** + * @file interrupt_internal.h + * @author Giovanni Bajo + * @brief Internal APIs for interrupt handling + * + * This module provides a low-level, faster alternative to the + * historical #disable_interrupts and #enable_interrupts functions. + * + * Since their usage is different from the public API, we can't replace + * them directly, but we can still use them internally to speed up performance + * sensitive code paths. + */ +#ifndef INTERRUPT_INTERNAL_H +#define INTERRUPT_INTERNAL_H + +#include "cop0.h" + +/** + * @brief Disable interrupts + * + * This is similar to #disable_interrupts. It does return a status + * value that must be passed to #__enable_interrupts to restore + * the previous interrupt state. + * + * @result An opaque value to pass to #__enable_interrupts + */ +__attribute__((always_inline, warn_unused_result)) +static inline uint32_t __disable_interrupts(void) +{ + uint32_t sr = C0_STATUS(); + C0_WRITE_STATUS(sr & ~C0_STATUS_IE); + return sr; +} + +/** + * @brief Reenable interrupts + * + * This is similar to #enable_interrupts. If called from a nested + * disable/enable pair, it will have no effect on the interrupt + * state. + * + * @param sr The opaque value returned by #__disable_interrupts + */ +__attribute__((always_inline)) +static inline void __enable_interrupts(uint32_t sr) +{ + C0_WRITE_STATUS(sr); +} + +#endif diff --git a/src/inthandler.S b/src/inthandler.S index 5b6e7eaa9..439983f7d 100644 --- a/src/inthandler.S +++ b/src/inthandler.S @@ -155,13 +155,17 @@ exception_critical: nop exception_syscall: - # Syscall exception + # Run the syscall exception handler. This one can optionally + # switch to a different thread: the return value is the new + # stack pointer to use. jal __onSyscallException addiu a0, sp, 32 - j end_interrupt - nop - + # Exit the exception and restore *all* registers including caller-saved + # ones, because if the syscall handler switched to a different thread, + # the new thread might have different values in those registers. + j end_exception_restore_all + addiu sp, v0, -32 exception_coprocessor: # Extract CE bits (28..29) from CR @@ -285,10 +289,61 @@ notcart: jal __MI_handler addiu a0, sp, 32 - # No more interrupts to process, we can exit - # (fallthrough) + # No more interrupts to process sw zero, interrupt_exception_frame + # Check if some interrupt handler requested + # a reschedule to happen + la t0, __isr_force_schedule + lbu t1, (t0) + beqz t1, end_interrupt + sb zero, (t0) + +thread_schedule_from_interrupt: + # Save full exception context in preparation for stack switching + mfc0 t0, C0_SR + or t0, SR_CU1 + mtc0 t0, C0_SR + jal save_fpu_regs + move a0, sp + jal finalize_exception_frame + nop + + # Call the scheduler + jal __kthread_syscall_schedule + addiu a0, sp, 32 + addiu sp, v0, -32 + # fallthrough to end_exception_restore_all + +end_exception_restore_all: + # Restore also caller-saved registers. These are only saved during an exception + # (by finalize_exception_frame), and we need to restore them only in case of + # kernel stack switch (because otherwise, the C handler for an exception will + # never modify them without restoring them too, so it would be useless to + # restore them here). + ld $16,(STACK_GPR+16*8)(sp) # S0 + ld $17,(STACK_GPR+17*8)(sp) # S1 + ld $18,(STACK_GPR+18*8)(sp) # S2 + ld $19,(STACK_GPR+19*8)(sp) # S3 + ld $20,(STACK_GPR+20*8)(sp) # S4 + ld $21,(STACK_GPR+21*8)(sp) # S5 + ld $22,(STACK_GPR+22*8)(sp) # S6 + ld $23,(STACK_GPR+23*8)(sp) # S7 + ld $28,(STACK_GPR+28*8)(sp) # GP + ld $30,(STACK_GPR+30*8)(sp) # FP + ldc1 $f20,(STACK_FPR+20*8)(sp) + ldc1 $f21,(STACK_FPR+21*8)(sp) + ldc1 $f22,(STACK_FPR+22*8)(sp) + ldc1 $f23,(STACK_FPR+23*8)(sp) + ldc1 $f24,(STACK_FPR+24*8)(sp) + ldc1 $f25,(STACK_FPR+25*8)(sp) + ldc1 $f26,(STACK_FPR+26*8)(sp) + ldc1 $f27,(STACK_FPR+27*8)(sp) + ldc1 $f28,(STACK_FPR+28*8)(sp) + ldc1 $f29,(STACK_FPR+29*8)(sp) + ldc1 $f30,(STACK_FPR+30*8)(sp) + ldc1 $f31,(STACK_FPR+31*8)(sp) + end_interrupt: mfc0 t0, C0_SR and t0, SR_CU1 diff --git a/src/joybus/bio_sensor.c b/src/joybus/bio_sensor.c new file mode 100644 index 000000000..2b5da4e6e --- /dev/null +++ b/src/joybus/bio_sensor.c @@ -0,0 +1,242 @@ +/** + * @file bio_sensor.c + * @author Christopher Bonhage + * @brief Bio Sensor Subsystem + * @ingroup bio_sensor + */ + +#include + +#include "bio_sensor.h" +#include "interrupt.h" +#include "joybus_commands.h" +#include "joybus_accessory_internal.h" +#include "timer.h" + +/** + * @addtogroup bio_sensor + * @{ + */ + + /** @brief One minute in microseconds */ + #define MICROS_PER_MINUTE 60000000LL + /** @brief Minimum number of beats required before calculating BPM */ +#define BIO_SENSOR_BEATS_MIN 3 +/** @brief Maximum number of heartbeat timestamps stored in the circular buffer */ +#define BIO_SENSOR_BEATS_MAX 9 + +/** @brief Convenience macro for joypad accessory type comparison */ +#define BIO_SENSOR_CONNECTED(port) \ + (joypad_get_accessory_type(port) == JOYPAD_ACCESSORY_TYPE_BIO_SENSOR) + +/** + * @brief Bio Sensor reading states + * + * Tracks the current state of heartbeat detection from the Bio Sensor. + */ +typedef enum +{ + /** @brief Sensor reading is stopped or not initialized */ + BIO_SENSOR_STATE_STOPPED = 0, + /** @brief Heart is between beats (sensor value: 0x03) */ + BIO_SENSOR_STATE_RESTING, + /** @brief Heart is actively beating (sensor value: 0x00) */ + BIO_SENSOR_STATE_PULSING, +} bio_sensor_state_t; + +/** + * @brief Bio Sensor reader context structure + * + * Maintains state and statistics for heartbeat detection on a single port. + * Uses a circular buffer to track individual beat timestamps for IBI calculation. + */ +typedef struct +{ + /** @brief Flag indicating an async read operation is in progress */ + bool read_pending; + /** @brief Current heartbeat detection state */ + bio_sensor_state_t state; + /** @brief Circular buffer storing timestamps of detected heartbeats */ + int64_t beat_timestamps[BIO_SENSOR_BEATS_MAX]; + /** @brief Current write position in the circular buffer */ + unsigned beat_cursor; + /** @brief Total number of heartbeats detected since reading started */ + unsigned beat_count; +} bio_sensor_reader_t; + +/** @brief Reference count tracking #bio_sensor_init vs #bio_sensor_close calls */ +static int bio_sensor_init_refcount = 0; + +/** @brief Array of Bio Sensor reader contexts, one per controller port */ +static volatile bio_sensor_reader_t bio_sensor_readers[JOYPAD_PORT_COUNT] = {0}; + +/** + * @brief Callback function for asynchronous Bio Sensor data reads + * + * Processes heartbeat data from the Bio Sensor accessory. Detects state transitions + * from PULSING to RESTING to count heartbeats. Maintains a rolling window of beat + * counts across multiple measurement periods for BPM calculation. + * + * @param out_dwords Pointer to the Joybus response data containing sensor readings + * @param ctx Context pointer containing the joypad port number + */ +static void bio_sensor_read_callback(uint64_t *out_dwords, void *ctx) +{ + const uint8_t *out_bytes = (void *)out_dwords; + joypad_port_t port = (joypad_port_t)ctx; + + // Extract the "N64 Accessory Read" command struct from the Joybus response + const joybus_cmd_n64_accessory_read_port_t *cmd = + (void *)&out_bytes[port + JOYBUS_COMMAND_METADATA_SIZE]; + assert(cmd->send.command == JOYBUS_COMMAND_ID_N64_ACCESSORY_READ); + + volatile bio_sensor_reader_t *reader = &bio_sensor_readers[port]; + // Ignore this read if this sensor has been stopped + if (reader->state == BIO_SENSOR_STATE_STOPPED) { return; } + + int crc_status = joybus_accessory_compare_data_crc(cmd->recv.data, cmd->recv.data_crc); + if (crc_status != JOYBUS_ACCESSORY_IO_STATUS_OK) + { + // Stop reading if the Bio Sensor has been disconnected + if (crc_status == JOYBUS_ACCESSORY_IO_STATUS_NO_PAK) + { + bio_sensor_read_stop(port); + } + // Skip this read if it fails the CRC check + reader->read_pending = false; + return; + } + + uint8_t sensor_data = cmd->recv.data[0]; + bio_sensor_state_t next_state = BIO_SENSOR_STATE_STOPPED; + if (sensor_data == 0x00) next_state = BIO_SENSOR_STATE_PULSING; + if (sensor_data == 0x03) next_state = BIO_SENSOR_STATE_RESTING; + + if ( + reader->state == BIO_SENSOR_STATE_PULSING && + next_state == BIO_SENSOR_STATE_RESTING + ) { + // Store the timestamp of this beat + int64_t now_ticks = timer_ticks(); + reader->beat_timestamps[reader->beat_cursor] = now_ticks; + reader->beat_cursor = (reader->beat_cursor + 1) % BIO_SENSOR_BEATS_MAX; + reader->beat_count++; + } + reader->state = next_state; + reader->read_pending = false; +} + +/** + * @brief VI interrupt handler for periodic Bio Sensor reads + * + * Called on every vertical interrupt (60Hz NTSC / 50Hz PAL) to initiate + * asynchronous reads from all active Bio Sensor accessories. Ensures + * continuous monitoring of heartbeat data across all controller ports. + */ +static void bio_sensor_vi_interrupt_callback(void) +{ + JOYPAD_PORT_FOREACH (port) + { + if ( + bio_sensor_readers[port].read_pending == false && + bio_sensor_readers[port].state != BIO_SENSOR_STATE_STOPPED + ) + { + if (!BIO_SENSOR_CONNECTED(port)) + { + // Stop reading if the Bio Sensor has been disconnected + bio_sensor_read_stop(port); + continue; + } + bio_sensor_readers[port].read_pending = true; + joybus_accessory_read_async( + port, JOYBUS_ACCESSORY_ADDR_BIO_PULSE, + bio_sensor_read_callback, (void *)port + ); + } + } +} + +void bio_sensor_init(void) +{ + // Just increment the refcount if already initialized + if (bio_sensor_init_refcount++ > 0) { return; } + + register_VI_handler(bio_sensor_vi_interrupt_callback); +} + +void bio_sensor_close(void) +{ + // Do nothing if there are still dangling references. + if (--bio_sensor_init_refcount > 0) { return; } + + unregister_VI_handler(bio_sensor_vi_interrupt_callback); + JOYPAD_PORT_FOREACH (port) { bio_sensor_read_stop(port); } +} + +void bio_sensor_read_start(joypad_port_t port) +{ + if (!BIO_SENSOR_CONNECTED(port)) { return; } + if (bio_sensor_readers[port].state != BIO_SENSOR_STATE_STOPPED) { return; } + volatile bio_sensor_reader_t *reader = &bio_sensor_readers[port]; + memset((void *)reader, 0, sizeof(*reader)); + reader->state = BIO_SENSOR_STATE_RESTING; +} + +void bio_sensor_read_stop(joypad_port_t port) +{ + volatile bio_sensor_reader_t *reader = &bio_sensor_readers[port]; + memset((void *)reader, 0, sizeof(*reader)); +} + +bool bio_sensor_get_active(joypad_port_t port) +{ + if (!BIO_SENSOR_CONNECTED(port)) { return false; } + return bio_sensor_readers[port].state != BIO_SENSOR_STATE_STOPPED; +} + +bool bio_sensor_get_pulsing(joypad_port_t port) +{ + if (!BIO_SENSOR_CONNECTED(port)) { return false; } + return bio_sensor_readers[port].state == BIO_SENSOR_STATE_PULSING; +} + +int bio_sensor_get_bpm(joypad_port_t port) +{ + if (!BIO_SENSOR_CONNECTED(port)) { return 0; } + volatile bio_sensor_reader_t *reader = &bio_sensor_readers[port]; + + // Need at least 3 beats to calculate 2 intervals + if (reader->beat_count < BIO_SENSOR_BEATS_MIN) { return 0; } + + // Determine how many beats to use (up to buffer size) + unsigned num_beats = reader->beat_count; + if (num_beats > BIO_SENSOR_BEATS_MAX) { + num_beats = BIO_SENSOR_BEATS_MAX; + } + int num_intervals = num_beats - 1; + + // Calculate oldest and newest beat indices + unsigned oldest_idx, newest_idx; + if (reader->beat_count <= BIO_SENSOR_BEATS_MAX) { + // Buffer hasn't wrapped yet + oldest_idx = 0; + newest_idx = num_beats - 1; + } else { + // Buffer has wrapped + oldest_idx = reader->beat_cursor; + newest_idx = (reader->beat_cursor + BIO_SENSOR_BEATS_MAX - 1) % BIO_SENSOR_BEATS_MAX; + } + + // Calculate total time span between first and last beat + int64_t time_span_ticks = reader->beat_timestamps[newest_idx] - reader->beat_timestamps[oldest_idx]; + int64_t time_span_us = TIMER_MICROS_LL(time_span_ticks); + + // Avoid division by zero and handle invalid time spans + if (time_span_us <= 0) { return 0; } + + // Calculate BPM with rounding + return (MICROS_PER_MINUTE * num_intervals + time_span_us / 2) / time_span_us; +} + +/** @} */ /* bio_sensor */ diff --git a/src/joybus/controller.c b/src/joybus/controller.c index da75f0203..f58102a5d 100644 --- a/src/joybus/controller.c +++ b/src/joybus/controller.c @@ -430,7 +430,7 @@ int get_dpad_direction( int controller ) * * @return A bitmask representing controllers present * - * @deprecated Use #joypad_is_connected or #joypad_get_identifier instead. + * @deprecated Use #joypad_is_connected or #joypad_get_style instead. */ int get_controllers_present( void ) { @@ -481,11 +481,11 @@ int get_accessories_present(struct controller_data *out) * @retval #ACCESSORY_NONE The accessory was not recognized * * @deprecated Use #joypad_get_accessory_type instead. - * For VRU/VRS devices, use #joypad_get_identifier. + * For VRU/VRS devices, use #joybus_get_identifier. */ int identify_accessory( int controller ) { - switch ( joypad_get_identifier(controller) ) + switch ( joybus_get_identifier(controller, NULL) ) { case JOYBUS_IDENTIFIER_N64_CONTROLLER: switch ( joypad_get_accessory_type(controller) ) diff --git a/src/joybus/cpak.c b/src/joybus/cpak.c new file mode 100644 index 000000000..17b50a156 --- /dev/null +++ b/src/joybus/cpak.c @@ -0,0 +1,116 @@ +/** + * @file cpak.c + * @author Giovanni Bajo + * @brief Controller Pak raw access + * @ingroup controllerpak + */ + +#include "cpak.h" +#include "joypad_accessory_internal.h" +#include +#include +#include +#ifdef N64 +#include "scratch.h" +#include "../rand_internal.h" +#else +#define scratch_malloc malloc +#define scratch_free free +#endif + +#define BLOCK_SIZE 32 ///< Size of a block in the controller pak (32 bytes) + +static int cpak_xfer(joypad_port_t port, uint8_t bank, uint16_t addr, void *data, int nbytes, joypad_accessory_xfer_t xfer) +{ + // Check validaty of the address and length. We cannot cross banks + if (addr + nbytes > 0x8000) { + errno = EINVAL; + return -1; + } + + // Switch the bank (if needed). If the cpak doesn't support bank switching at all, + // it will return JOYPAD_ACCESSORY_ERROR_CONTROLLER_PAK_BANK_SWITCH that we ignore, + // since we treat all transfers as if they were done to bank 0. + // This makes them behave just like bankswitching ones, where we can't anyway + // return an error if the bank number is wrong. + joypad_accessory_error_t err = joypad_controller_pak_set_bank(port, bank); + if (err != JOYPAD_ACCESSORY_ERROR_NONE && err != JOYPAD_ACCESSORY_ERROR_CONTROLLER_PAK_BANK_SWITCH) { + errno = EIO; // FIXME + return -1; // Bank switch failed + } + + // Perform the block transfer + if (joypad_accessory_xfer(port, xfer, addr, data, nbytes) != JOYPAD_ACCESSORY_ERROR_NONE) { + errno = EIO; // FIXME + return -1; // Transfer failed + } + + return nbytes; +} + +int cpak_read(joypad_port_t port, uint8_t bank, uint16_t address, void *buffer, size_t len) +{ + return cpak_xfer(port, bank, address, buffer, len, JOYPAD_ACCESSORY_XFER_READ); +} + +int cpak_write(joypad_port_t port, uint8_t bank, uint16_t address, const void *buffer, size_t len) +{ + return cpak_xfer(port, bank, address, (void *)buffer, len, JOYPAD_ACCESSORY_XFER_WRITE); +} + + +bool cpak_supports_bankswitching(joypad_port_t port) +{ + return joypad_controller_pak_supports_bankswitching(port); +} + +int cpak_probe_banks(joypad_port_t port) +{ + if (!cpak_supports_bankswitching(port)) { + return 1; + } + + int retcode = -1; + const int max_probe_banks = 256; + uint8_t* save_label = scratch_malloc(max_probe_banks * BLOCK_SIZE); + assert(save_label); + + // Create a random probe label that we will use to mark banks that we have already probed. + uint8_t probe_label[BLOCK_SIZE]; + __rand(probe_label, BLOCK_SIZE); + + int bnk; + for (bnk = 0; bnk < max_probe_banks; bnk++) { + + // Read the current label into the save area + if (cpak_read(port, bnk, 0, save_label + bnk * BLOCK_SIZE, BLOCK_SIZE) < 0) { + goto exit; + } + + // If the label matches the probe label, it means that an already probed + // bank was selected (no need to restore it as it doesn't exist). + if (bnk > 0 && memcmp(save_label + bnk * BLOCK_SIZE, probe_label, BLOCK_SIZE) == 0) + break; + + // Write the probe label to the current bank + if (cpak_write(port, bnk, 0, probe_label, BLOCK_SIZE) < 0) { + goto exit; + } + } + + // Return the number of banks found + retcode = bnk; + +exit: + for (int i=0; i + * @brief Controller Pak Filesystem Routines + * @ingroup controllerpak + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include "system.h" +#include "cpak.h" +#include "cpakfs.h" +#include "cpakfs_internal.h" +#include "../utils.h" +#ifdef N64 +#include "../rand_internal.h" +#include "cop0.h" +#include "debug.h" +#endif + +#define FLAG_READING (1<<0) ///< Flag to mark a file as being read +#define FLAG_WRITING (1<<1) ///< Flag to mark a file as being written +#define FLAG_NOTE_DIRTY (1<<2) ///< Flag to mark a file whose note requires being rewritten +#define FLAG_NEW_PAGES (1<<3) ///< Flag to mark a file with newly allocated pages that may need cleanup + +/** @brief A mounted cpak filesystem */ +typedef struct { + joypad_port_t port; ///< Joypad port + cpakfs_note_t notes[MAX_NOTES]; ///< Cache of all the notes + uint16_t notes_mask; ///< Bitmask of read notes + int cur_bank; ///< Current bank for multi-bank cpaks + uint64_t fat_dirty; ///< Dirty mask of FAT + int fat_size; ///< Size of the FAT in bytes + int reserved; ///< Number of reserved pages in the first bank + int8_t free_pages[MAX_BANKS]; ///< Free pages in each bank (0-62) + cpakfs_fat_entry_t fat[][NUM_PAGES];///< FAT entries +} cpakfs_t; + +/** @brief A open file in the cpakfs */ +typedef struct { + int port; ///< Joypad port + cpakfs_note_t *note; ///< Note this file is associated with + int pos; ///< Current position in the file + int size; ///< File size + cpakfs_fat_entry_t *cur_page_ptr; ///< Pointer to the current page in the FAT (or in the note) + uint8_t flags; ///< Flags +} cpakfs_openfile_t; + +/** @brief Modify the FAT and mark it as dirty */ +#define FAT_WRITE(fs, lvalue, val) ({ \ + cpakfs_fat_entry_t __val = (val); \ + cpakfs_fat_entry_t *__ptr = &(lvalue); \ + if (__ptr >= &fs->fat[0][0] && __ptr < &fs->fat[0][fs->reserved]) { \ + assertf(0, "FAT_WRITE with value %02x:%02x in reserved area", __val.bank, __val.page); \ + } \ + *__ptr = __val; \ + if (!(__ptr >= &fs->notes[0].first_page && __ptr <= &fs->notes[MAX_NOTES-1].first_page)) { \ + fs->fat_dirty |= 1 << ((__ptr - &fs->fat[0][0]) >> 7); \ + } \ +}) + +static cpakfs_t *filesystems[4]; +static char *prefixes[4]; + +void __cpakfs_fsid_checksum(cpakfs_id_t *id, uint16_t *checksum1, uint16_t *checksum2) +{ + *checksum1 = 0; + for (int i=0; i<14; i++) + *checksum1 += be16(id->data16[i]); + *checksum2 = 0xFFF2 - *checksum1; +} + +static int fsid_read(joypad_port_t port, cpakfs_id_t *id) +{ + // Check if one of the ID sectors is correct + int sectors[4] = { 0x20, 0x60, 0x80, 0xC0 }; + for (int i=0; i<4; i++) { + if (cpak_read(port, 0, sectors[i], id->data8, 32) < 0) { + return -1; + } + + // Verify that FAT size is within the valid range. There are no paks + // bigger than 2 MiB, and also we currently don't support more than + // 64 pages (because fat_dirty has 64 bits). + if ((be16(id->bank_size_msb) & 0xFF00) >= 0x4000) + continue; + + // Verify checksum + uint16_t checksum1, checksum2; + __cpakfs_fsid_checksum(id, &checksum1, &checksum2); + if (checksum1 == be16(id->checksum1) && checksum2 == be16(id->checksum2)) + return 0; + + // Special case for DexDrive: many dumps have this wrong hard-coded checksum. + // Just let them pass because the file is otherwise correct. + if (checksum1 == 0x0101 && be16(id->checksum1) == 0x0101 && be16(id->checksum2) == 0xFEFD) + return 0; + } + + // No valid ID sector found + errno = ENODEV; + return -1; +} + +/** + * @brief Compute the checksum of a FAT page, starting from a given entry index. + */ +uint8_t __cpakfs_fat_checksum(cpakfs_fat_entry_t *fat_page, int start_idx) +{ + uint8_t checksum = 0; + for (int i=start_idx; ifat_size >> 8; + const int main_addr = 0x100; + const int backup_addr = 0x100 + fs->fat_size; + + // Read the main FAT copy + if (cpak_read(fs->port, 0, main_addr, fs->fat[0], fs->fat_size) < 0) + return -1; + + for (int i=0; ireserved : 1; + if (__cpakfs_fat_checksum(fs->fat[i], num_reserved) != fs->fat[i][0].page) { + + // If the checksum is wrong, read the backup copy and check if the checksum is correct there + if (cpak_read(fs->port, 0, backup_addr + i*PAGE_SIZE, fs->fat[i], PAGE_SIZE) < 0) + return -1; + if (__cpakfs_fat_checksum(fs->fat[i], num_reserved) != fs->fat[i][0].page){ + errno = ENODEV; + return -3; + } + } + + // Set reserved pages as really reserved. Technically we could write lots + // of code to ignore their values, but it's simpler to just normalize them + // as they should be. + for (int j=0; jfat[i][j] = FAT_RESERVED; + } + + // Compute the number of free pages in this bank + for (int j=num_reserved; jfat[i][j])) + fs->free_pages[i]++; + } + } + + return 0; +} + +static int write_fat(cpakfs_t *fs) +{ + const int main_addr = 0x100; + const int backup_addr = 0x100 + fs->fat_size; + + for (int i=0; i<64 && fs->fat_dirty; i++) { + if (fs->fat_dirty & (1 << i)) { + assertf(i < (fs->fat_size >> 8), "FAT page %d is out of bounds (max %d)", i, fs->fat_size >> 8); + cpakfs_fat_entry_t *fat_page = fs->fat[i]; + tracef("Writing FAT page %d\n", i); + + // Update checksum + int csum_start_idx = i == 0 ? fs->reserved : 1; + fat_page[0].page = __cpakfs_fat_checksum(fat_page, csum_start_idx); + + // Write both copies of the FAT page + if (cpak_write(fs->port, 0, main_addr + i*PAGE_SIZE, fat_page, PAGE_SIZE) < 0) + return -1; + if (cpak_write(fs->port, 0, backup_addr + i*PAGE_SIZE, fat_page, PAGE_SIZE) < 0) + return -1; + + fs->fat_dirty &= ~(1 << i); + } + } + return 0; +} + +static uint16_t crc16(cpakfs_note_t *node) +{ + // Use Koopman polynomy (0x5935) which has HD=5 for up to 30 bytes + uint16_t crc = 0x0000; + for (size_t i = 0; i < sizeof(node->data8); ++i) { + uint16_t tmp = (uint16_t)(crc ^ ((uint16_t)node->data8[i] << 8)); + for (int b = 0; b < 8; ++b) { + if (tmp & 0x8000u) tmp = (uint16_t)((tmp << 1) ^ 0x5935); + else tmp = (uint16_t)(tmp << 1); + } + crc = tmp; + } + return crc; +} + +static cpakfs_note_t* read_note(cpakfs_t *fs, int note_id) +{ + assert(note_id >= 0 && note_id < MAX_NOTES); + cpakfs_note_t *note = &fs->notes[note_id]; + if (!(fs->notes_mask & (1 << note_id))) { + int note_start = 0x100 + fs->fat_size*2; + if (cpak_read(fs->port, 0, note_start + note_id*32, note, sizeof(cpakfs_note_t)) < 0) + return NULL; + fs->notes_mask |= 1 << note_id; + + // Sanitize filename and extension by removing all invalid characters. + // These wouldn't roundtrip to UTF-8 anyway, and they are invalid so better + // get rid of them from the get-go. + __cpakfs_n64_string_sanitize(note->filename, 16); + __cpakfs_n64_string_sanitize(note->ext, 4); + + // We store a crc16 in the note to mark it as using libdragon extension. + // Basically it's a way to record that the file was written by libdragon. + // For now, this is useful for one thing: we store the last page's padding size + // in the note, so that we know the exact size of the file. + uint16_t crc = be16(note->ext_crc16); + note->ext_crc16 = 0; // Clear CRC for comparison + if (crc16(note) != crc) { + // CRC is correct, the note is not using libdragon extensions. + // Clear the padding size field as we don't know the exact size + // so we must assume it is 0. + note->ext_padding_size = 0; + } + } + return note; +} + +/** + * @brief Find a note matching the given parsed path components + * + * Searches through all notes to find one that matches the given game code, + * publisher code, filename, and extension. + * + * @param fs Mounted filesystem + * @param path Parsed path components to search for + * @param note_id Output pointer to store the found note ID + * @return Pointer to the found note, or NULL if not found + */ +static cpakfs_note_t* find_note(cpakfs_t *fs, const cpakfs_path_t *path, int *note_id) +{ + cpakfs_note_t *note = NULL; + + // Hexdump the whole path + tracef("Searching for note:\n"); + tracef("Game code: %02x%02x%02x%02x\n", path->gamecode[0], path->gamecode[1], path->gamecode[2], path->gamecode[3]); + tracef("Publisher code: %02x%02x\n", path->pubcode[0], path->pubcode[1]); + tracef("Filename: %02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x\n", + path->filename[0], path->filename[1], path->filename[2], path->filename[3], + path->filename[4], path->filename[5], path->filename[6], path->filename[7], + path->filename[8], path->filename[9], path->filename[10], path->filename[11], + path->filename[12], path->filename[13], path->filename[14], path->filename[15]); + tracef("Extension: %02x%02x%02x%02x\n", path->ext[0], path->ext[1], path->ext[2], path->ext[3]); + + + for (int i = 0; i < MAX_NOTES; i++) { + note = read_note(fs, i); + if (!(note->status & NOTE_STATUS_OCCUPIED)) + continue; + + if (memcmp(note->gamecode, path->gamecode, 4) == 0 && + memcmp(note->pubcode, path->pubcode, 2) == 0 && + memcmp(note->filename, path->filename, 16) == 0 && + memcmp(note->ext, path->ext, 4) == 0) { + + tracef("Found note %d for path\n", i); + if (note_id) *note_id = i; + return note; + } + } + + if (note_id) *note_id = MAX_NOTES; + return NULL; +} + +static void truncate_note(cpakfs_t *fs, cpakfs_note_t *note) +{ + cpakfs_fat_entry_t *cur = ¬e->first_page; + while (cur) { + cpakfs_fat_entry_t *next = NULL; + if (FAT_IS_VALID(*cur, fs->reserved)) { + next = &FAT_NEXT(fs->fat, *cur); + fs->free_pages[cur->bank]++; + } else { + next = NULL; + } + FAT_WRITE(fs, *cur, FAT_UNUSED); + cur = next; + } + // A 0-byte note should have FAT_TERMINATOR as first page + note->first_page = FAT_TERMINATOR; +} + +static int __cpakfs_read(void *file, uint8_t *ptr, int len) +{ + cpakfs_openfile_t *f = file; + cpakfs_t *fs = filesystems[f->port]; + int read = 0; + + tracef("__cpak_read(%p, %p, %d)\n", file, ptr, len); + + if (!(f->flags & FLAG_READING)) { + tracef("__cpak_read: not reading\n"); + errno = EBADF; + return -1; + } + + if (f->pos >= f->size) + return 0; + + len = MIN(len, f->size - f->pos); + while (len > 0) { + // Perform the maximum read operation within the current page + int page_offset = f->pos % PAGE_SIZE; + int n = MIN(len, PAGE_SIZE - page_offset); + + // See if we can read multiple pages at once. This is only possible if + // they are consecutive in the filesystem. + cpakfs_fat_entry_t *last = f->cur_page_ptr; + while (n < len) { + cpakfs_fat_entry_t *next = &FAT_NEXT(fs->fat, *last); + if (next->bank != last->bank) break; + if (next->page != last->page + 1) break; + n += MIN(len-n, PAGE_SIZE); + last = next; + } + + // Perform the read + if (cpak_read(f->port, f->cur_page_ptr->bank, f->cur_page_ptr->page * PAGE_SIZE + page_offset, ptr, n) < 0) + return -1; + + // Update counters and optionally move to the next page + f->pos += n; + ptr += n; + len -= n; + read += n; + f->cur_page_ptr = last; + + if (f->pos % PAGE_SIZE == 0) { + f->cur_page_ptr = &FAT_NEXT(fs->fat, *f->cur_page_ptr); + } + } + return read; +} + +/** + * @brief Allocate one page in the filesystem for writing content to it + * + * Hardware constraints: + * - Bank switching costs one joybus operation, so we want to minimize it, that + * is, store a file within the same bank as much as possible. + * - Libdragon allows for large consecutive read operations that perform slightly + * better than single 32-byte ones (fewer context switches), so all things + * equal, it is faster for a file to be stored in consecutive pages. + * - Besides this, pages can be randomly accessed, so fragmentation is not + * a problem. + * + * This function performs a locality-first selection with random fallback. + * It works like this: + * + * 1. If the bank and page are specified, it tries to allocate the next page + * consecutive to the previous one. This allows to later read files faster + * with larger read operations. + * 2. If the bank is not specified, or the specified bank is full, it selects + * the bank with the most free pages available. Without further information, + * this gives the more likeness that a file will not be fragmented across + * too many different banks. + * 3. If the bank is specified (or was selected by the previous step), select + * a random run of unused pages in the bank, and return the first page of + * the run. + * + * @param fs Mounted filesystem + * @param bank Bank to allocate the page in if possible, or -1 if no + * preference is provided + * @param page Previous page that was written in the same file if any, + * or -1 if it's the first page of a file. + * + * @return The FAT entry of the allocated page, or FAT_RESERVED if no space is left + */ +static cpakfs_fat_entry_t allocate_page(cpakfs_t *fs, int bank, int page) +{ + // If possible, allocate linearly to speed up reads. + if (bank >= 0 && page >= 0 && page < 0x7F && FAT_IS_UNUSED(fs->fat[bank][page+1])) { + fs->free_pages[bank]--; + return (cpakfs_fat_entry_t){bank, page+1}; + } + + // If no bank affinity was specified (or the current bank is full), select + // the bank with more free pages available. + if (bank < 0 || fs->free_pages[bank] == 0) { + bank = 0; + for (int i=1; ifat_size>>8; i++) { + if (fs->free_pages[i] > fs->free_pages[bank]) + bank = i; + } + if (fs->free_pages[bank] <= 0) + return FAT_RESERVED; + } + + // The selected bank must have free pages available + assert(bank >= 0 && bank < (fs->fat_size >> 8)); + assert(fs->free_pages[bank] > 0); + + // Select a random page in the current bank + page = __randn(NUM_PAGES); + + if (FAT_IS_UNUSED(fs->fat[bank][page])) { + // If the page is part of a unused run, return the first page of the run + while (page > 0 && FAT_IS_UNUSED(fs->fat[bank][page-1])) + page--; + fs->free_pages[bank]--; + return (cpakfs_fat_entry_t){bank, page}; + } + + // Otherwise search for the first unused page starting from the current page + for (int i=0; ifat[bank][page])) { + fs->free_pages[bank]--; + return (cpakfs_fat_entry_t){bank, page}; + } + page += 1; + page %= NUM_PAGES; + } + + // This should never happen, as we already checked that the bank has at least one free page + assert(0); + return FAT_RESERVED; +} + +/** + * @brief Allocate one next page in a file. + * + * This is meant to be run on the page pointer to the terminator, so that + * it appends the next page to the chain. The new page will become the new + * terminator. + * + * @param page_ptr Pointer to the page that is the terminator + * @param fs Filesystem to allocate the page in + * @return 0 on success, -1 on error (ENOSPC) + */ +static int allocate_next_page(cpakfs_fat_entry_t *page_ptr, cpakfs_t *fs) +{ + assert(FAT_IS_TERMINATOR(*page_ptr)); + + // If the last page is not valid, allocate a new one + int num_bank = -1, num_page = -1; + if (page_ptr != &fs->notes[0].first_page) { + num_bank = (page_ptr - &fs->fat[0][0]) >> 7; + num_page = (page_ptr - &fs->fat[0][0]) & 0x7F; + } + cpakfs_fat_entry_t new_page = allocate_page(fs, num_bank, num_page); + if (FAT_IS_RESERVED(new_page)) { + tracef("No space left in the filesystem\n"); + errno = ENOSPC; + return -1; + } + tracef("Allocated next page %02x:%02x\n", new_page.bank, new_page.page); + FAT_WRITE(fs, *page_ptr, new_page); + FAT_WRITE(fs, FAT_NEXT(fs->fat, new_page), FAT_TERMINATOR); + return 0; +} + +static int __cpakfs_write(void *file, uint8_t *ptr, int len) +{ + cpakfs_openfile_t *f = file; + cpakfs_t *fs = filesystems[f->port]; + int written = 0; + + tracef("__cpak_write(%p, %p, %d) %d\n", file, ptr, len, errno); + + if (!(f->flags & FLAG_WRITING)) { + tracef("__cpak_write: not writing\n"); + errno = EBADF; + return -1; + } + + while (len > 0) { + int page_offset = f->pos % PAGE_SIZE; + int n = MIN(len, PAGE_SIZE - page_offset); + + // If the current page is not valid (eg: beginning of a file, or last + // write finished exactly at the end of a page), we need to allocate a new page. + assert(FAT_IS_VALID(*f->cur_page_ptr, fs->reserved) || FAT_IS_TERMINATOR(*f->cur_page_ptr)); + if (FAT_IS_TERMINATOR(*f->cur_page_ptr)) { + if (allocate_next_page(f->cur_page_ptr, fs) < 0) return -1; + f->flags |= FLAG_NEW_PAGES; + } + + cpakfs_fat_entry_t *last = f->cur_page_ptr; + while (n < len) { + // We will need to write more bytes, so allocate immediately one more page + cpakfs_fat_entry_t *next = &FAT_NEXT(fs->fat, *last); + if (allocate_next_page(next, fs) < 0) break; + f->flags |= FLAG_NEW_PAGES; + + // Check if it's consecutive to the current one. If so, just keep writing + // in the same large write. Otherwise, stop here: the next page is ready + // for next loop anyway. + if (next->bank != last->bank) break; + if (next->page != last->page + 1) break; + + // Add this page to the batch write + n += MIN(len - n, PAGE_SIZE); + last = next; + } + + tracef("__cpak_write: writing %d bytes to page 0x%02x%02x offset %d\n", n, f->cur_page_ptr->bank, f->cur_page_ptr->page, page_offset); + if (cpak_write(f->port, f->cur_page_ptr->bank, f->cur_page_ptr->page * PAGE_SIZE + page_offset, ptr, n) < 0) + return -1; + + f->pos += n; + ptr += n; + len -= n; + written += n; + f->cur_page_ptr = last; + + if (f->pos % PAGE_SIZE == 0) { + f->cur_page_ptr = &FAT_NEXT(fs->fat, *f->cur_page_ptr); + } + + tracef("__cpak_write: pos=%d size=%d left=%d\n", f->pos, f->size, len); + } + + // If this write increased the file size, record the new size, + // and mark the note as dirty as we need to update it with the new + // padding size. + if (f->pos > f->size) { + f->size = f->pos; + f->flags |= FLAG_NOTE_DIRTY; + } + + return written; +} + +static int __cpakfs_lseek(void *file, int offset, int whence) +{ + cpakfs_openfile_t *f = file; + cpakfs_t *fs = filesystems[f->port]; + int size = f->size; + int pos = f->pos; + + tracef("__cpak_lseek(%p, %d, %d)\n", file, offset, whence); + + switch (whence) { + case SEEK_SET: + pos = offset; + break; + case SEEK_CUR: + pos += offset; + break; + case SEEK_END: + pos = size + offset; + break; + } + + if (pos < 0) + pos = 0; + if (pos > size) + pos = size; + + // Check if the current page changed. If so, update the current page. + bool page_changed = pos / PAGE_SIZE != f->pos / PAGE_SIZE; + if (page_changed) { + int page_idx = pos / PAGE_SIZE; + f->cur_page_ptr = &f->note->first_page; + for (int i=0; icur_page_ptr, fs->reserved)) { + errno = EFTYPE; + return -1; + } + f->cur_page_ptr = &FAT_NEXT(fs->fat, *f->cur_page_ptr); + } + } + + f->pos = pos; + return pos; +} + +static int calc_size(cpakfs_t *fs, cpakfs_note_t *note) +{ + // Calculate the size of the file by traversing the FAT chain + int size = 0; + cpakfs_fat_entry_t cur_page = note->first_page; + while (!FAT_IS_TERMINATOR(cur_page)) { + if (!FAT_IS_VALID(cur_page, fs->reserved) || size > PAGE_SIZE * fs->fat_size) { // prevent infinite loop + return -1; + } + size += PAGE_SIZE; + cur_page = FAT_NEXT(fs->fat, cur_page); + } + // Subtract padding size if any (notice this is always valid, + // even without libdragon extensions, as we cleared it in that case). + return size - note->ext_padding_size; +} + + +static void *__cpakfs_open(char *name, int flags, int port) +{ + cpakfs_path_t path; + const char *error_pos; + + tracef("__cpak_open(%s, %d, %d) %d\n", name, flags, port, errno); + + // Parse the path + cpakfs_parse_err_t parse_result = cpakfs_path_parse(name, &path, &error_pos); + if (parse_result != CPAKFS_PARSE_OK) { + errno = EINVAL; + return NULL; + } + + // Find the note for this file + cpakfs_t *fs = filesystems[port]; + int note_id; + cpakfs_note_t *note = find_note(fs, &path, ¬e_id); + + if (note) { + // Can't create a file that already exists + if ((flags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL)) { + errno = EEXIST; + return NULL; + } + } else { + // Create a new note + if (!(flags & O_CREAT)) { + errno = ENOENT; + return NULL; + } + + // Find an empty note + for (note_id=0; note_idstatus & NOTE_STATUS_OCCUPIED)) { + break; + } + } + + // Too many notes + if (note_id == MAX_NOTES) { + errno = EMFILE; + return NULL; + } + } + + int mode = flags & 7; + cpakfs_openfile_t *file = malloc(sizeof(cpakfs_openfile_t)); + if (!file) { + errno = ENOMEM; + return NULL; + } + memset(file, 0, sizeof(*file)); + file->port = port; + file->note = note; + file->cur_page_ptr = ¬e->first_page; + file->flags = (mode == O_RDONLY || mode == O_RDWR) ? FLAG_READING : 0; + file->flags |= (mode == O_WRONLY || mode == O_RDWR) ? FLAG_WRITING : 0; + + // If O_APPEND is set, seek to the end of the file + if (flags & O_CREAT) { + // Free the FAT chain for this file, unless it's a new file + truncate_note(fs, file->note); + + // Populate the note + memcpy((char*)note->gamecode, path.gamecode, 4); + memcpy((char*)note->pubcode, path.pubcode, 2); + memcpy((char*)note->filename, path.filename, 16); + memcpy((char*)note->ext, path.ext, 4); + + note->status |= NOTE_STATUS_OCCUPIED; + note->first_page = FAT_TERMINATOR; + file->flags |= FLAG_NOTE_DIRTY; + } else { + // If O_TRUNC is set, truncate the existing file immediately + if (flags & O_TRUNC) { + truncate_note(fs, file->note); + file->note->first_page = FAT_TERMINATOR; + file->size = 0; + file->flags |= FLAG_NOTE_DIRTY; + } else { + // Calculate the size only if we're not truncating. + // This also serves as a check that the FAT chain is valid. + file->size = calc_size(fs, note); + if (file->size < 0) { + errno = EFTYPE; + free(file); + return NULL; + } + } + + if (flags & O_APPEND) + file->pos = file->size; + } + + return file; +} + +static int __cpakfs_close(void *file) +{ + cpakfs_openfile_t *f = file; + cpakfs_t *fs = filesystems[f->port]; + int err = 0; + + tracef("__cpak_close(%p)\n", file); + + // If we have newly allocated pages and the file doesn't end at a page boundary, + // clean up the partial last page to avoid data leakage + if ((f->flags & FLAG_NEW_PAGES) && (f->size % PAGE_SIZE) != 0) { + // Navigate to the last page by following the FAT chain to the terminator + cpakfs_fat_entry_t *last_page_ptr = &f->note->first_page; + while (!FAT_IS_TERMINATOR(FAT_NEXT(fs->fat, *last_page_ptr))) { + last_page_ptr = &FAT_NEXT(fs->fat, *last_page_ptr); + } + + // Clear the unused portion of the last page + int used_bytes = f->size % PAGE_SIZE; + int cleanup_bytes = PAGE_SIZE - used_bytes; + uint8_t zeros[PAGE_SIZE] = {0}; + if (cpak_write(f->port, last_page_ptr->bank, + last_page_ptr->page * PAGE_SIZE + used_bytes, + zeros, cleanup_bytes) < 0) { + err = -1; + } + + tracef("__cpak_close: cleaned %d bytes from last page\n", cleanup_bytes); + } + + if (f->flags & FLAG_NOTE_DIRTY) { + // Update the padding size + f->note->ext_padding_size = (f->size % PAGE_SIZE) ? PAGE_SIZE - (f->size % PAGE_SIZE) : 0; + + // Update the crc16, so that we mark + f->note->ext_crc16 = 0; + f->note->ext_crc16 = be16(crc16(f->note)); + + int note_start = 0x100 + fs->fat_size*2; + int note_id = f->note - fs->notes; + tracef("__cpak_close: writing note %d\n", note_id); + if (cpak_write(f->port, 0, note_start + note_id*32, f->note, 32) < 0) + err = -1; + } + if (f) { + tracef("__cpak_close: writing fat\n"); + if (write_fat(fs) < 0) + err = -1; + } + + free(file); + return err; +} + +static int __cpakfs_fstat(void *file, struct stat *st) +{ + cpakfs_openfile_t *f = file; + + memset(st, 0, sizeof(struct stat)); + st->st_dev = f->port; + st->st_ino = f->note - filesystems[f->port]->notes; + #ifndef __MINGW32__ + st->st_blksize = PAGE_SIZE; + st->st_blocks = (f->size + PAGE_SIZE - 1) / PAGE_SIZE; + #endif + st->st_mode = S_IFREG; + st->st_size = f->size; + + return 0; +} + +static int __cpakfs_unlink(char *name, int port) +{ + cpakfs_path_t path; + const char *error_pos; + + tracef("__cpak_unlink(%s, %d)\n", name, port); + + // Parse the path + cpakfs_parse_err_t parse_result = cpakfs_path_parse(name, &path, &error_pos); + if (parse_result != CPAKFS_PARSE_OK) { + errno = EINVAL; + return -1; + } + + // Find the note for this file + cpakfs_t *fs = filesystems[port]; + int note_id; + cpakfs_note_t *note = find_note(fs, &path, ¬e_id); + + // File not found + if (!note) { + errno = ENOENT; + return -1; + } + + // Free all pages in the FAT chain + truncate_note(fs, note); + + // Mark the note as empty + memset(note, 0, sizeof(cpakfs_note_t)); + int note_start = 0x100 + fs->fat_size*2; + if (cpak_write(port, 0, note_start + note_id*32, note, 32) < 0) + return -1; + + // Write the updated FAT to disk + if (write_fat(fs) < 0) + return -1; + + return 0; +} + +static int __cpakfs_findnext(const char *basepath, dir_t *dir, int port) { + cpakfs_t *fs = filesystems[port]; + cpakfs_note_t *note = NULL; + + // Find the next occupied note + while (++dir->d_cookie < MAX_NOTES) { + note = read_note(fs, dir->d_cookie); + + if ((note->status & NOTE_STATUS_OCCUPIED) == 0) + continue; + + // Found a valid file, format the name using the new dash format + cpakfs_path_t path; + memcpy(path.gamecode, note->gamecode, 4); + memcpy(path.pubcode, note->pubcode, 2); + memcpy(path.filename, note->filename, 16); + memcpy(path.ext, note->ext, 4); + + cpakfs_path_format(&path, dir->d_name, sizeof(dir->d_name)); + dir->d_type = DT_REG; + + // Calculate file size. If negative, ignore the error and just return the file. + // It means the file will fail to open, but we still want to return it. + dir->d_size = calc_size(fs, note); + + return 0; + } + + // No more files found + errno = ENOENT; + return -1; +} + +static int __cpakfs_findfirst(char *path, dir_t *dir, int port) { + if (!path || strcmp(path, "/") != 0) { + errno = EINVAL; + return -1; + } + + dir->d_cookie = -1; + return __cpakfs_findnext(path, dir, port); +} + +static int __cpakfs_ioctl(void *file, unsigned long request, void *arg) +{ + cpakfs_openfile_t *f = (cpakfs_openfile_t*)file; + if (!f || !f->cur_page_ptr) { + errno = EBADF; + return -1; + } + if (request == IOCPAKFS_GET_PAGE) { + uint16_t *bankpage = (uint16_t*)arg; + *bankpage = ((uint16_t)f->cur_page_ptr->bank << 8) | (f->cur_page_ptr->page & 0xFF); + return 0; + } + errno = ENOTTY; + return -1; +} + +static void *__cpakfs_open_port0(char *name, int flags) { return __cpakfs_open(name, flags, 0); } +static void *__cpakfs_open_port1(char *name, int flags) { return __cpakfs_open(name, flags, 1); } +static void *__cpakfs_open_port2(char *name, int flags) { return __cpakfs_open(name, flags, 2); } +static void *__cpakfs_open_port3(char *name, int flags) { return __cpakfs_open(name, flags, 3); } + +static int __cpakfs_findfirst_port0(char *name, dir_t *dir) { return __cpakfs_findfirst(name, dir, 0); } +static int __cpakfs_findfirst_port1(char *name, dir_t *dir) { return __cpakfs_findfirst(name, dir, 1); } +static int __cpakfs_findfirst_port2(char *name, dir_t *dir) { return __cpakfs_findfirst(name, dir, 2); } +static int __cpakfs_findfirst_port3(char *name, dir_t *dir) { return __cpakfs_findfirst(name, dir, 3); } + +static int __cpakfs_findnext_port0(const char *name, dir_t *dir) { return __cpakfs_findnext(name, dir, 0); } +static int __cpakfs_findnext_port1(const char *name, dir_t *dir) { return __cpakfs_findnext(name, dir, 1); } +static int __cpakfs_findnext_port2(const char *name, dir_t *dir) { return __cpakfs_findnext(name, dir, 2); } +static int __cpakfs_findnext_port3(const char *name, dir_t *dir) { return __cpakfs_findnext(name, dir, 3); } + +static int __cpakfs_unlink_port0(char *name) { return __cpakfs_unlink(name, 0); } +static int __cpakfs_unlink_port1(char *name) { return __cpakfs_unlink(name, 1); } +static int __cpakfs_unlink_port2(char *name) { return __cpakfs_unlink(name, 2); } +static int __cpakfs_unlink_port3(char *name) { return __cpakfs_unlink(name, 3); } + +static filesystem_t fsdef[4] = { + [0] = { + .open = __cpakfs_open_port0, + .read = __cpakfs_read, + .close = __cpakfs_close, + .fstat = __cpakfs_fstat, + .write = __cpakfs_write, + .lseek = __cpakfs_lseek, + .unlink = __cpakfs_unlink_port0, + .findfirst = __cpakfs_findfirst_port0, + .findnext2 = __cpakfs_findnext_port0, + .ioctl = __cpakfs_ioctl, + }, + [1] = { + .open = __cpakfs_open_port1, + .read = __cpakfs_read, + .close = __cpakfs_close, + .fstat = __cpakfs_fstat, + .write = __cpakfs_write, + .lseek = __cpakfs_lseek, + .unlink = __cpakfs_unlink_port1, + .findfirst = __cpakfs_findfirst_port1, + .findnext2 = __cpakfs_findnext_port1, + .ioctl = __cpakfs_ioctl, + }, + [2] = { + .open = __cpakfs_open_port2, + .read = __cpakfs_read, + .close = __cpakfs_close, + .fstat = __cpakfs_fstat, + .write = __cpakfs_write, + .lseek = __cpakfs_lseek, + .unlink = __cpakfs_unlink_port2, + .findfirst = __cpakfs_findfirst_port2, + .findnext2 = __cpakfs_findnext_port2, + .ioctl = __cpakfs_ioctl, + }, + [3] = { + .open = __cpakfs_open_port3, + .read = __cpakfs_read, + .close = __cpakfs_close, + .fstat = __cpakfs_fstat, + .write = __cpakfs_write, + .lseek = __cpakfs_lseek, + .unlink = __cpakfs_unlink_port3, + .findfirst = __cpakfs_findfirst_port3, + .findnext2 = __cpakfs_findnext_port3, + .ioctl = __cpakfs_ioctl, + }, +}; + +int cpakfs_mount(joypad_port_t port, const char *prefix) +{ + joypad_accessory_type_t type = joypad_get_accessory_type(port); + if (type != JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK) { + errno = ENODEV; + return -1; + } + + cpakfs_id_t fsid; + if (fsid_read(port, &fsid) < 0) + return -2; + + int fat_size = be16(fsid.bank_size_msb) & 0xFF00; + cpakfs_t *fs = malloc(sizeof(cpakfs_t) + fat_size); + assertf(fs != NULL, "Out of memory"); + memset(fs, 0, sizeof(cpakfs_t)); + fs->port = port; + fs->fat_size = fat_size; + fs->reserved = 1 + (fat_size >> 8) * 2 + 2; // Reserved space: sector ID, two FAT copies, note table + tracef("cpak_mount: port %d, fs size %d\n", port, (fs->fat_size >> 8) * BANK_SIZE); + + // Force a bank switch first, as we can't know the current selected bank + fs->cur_bank = -1; + + if (read_fat(fs) < 0) { + free(fs); + return -3; + } + + if (attach_filesystem(prefix, &fsdef[port]) < 0) { + free(fs); + return -4; + } + + prefixes[port] = strdup(prefix); + filesystems[port] = fs; + return 0; +} + +int cpakfs_unmount(joypad_port_t port) +{ + cpakfs_t *fs = filesystems[port]; + if (fs == NULL) { + errno = ENODEV; + return -1; + } + + if (detach_filesystem(prefixes[port]) < 0) + return -2; + + free(fs); + free(prefixes[port]); + prefixes[port] = NULL; + filesystems[port] = NULL; + return 0; +} + +int cpakfs_get_serial(joypad_port_t port, uint8_t serial[24]) +{ + joypad_accessory_type_t type = joypad_get_accessory_type(port); + if (type != JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK) { + errno = ENODEV; + return -1; + } + + cpakfs_id_t fsid; + if (fsid_read(port, &fsid) < 0) + return -2; + + memcpy(serial, fsid.serial, 24); + return 0; +} + +int cpakfs_get_stats(joypad_port_t port, cpakfs_stats_t *stats) +{ + cpakfs_t *fs = filesystems[port]; + if (fs == NULL) { + errno = ENODEV; + return -1; + } + + // Reserved space: sector ID, two FAT copies, note table + int num_banks = fs->fat_size >> 8; + int reserved_others = num_banks - 1; + + memset(stats, 0, sizeof(*stats)); + stats->num_banks = num_banks; + stats->notes.total = MAX_NOTES; + stats->pages.total = num_banks * 128 - fs->reserved - reserved_others; + + for (int i=0; istatus & NOTE_STATUS_OCCUPIED) { + stats->notes.used++; + } + } + + for (int b=0; breserved : 1; + for (int i=first_page; ifat[b][i], fs->reserved) || FAT_IS_TERMINATOR(fs->fat[b][i])) + stats->pages.used++; + } + } + + return 0; +} diff --git a/src/joybus/cpakfs_fsck.c b/src/joybus/cpakfs_fsck.c new file mode 100644 index 000000000..abccad8f5 --- /dev/null +++ b/src/joybus/cpakfs_fsck.c @@ -0,0 +1,713 @@ +/** + * @file cpakfs_fsck.c + * @author Giovanni Bajo + */ +#include +#include +#include +#include +#include +#include "cpak.h" +#include "cpakfs.h" +#ifdef N64 +#include "scratch.h" +#include "../rand_internal.h" +#else +#define scratch_calloc calloc +#define scratch_free free +#endif +#include "cpakfs_internal.h" +#include "../utils.h" + +/** @brief Filesystem check modes */ +typedef enum { + /** + * @brief Check the filesystem for errors. + * + * This function will scan the filesystem for errors and report them + * using the provided report callback. + */ + MODE_CHECK = 1, + + /** + * @brief Fix filesystem errors. + * + * This function will attempt to fix any errors found in the filesystem. + */ + MODE_FIX = 2, +} fsck_mode_; + + +/** @brief Context of a mount/fsck operation */ +typedef struct { + joypad_port_t port; ///< Joypad port number + int mode; ///< Fix filesystem problems, or just check + int nissues; ///< Number of issues found + __attribute__((format(printf, 4, 5))) + cpakfs_report_fn report; ///< Report callback for fsck issues + void *report_ctx; ///< Context to pass to the report callback +} fsck_ctx_t; + +static void fsid_new(cpakfs_id_t *id, int new_banks) +{ + memset(id, 0, sizeof(cpakfs_id_t)); + + uint16_t fs_flags = FSFLAGS_DEFAULT; + + __rand(id->serial, 16); + memcpy(id->serial+16, "DRAGON", 6); + id->serial[22] = fs_flags >> 8; + id->serial[23] = fs_flags & 0xFF; + + id->device_id_lsb = be16(0x0001); + id->bank_size_msb = be16(new_banks << 8); + uint16_t checksum1, checksum2; + __cpakfs_fsid_checksum(id, &checksum1, &checksum2); + id->checksum1 = be16(checksum1); + id->checksum2 = be16(checksum2); +} + +static int fsck_fsid(fsck_ctx_t *ctx, cpakfs_id_t *id) +{ + bool writeback = false; + + // Read all 4 sectors + int sec_idx[4] = { 0x20, 0x60, 0x80, 0xC0 }; + cpakfs_id_t sectors[4]; + for (int i=0; i<4; i++) { + if (cpak_read(ctx->port, 0, sec_idx[i], sectors[i].data8, BLOCK_SIZE) < 0) { + return -1; + } + } + + // Search for a valid ID sector (where the checksums match). + cpakfs_id_t *valid = NULL; + for (int i=0; i<4; i++) { + // Check checksum + uint16_t checksum1, checksum2; + __cpakfs_fsid_checksum(§ors[i], &checksum1, &checksum2); + if (checksum1 == be16(sectors[i].checksum1) && checksum2 == be16(sectors[i].checksum2)) { + valid = §ors[i]; + break; + } + + // Check if this is a DexDrive dump (which has a hard-coded wrong checksum) + if (checksum1 == 0x0101 && be16(sectors[i].checksum1) == 0x0101 && be16(sectors[i].checksum2) == 0xFEFD) { + valid = §ors[i]; + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_FSID_DEXDRIVE, CPAKFS_LEVEL_INFO, + "ID sector has a known-corrupted checksum generated by DexDrive"); + valid->checksum2 = be16(checksum2); + writeback = true; + break; + } + } + + if (valid == NULL) { + // No valid ID sector found. We don't know which one is valid. + ctx->nissues++; + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_FSID_CHECKSUM_FAILURE, CPAKFS_LEVEL_WARNING, + "Cannot find an ID sector with correct checksum"); + + // The only information we would need to extract is how many banks this + // cpak has. Since we cannot read the ID sector, we will try to + // probe the cpak instead. + // Notice that we do this even if we are not requested of fixing the errors, + // because we need to know how many banks the cpak has in order to + // correctly interpret the FAT. We assume the FAT might be valid even if + // the ID sector is not. + int num_banks = cpak_probe_banks(ctx->port); + if (num_banks < 0) return -1; + + // Create a valid ID sector from scratch (and actually write it back if needed). + valid = §ors[0]; + fsid_new(valid, num_banks); + writeback = true; + } else if (ctx->mode >= MODE_CHECK) { + // A valid ID sector was found. Check whether it matches all the copies + for (int i=0; i<4; i++) { + if (memcmp(valid->data8, sectors[i].data8, BLOCK_SIZE) != 0) { + // This copy is different from the valid one. If we are fixing, + // we will write back the valid copy to this sector. + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_FSID_CORRUPTED_BACKUP, CPAKFS_LEVEL_INFO, + "ID sector backup is corrupted (can be recovered from main)"); + writeback = true; + } + } + } + + // Check if the device ID is valid. This is just advisory + if (ctx->mode >= MODE_CHECK && (be16(valid->device_id_lsb) & 0x01) != 0x01) { + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_FSID_WRONG_DEVICE_ID, CPAKFS_LEVEL_INFO, + "Device ID is not valid (%04X); this is just advisory so it can be ignored", be16(valid->device_id_lsb)); + valid->device_id_lsb = be16(0x01); + writeback = true; + } + + // Check if writeback is needed. If so (and we are in fix mode), + // write the valid ID sector back to all 4 sectors. + if (writeback && ctx->mode == MODE_FIX) { + for (int i=0; i<4; i++) { + if (cpak_write(ctx->port, 0, sec_idx[i], valid->data8, BLOCK_SIZE) < 0) { + return -1; + } + } + } + + // Copy the valid ID sector to the output + memcpy(id, valid, BLOCK_SIZE); + return 0; +} + +/** + * @brief Check and fix the FAT of a cpak filesystem. + * + * @param ctx The fsck context containing the port and fix options + * @param fsid A known-valid ID sector + * @param out_fat Pointer to store the FAT data + * @return 0 on success, negative on error + */ +static int fsck_fat(fsck_ctx_t *ctx, cpakfs_id_t* fsid, cpakfs_fat_entry_t **out_fat) +{ + // Allocate memory for the FAT + int fat_size = be16(fsid->bank_size_msb) & 0xFF00; + int reserved = 1 + (fat_size >> 8) * 2 + 2; // Reserved pages: ID sector, two FAT copies, note table + cpakfs_fat_entry_t *fat = malloc(be16(fsid->bank_size_msb) & 0xFF00); + assert(fat); + int count_main_backup = 0; + uint64_t fat_dirty = 0; + int retcode = -1; + cpakfs_fat_entry_t backup[128]; + + // Read the main FAT copy in one block read + if (cpak_read(ctx->port, 0, 0x100, fat, fat_size) < 0) goto exit; + + // Verify checksum of each FAT page. + for (int i=0; iport, 0, 0x100 + fat_size + i, backup, PAGE_SIZE) < 0) goto exit; + + // While the checksum was designed to be calculated over the whole page saving + // for the checksum entry itself, in many cpak images the checksum seems to be + // computed skipping the reserved entries in the first page. + // Those entries should be 0 anyway. + int csum_start_idx = (i == 0) ? reserved : 1; + + // Check checksum. We calculate it over the whole page as expected, but if it + // fails, we also try ignoring the reserved pages in case they erroneously + // contain non-zero entries. We found cpak images around that had the + // checksum calculated like this. + bool main_csum_ok = __cpakfs_fat_checksum(fat + i/2, csum_start_idx) == fat[i/2].page; + bool backup_csum_ok = __cpakfs_fat_checksum(backup, csum_start_idx) == backup[0].page; + + if (!main_csum_ok && !backup_csum_ok) { + ctx->nissues++; + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_FAT_CHECKSUM_FAILURE, CPAKFS_LEVEL_ERROR, + "FAT page at 0x%x/0x%x has invalid checksum", 0x100+i, 0x100+fat_size+i); + + // The best we can do now is to still keep either of the copies, hoping for the best. + // We keep the page coming from the copy that is being used most. + if (count_main_backup < 0) + memcpy(fat + i/2, backup, PAGE_SIZE); + } else { + // Report unsynchronized FAT copies + if (memcmp(fat + i/2, backup, PAGE_SIZE) != 0) { + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_FAT_UNSYNCHRONIZED, CPAKFS_LEVEL_INFO, + "FAT page at 0x%x/0x%x has unsynchronized copies", 0x100+i, 0x100+fat_size+i); + + // If we are in fix mode, write back this page to synchronize the copies. + if (ctx->mode == MODE_FIX) fat_dirty |= ((uint64_t)1 << (i >> 7)); + } + + // At least one of the FAT copies is valid. Keep the good one. + if (main_csum_ok) { + count_main_backup++; + } else { + memcpy(fat + i/2, backup, PAGE_SIZE); + count_main_backup--; + } + } + } + + if (ctx->mode >= MODE_CHECK) { + int reserved = 1 + (be16(fsid->bank_size_msb) >> 8) * 2 + 2; + + for (int i=0; i < fat_size/2; i++) { + if ((i & 0x7F) == 0) // skip checksums + continue; + + if (i < reserved) { + // Entries for reserved pages should be 0 + if (!FAT_IS_RESERVED(fat[i])) { + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_FAT_INVALID_RESERVED, CPAKFS_LEVEL_INFO, + "Reserved page %d has an invalid FAT entry (0x%02x%02x)", i, fat[i].bank, fat[i].page); + fat[i] = FAT_RESERVED; + fat_dirty |= ((uint64_t)1 << (i >> 7)); + } + // If a potentially valid page is neither a valid, terminator, or unused entry, + // we need to report it as an error. + } else if (!FAT_IS_VALID(fat[i], reserved) && !FAT_IS_TERMINATOR(fat[i]) && !FAT_IS_UNUSED(fat[i])) { + ctx->nissues++; + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_FAT_INVALID_RESERVED, CPAKFS_LEVEL_ERROR, + "FAT entry for page %d is reserved but has invalid value (%02x%02x)", i, fat[i].bank, fat[i].page); + + // Since we can't know whether this is a valid page or not, we will + // mark it as unused, so that it can be allocated later. + fat[i] = FAT_UNUSED; + fat_dirty |= ((uint64_t)1 << (i >> 7)); + } + + // Check if the bank number is valid. The bank number should be lower + // than the number of banks in the pak, as recorded by the fsid. + if (FAT_IS_VALID(fat[i], reserved) && fat[i].bank >= be16(fsid->bank_size_msb) >> 8) { + ctx->nissues++; + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_FAT_INVALID_BANK, CPAKFS_LEVEL_ERROR, + "FAT entry for page %d has invalid bank number (%d)", i, fat[i].bank); + fat[i] = FAT_UNUSED; + fat_dirty |= ((uint64_t)1 << (i >> 7)); + } + } + } + + if (ctx->mode == MODE_FIX) { + for (int i=0; i<64 && fat_dirty; i++) { + if (fat_dirty & (1 << i)) { + cpakfs_fat_entry_t *fat_page = fat + i*128; + + // Update checksum + uint8_t checksum = __cpakfs_fat_checksum(fat_page, 1); + fat_page[0] = (cpakfs_fat_entry_t){0, checksum}; + + // Write both copies of the FAT page. Try writing both copies, before + // giving up, in case one of them is non writable for some hardware reason. + bool done = false; + if (cpak_write(ctx->port, 0, 0x100 + 0*fat_size + i*PAGE_SIZE, fat_page, PAGE_SIZE) >= 0) done = true; + if (cpak_write(ctx->port, 0, 0x100 + 1*fat_size + i*PAGE_SIZE, fat_page, PAGE_SIZE) >= 0) done = true; + if (!done) goto exit; + + fat_dirty &= ~(1 << i); + } + } + } + + // We have a valid or at least workable FAT. Proceed. + retcode = 0; + +exit: + if (retcode < 0) + free(fat); + else + *out_fat = fat; + return retcode; +} + +/** Maximum number of orphaned notes to track*/ +#define MAX_ORPHAN_NOTES 64 +/** Number of bitset words per bank */ +#define WORDS_PER_BANK (NUM_PAGES / 64) +/** Set a bit in a FAT bitset */ +#define BIT_SET(bitset, entry) ((bitset)[(entry).bank][(entry).page >> 6] |= (1ULL << ((entry).page & 63))) +/** Test a bit in a FAT bitset */ +#define BIT_TEST(bitset, entry) (((bitset)[(entry).bank][(entry).page >> 6] >> ((entry).page & 63)) & 1) + +static int fsck_chains(fsck_ctx_t *ctx, int num_banks, cpakfs_fat_entry_t fat[][NUM_PAGES], cpakfs_fat_entry_t roots[MAX_NOTES], int nroots) +{ + int reserved = 1 + (num_banks * 2) + 2; + + // Allocate 2D bitsets: [bank][word] where each word covers 64 pages + uint64_t (*visited)[WORDS_PER_BANK] = scratch_calloc(num_banks, sizeof(*visited)); + uint64_t (*processed)[WORDS_PER_BANK] = scratch_calloc(num_banks, sizeof(*processed)); + assert(visited && processed); + + // Phase 1: Validate existing root chains + for (int i = 0; i < nroots; i++) { + cpakfs_fat_entry_t current = roots[i]; + + while (true) { + // Check for infinite loop + if (BIT_TEST(visited, current)) { + ctx->nissues++; + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_CHAIN_INFINITE_LOOP, CPAKFS_LEVEL_ERROR, + "Chain %d has infinite loop at page %02x:%02x", i, current.bank, current.page); + break; + } + + // Mark as visited and get next entry + BIT_SET(visited, current); + cpakfs_fat_entry_t next = fat[current.bank][current.page]; + + if (FAT_IS_TERMINATOR(next)) break; + + if (!FAT_IS_VALID(next, reserved)) { + ctx->nissues++; + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_CHAIN_NO_TERMINATOR, CPAKFS_LEVEL_ERROR, + "Chain %d does not end with terminator (invalid entry %02x:%02x)", + i, next.bank, next.page); + break; + } + + current = next; + } + } + + // Phase 2: Find and reconstruct orphaned chains + cpakfs_fat_entry_t orphan_roots[MAX_ORPHAN_NOTES]; + int16_t orphan_len[MAX_ORPHAN_NOTES]; + int num_orphan_roots = 0; + + for (int bank = 0; bank < num_banks; bank++) { + for (int page = reserved; page < NUM_PAGES; page++) { + cpakfs_fat_entry_t current = {bank, page}; + + // Skip if already processed or not orphaned + if (BIT_TEST(visited, current) || BIT_TEST(processed, current) || + FAT_IS_UNUSED(fat[bank][page])) continue; + + // Found orphaned page - follow chain forward to validate and mark + cpakfs_fat_entry_t chain_start = current; + int chain_len = 0; + + while (true) { + if (BIT_TEST(processed, current)) { + // Hit already processed orphan - check if it's an existing root + for (int j = 0; j < num_orphan_roots; j++) { + if (orphan_roots[j].bank == current.bank && orphan_roots[j].page == current.page) { + // Update existing root with earlier start and new total length + orphan_roots[j] = chain_start; + orphan_len[j] = chain_len + orphan_len[j]; + goto next_page; + } + } + // Collision with another orphaned chain that doesn't have a known root + ctx->nissues++; + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_CHAIN_COLLISION, CPAKFS_LEVEL_ERROR, + "Orphaned chain at %02x:%02x collides with another orphaned chain at %02x:%02x", + chain_start.bank, chain_start.page, current.bank, current.page); + break; + } + + // Check for collision with legitimate chains + if (BIT_TEST(visited, current)) { + ctx->nissues++; + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_CHAIN_COLLISION, CPAKFS_LEVEL_ERROR, + "Orphaned chain at %02x:%02x collides with legitimate chain at %02x:%02x", + chain_start.bank, chain_start.page, current.bank, current.page); + break; + } + + BIT_SET(processed, current); + chain_len++; + + cpakfs_fat_entry_t next = fat[current.bank][current.page]; + if (FAT_IS_TERMINATOR(next)) break; + + if (!FAT_IS_VALID(next, reserved)) { + ctx->nissues++; + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_CHAIN_NO_TERMINATOR, CPAKFS_LEVEL_ERROR, + "Orphaned chain at %02x:%02x does not end with terminator", + chain_start.bank, chain_start.page); + break; + } + + current = next; + } + + // Add new orphaned root + if (num_orphan_roots < MAX_ORPHAN_NOTES) { + orphan_roots[num_orphan_roots] = chain_start; + orphan_len[num_orphan_roots] = chain_len; + num_orphan_roots++; + } + + next_page:; + } + } + + // Report orphaned chains now that they are definitively identified + for (int i = 0; i < num_orphan_roots; i++) { + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_CHAIN_ORPHANED, CPAKFS_LEVEL_WARNING, + "Orphaned chain found at %02x:%02x (length %d)", + orphan_roots[i].bank, orphan_roots[i].page, orphan_len[i]); + } + + // Phase 3: Select longest orphaned chains and append to roots + int max_to_add = MAX_NOTES - nroots; + if (max_to_add > num_orphan_roots) max_to_add = num_orphan_roots; + + // Iteratively find longest chain, add it to roots, and remove it from orphan_roots + for (int added = 0; added < max_to_add; added++) { + // Find index of longest remaining orphaned chain + int longest_idx = 0; + for (int i = 1; i < num_orphan_roots; i++) { + if (orphan_len[i] > orphan_len[longest_idx]) { + longest_idx = i; + } + } + + // Add longest chain to roots + roots[nroots++] = orphan_roots[longest_idx]; + + // Remove it by swapping with last element and decreasing count + if (longest_idx != num_orphan_roots - 1) { + SWAP(orphan_roots[longest_idx], orphan_roots[num_orphan_roots - 1]); + SWAP(orphan_len[longest_idx], orphan_len[num_orphan_roots - 1]); + } + num_orphan_roots--; + } + + scratch_free(visited); + scratch_free(processed); + return num_orphan_roots; +} + +static int fsck_notes(fsck_ctx_t *ctx, cpakfs_id_t* fsid, cpakfs_fat_entry_t fat[][NUM_PAGES]) +{ + int fat_size = be16(fsid->bank_size_msb) & 0xFF00; + int reserved = 1 + (fat_size >> 8) * 2 + 2; // Reserved pages: ID sector, two FAT copies, note table + const int note_start = 0x100 + fat_size * 2; + uint16_t note_dirty = 0; + cpakfs_fat_entry_t roots[MAX_NOTES]; int nroots = 0; + + // Read all notes + cpakfs_note_t notes[MAX_NOTES]; + if (cpak_read(ctx->port, 0, note_start, notes, sizeof(notes)) < 0) + return -1; + + for (int i=0; istatus & NOTE_STATUS_OCCUPIED) != 0; + + // Gamecode and pubcode are ASCII most of the time, but Datel Gameshark + // saves binary codes in there. So what we just consider as invalid + // is full zeros. + if (occupied && + note->gamecode[0] == 0 && note->gamecode[1] == 0 && + note->gamecode[2] == 0 && note->gamecode[3] == 0) { + ctx->nissues++; + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_NOTE_INVALID_GAMECODE, CPAKFS_LEVEL_ERROR, + "Note %d has invalid gamecode: %02x%02x%02x%02x", i, + note->gamecode[0], note->gamecode[1], + note->gamecode[2], note->gamecode[3]); + if (ctx->mode == MODE_FIX) { + memset(note, 0, sizeof(cpakfs_note_t)); + note_dirty |= (1 << i); + } + } + + if (occupied && + note->pubcode[0] == 0 && note->pubcode[1] == 0) { + ctx->nissues++; + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_NOTE_INVALID_PUBCODE, CPAKFS_LEVEL_ERROR, + "Note %d has invalid publisher code: %02x%02x", i, + note->pubcode[0], note->pubcode[1]); + if (ctx->mode == MODE_FIX) { + memset(note, 0, sizeof(cpakfs_note_t)); + note_dirty |= (1 << i); + } + } + + // Check if the first page is valid + if (occupied && !FAT_IS_VALID(note->first_page, reserved)) { + ctx->nissues++; + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_NOTE_INVALID_FIRST_PAGE, CPAKFS_LEVEL_ERROR, + "Note %d has invalid first page: %02x:%02x", i, + note->first_page.bank, note->first_page.page); + if (ctx->mode == MODE_FIX) { + // If the first page is invalid, we cannot fix it, so we just + // clear the note. + memset(note, 0, sizeof(cpakfs_note_t)); + note_dirty |= (1 << i); + } + } + + // Check if this is a broken note which is valid but doesn't have the status bit set. + if (!occupied && + // First check non empty gamecode/pubcode/filename + note->gamecode[0] != 0 && note->gamecode[1] != 0 && + note->gamecode[2] != 0 && note->gamecode[3] != 0 && + note->pubcode[0] != 0 && note->pubcode[1] != 0 && + note->filename[0] != 0 && + // Check also that the first page is valid + FAT_IS_VALID(note->first_page, reserved)) { + + // Check that the FAT chain is valid, with a terminator and doesn't loop + // (total size of the file is less than the pak size) + cpakfs_fat_entry_t current = note->first_page; + int size = 0; const int max_size = fat_size * PAGE_SIZE; + while (!FAT_IS_TERMINATOR(current)) { + size += PAGE_SIZE; + if (size >= max_size) break; + current = fat[current.bank][current.page]; + } + if (size < max_size) { + ctx->nissues++; + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_NOTE_NOT_OCCUPTED, CPAKFS_LEVEL_WARNING, + "Note %d has no status bit set but has valid gamecode, pubcode, filename and first page", i); + if (ctx->mode == MODE_FIX) { + // Fix it by setting the status bit + note->status |= NOTE_STATUS_OCCUPIED; + note_dirty |= (1 << i); + } + occupied = true; // Mark as occupied for further checks + } + } + + if (occupied) { + bool valid = true; + for (int i=0; i<16 && valid; i++) { + if (!__cpakfs_n64_string_valid(note->filename[i])) + valid = false; + } + for (int i=0; i<4 && valid; i++) { + if (!__cpakfs_n64_string_valid(note->ext[i])) + valid = false; + } + if (!valid) { + ctx->nissues++; + if (ctx->report) ctx->report(ctx->report_ctx, CPAKFS_ISSUE_NOTE_INVALID_CHARSET, CPAKFS_LEVEL_INFO, + "Note %d uses invalid/unprintable characters in filename or extension", i); + if (ctx->mode == MODE_FIX) { + __cpakfs_n64_string_sanitize(note->filename, 16); + __cpakfs_n64_string_sanitize(note->ext, 4); + note_dirty |= (1 << i); + } + } + } + + // If the note is occupied, we mark it as root, so that it is not + // invalidly detected as an orphaned FAT chain. + if (occupied && FAT_IS_VALID(note->first_page, reserved)) + roots[nroots++] = note->first_page; + } + + if (ctx->mode == MODE_FIX && note_dirty) { + for (int i=0; iport, 0, note_start + i * sizeof(cpakfs_note_t), ¬es[i], sizeof(cpakfs_note_t)) < 0) + return -1; + note_dirty &= ~(1 << i); + } + } + } + + // Check and fix chains + fsck_chains(ctx, be16(fsid->bank_size_msb) >> 8, fat, roots, nroots); + + return 0; +} + +int cpakfs_fsck(joypad_port_t port, bool fix_errors, cpakfs_report_fn report, void *report_ctx) +{ + int err; + + joypad_accessory_type_t type = joypad_get_accessory_type(port); + if (type != JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK) { + errno = ENODEV; + return -1; + } + + fsck_ctx_t ctx = { + .port = port, + .mode = fix_errors ? MODE_FIX : MODE_CHECK, + .report = report, + .report_ctx = report_ctx, + }; + + // Check and fix the sector ID + cpakfs_id_t fsid; + err = fsck_fsid(&ctx, &fsid); + if (err < 0) return err; + + // Check and fix the FAT + cpakfs_fat_entry_t *fat = NULL; + err = fsck_fat(&ctx, &fsid, &fat); + if (err < 0) return err; + + // Check and fix notes + err = fsck_notes(&ctx, &fsid, (cpakfs_fat_entry_t (*)[NUM_PAGES])fat); + if (err < 0) return err; + + free(fat); + return ctx.nissues; +} + +int cpakfs_format(joypad_port_t port, bool erase) +{ + joypad_accessory_type_t type = joypad_get_accessory_type(port); + if (type != JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK) { + errno = ENODEV; + return -1; + } + + // Detect the number of banks + int num_banks = cpak_probe_banks(port); + if (num_banks < 0) return -1; + + // Create a new fresh ID sector + cpakfs_id_t fsid; + fsid_new(&fsid, num_banks); + + // Emtpy page to write + uint8_t empty[PAGE_SIZE] = {0}; + + // Write the whole ID page. Write fsid only in the blocks that are supposed + // to contain it, otherwise just clear. + for (int i=0; i<8; i++) { + uint8_t *data = (0x5A & (1<> 8) * 2 + 2; + + for (int i=0; i 1) { + for (int i=0; i> 15, addr & 0x7FFF, empty, PAGE_SIZE) < 0) + return -1; + addr += PAGE_SIZE; + } + } + + return 0; +} diff --git a/src/joybus/cpakfs_internal.h b/src/joybus/cpakfs_internal.h new file mode 100644 index 000000000..ab321e4a5 --- /dev/null +++ b/src/joybus/cpakfs_internal.h @@ -0,0 +1,106 @@ +/** + * @file cpakfs_internal.h + * @author Giovanni Bajo + * @brief Internal Controller Pak Filesystem Routines + */ +#ifndef LIBDRAGON_CPAKFS_INTERNAL_H +#define LIBDRAGON_CPAKFS_INTERNAL_H + +#include "joypad.h" +#include "joypad_accessory_internal.h" +#include "joybus_accessory.h" +#include + +/// @cond +#ifndef N64 +#define be16(x) __builtin_bswap16(x) +#define be16i(x) (int16_t)__builtin_bswap16(x) +#define be32(x) __builtin_bswap32(x) +#else +#define be16(x) (x) +#define be16i(x) (x) +#define be32(x) (x) +#endif +/// @endcond + +/** @brief Set to 1 to activate debug logs */ +#ifndef CPAKFS_TRACE +#define CPAKFS_TRACE 0 +#endif + +/** @brief like tracef(), but writes only if #CPAKFS_TRACE is not 0 */ +#define tracef(fmt, ...) ({ if (CPAKFS_TRACE) fprintf(stderr, fmt, ##__VA_ARGS__); }) + +#define MAX_NOTES 16 ///< Maximum number of notes in a controller pak +#define MAX_BANKS 62 ///< Maximum number of banks in a controller pak +#define NUM_PAGES 128 ///< Number of pages in a bank (128 pages = 32KB) +#define BANK_SIZE 32768 ///< Size of a bank in bytes (32KB) +#define PAGE_SIZE 256 ///< Page size in bytes +#define BLOCK_SIZE 32 ///< Block size in bytes + +#define NOTE_STATUS_OCCUPIED (1<<1) ///< Flag to mark a note as occupied (not empty) + +#define FSFLAGS_DEFAULT 0x0000 ///< Default flags for a filesystem + +/** @brief FAT entry (File Allocation Table) */ +typedef struct { + uint8_t bank; ///< Bank number (0-61) + uint8_t page; ///< Page number (0-127) +} cpakfs_fat_entry_t; + +static_assert(sizeof(cpakfs_fat_entry_t) == 2, "cpakfs_fat_entry_t must be 2 bytes"); + +#define FAT_LINEAR(e) ((e).bank * NUM_PAGES + (e).page) ///< Convert a FAT entry to linear FAT index +#define FAT_NEXT(fat, e) ((fat)[(e).bank][(e).page]) ///< Get the next FAT entry for a given entry +#define FAT_IS_VALID(e, reserved) ((e).page > 0 && (e).page < 0x80 && ((e).bank != 0 || (e).page >= reserved)) ///< Check if a FAT entry is valid (point to a valid page) +#define FAT_IS_RESERVED(e) ((e).page == 0 && (e).bank == 0) ///< Check if the FAT entry marks a reserved page +#define FAT_IS_TERMINATOR(e) ((e).page == 1 && (e).bank == 0) ///< Check if a FAT entry marks the terminator of a file +#define FAT_IS_UNUSED(e) ((e).page == 3 && (e).bank == 0) ///< Check if a FAT entry marks an unused page + +#define FAT_RESERVED ((cpakfs_fat_entry_t){0, 0}) ///< Reserved FAT entry +#define FAT_TERMINATOR ((cpakfs_fat_entry_t){0, 1}) ///< Terminator FAT entry +#define FAT_UNUSED ((cpakfs_fat_entry_t){0, 3}) ///< Unused FAT entry + +/** @brief ID sector */ +typedef union { + struct { + uint8_t serial[24]; ///< Serial number + uint16_t device_id_lsb; ///< Device ID (in the LSB). Bit 0 should be 1 + uint16_t bank_size_msb; ///< Bank size (in the MSB): number of FAT entries + uint16_t checksum1; ///< Checksum of the first 14 words + uint16_t checksum2; ///< Reversed checksum of the first 14 words + }; + uint8_t data8[32]; ///< Raw data (8-bit access) + uint16_t data16[16]; ///< Raw data (16-bit access) +} cpakfs_id_t; + +/** @brief A note (similar to inode) */ +typedef union { + struct { + char gamecode[4]; ///< Game code (ASCII, 4 chars) + char pubcode[2]; ///< Publisher code (ASCII, 2 chars) + cpakfs_fat_entry_t first_page; ///< First page where data is stored + uint8_t status; ///< Status flags (bit 1: occupied) + uint8_t ext_padding_size; ///< Size of the padding in the last page + uint16_t ext_crc16; ///< CRC16 checksum (libdragon extension) + uint8_t ext[4]; ///< File extension (custom codepage) + uint8_t filename[16]; ///< Filename (custom codepage) + }; + uint8_t data8[32]; ///< Raw data (8-bit access) +} cpakfs_note_t; + +static_assert(sizeof(cpakfs_note_t) == 32, "cpakfs_note_t must be 32 bytes"); + +/** @brief Calculate the checksum of a cpak sector ID */ +void __cpakfs_fsid_checksum(cpakfs_id_t *id, uint16_t *checksum1, uint16_t *checksum2); + +/** @brief Compute the checksum of a FAT page, starting from a given entry index. */ +uint8_t __cpakfs_fat_checksum(cpakfs_fat_entry_t *fat_page, int start_idx); + +/** @brief Check whether a character is valid in the N64 codepage */ +bool __cpakfs_n64_string_valid(uint8_t ch); + +/** @brief Sanitize a string in the N64 codepage by replacing all invalid characters with NUL */ +void __cpakfs_n64_string_sanitize(uint8_t *buf, int len); + +#endif diff --git a/src/joybus/cpakfs_path.c b/src/joybus/cpakfs_path.c new file mode 100644 index 000000000..9c92352f2 --- /dev/null +++ b/src/joybus/cpakfs_path.c @@ -0,0 +1,547 @@ +/** + * @file cpakfs_path.c + * @author Giovanni Bajo + * @brief Controller Pak Filesystem Routines + * @ingroup controllerpak + */ +#include "cpakfs.h" +#include "cpakfs_internal.h" +#include +#ifdef N64 +#include "debug.h" +#endif + +/** + * @brief Check if a character is valid for gamecode/pubcode + */ +static bool is_valid_gamecode_char(char c) +{ + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); +} + +/** + * @brief Check if a character is a valid hex digit + */ +static bool is_hex_char(char c) +{ + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); +} + +/** + * @brief Convert a hex character to its numeric value + */ +static int hex_to_int(char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + return -1; +} + +/** + * @brief Parse a hex string into bytes + */ +static bool parse_hex_string(const char *hex_str, int hex_len, uint8_t *output, int output_len) +{ + if (hex_len != output_len * 2) return false; + + for (int i = 0; i < output_len; i++) { + if (!is_hex_char(hex_str[i*2]) || !is_hex_char(hex_str[i*2+1])) { + return false; + } + output[i] = (hex_to_int(hex_str[i*2]) << 4) | hex_to_int(hex_str[i*2+1]); + } + return true; +} + +/** + * @brief Calculate the actual length of a N64 codepage string + * + * The length is the number of bytes in the string, excluding zero padding. + * Internal 0x00 (NUL) characters are instead part of the string. + */ +static int n64_string_length(const uint8_t *str, int max_len) +{ + int len = max_len; + while (len > 0 && str[len - 1] == 0x00) { + len--; + } + return len; +} + +/** + * @brief Check whether a character is valid in the N64 codepage. + * + * The N64 codepage is a 8-bit codepage where not all characters are valid. + * This function checks whether the provided character is valid or not. Invalid + * characters cannot be converted to UTF-8. + */ +bool __cpakfs_n64_string_valid(uint8_t ch) +{ + return ch == 0 || (ch >= 0x0F && ch <= 0x94); +} + +/** + * @brief Sanitize a string in the N64 codepage by replacing all invalid characters with NUL + */ +void __cpakfs_n64_string_sanitize(uint8_t *buf, int len) +{ + for (int i = 0; i < len; i++) { + if (!__cpakfs_n64_string_valid(buf[i])) { + buf[i] = 0x00; + } + } +} + +static int n64_to_utf8(uint8_t c, char *out) +{ + /* Upper case letters */ + if (c >= 0x1A && c <= 0x33) { *out++ = 'A' + (c - 0x1A); return 1; } + /* Numbers */ + if (c >= 0x10 && c <= 0x19) { *out++ = '0' + (c - 0x10); return 1; } + /* Miscelaneous chart */ + switch (c) { + case 0x00: *out++ = 0x01; return 1; // we can't store 0x00 in a ASCIIZ C string, so use 0x01 instead + case 0x0F: *out++ = ' '; return 1; + case 0x34: *out++ = '!'; return 1; + case 0x35: *out++ = '\"'; return 1; + case 0x36: *out++ = '#'; return 1; + case 0x37: *out++ = '`'; return 1; + case 0x38: *out++ = '*'; return 1; + case 0x39: *out++ = '+'; return 1; + case 0x3A: *out++ = ','; return 1; + case 0x3B: *out++ = '-'; return 1; + case 0x3C: *out++ = '.'; return 1; + case 0x3D: *out++ = '/'; return 1; + case 0x3E: *out++ = ':'; return 1; + case 0x3F: *out++ = '='; return 1; + case 0x40: *out++ = '?'; return 1; + case 0x41: *out++ = '@'; return 1; + } + + /* Katakana and CJK symbols */ + if (c >= 0x42 && c <= 0x94) { + const int cjk_base = 0x3000; + static uint8_t cjk_map[83] = { 2, 155, 156, 161, 163, 165, 167, 169, 195, 227, 229, 231, 242, 243, 162, 164, 166, 168, 170, 171, 173, 175, 177, 179, 181, 183, 185, 187, 189, 191, 193, 196, 198, 200, 202, 203, 204, 205, 206, 207, 210, 213, 216, 219, 222, 223, 224, 225, 226, 228, 230, 232, 233, 234, 235, 236, 237, 239, 172, 174, 176, 178, 180, 182, 184, 186, 188, 190, 192, 194, 197, 199, 201, 208, 211, 214, 217, 220, 209, 212, 215, 218, 221 }; + uint16_t codepoint = cjk_base + cjk_map[c - 0x42]; + *out++ = 0xE0 | ((codepoint >> 12) & 0x0F); + *out++ = 0x80 | ((codepoint >> 6) & 0x3F); + *out++ = 0x80 | (codepoint & 0x3F); + return 3; + } + + assertf(0, "cpakfs: cannot convert invalid N64CP char %02X to UTF-8", c); + return 0; +} + +/* + * Function that converts a UTF-8 string (input) into a string using the cpak codepage, + * writing the output bytes into "out". The function returns the number of bytes written. + * + * The function will fail if any unsupported character is encountered, returning -1. + * If the output buffer is too small, returns -2 and sets *error_input to the position + * in the input string where conversion stopped. + */ +static int utf8_to_n64(const char *input, int in_size, uint8_t *out, int out_size, const char **error_input) { + const char *end_input = input + in_size; + const uint8_t *start_out = out; + const uint8_t *end_out = out + out_size; + + /* + * Inverse lookup table for the CJK part. + * For every possible offset (codepoint – 0x3000) from 0 to 255, + * the value is the index in the range [0, 82] to be added to 0x42. + * If an offset does not correspond to any mapped character, the value will be -1. + */ + static const int8_t inv_cjk_map[256] = { + [2] = 1, [155] = 2, [156] = 3, [161] = 4, + [163] = 5, [165] = 6, [167] = 7, [169] = 8, [195] = 9, + [227] = 10, [229] = 11, [231] = 12, [242] = 13, [243] = 14, + [162] = 15, [164] = 16, [166] = 17, [168] = 18, + [170] = 19, [171] = 20, [173] = 21, [175] = 22, + [177] = 23, [179] = 24, [181] = 25, [183] = 26, + [185] = 27, [187] = 28, [189] = 29, [191] = 30, + [193] = 31, [196] = 32, [198] = 33, [200] = 34, + [202] = 35, [203] = 36, [204] = 37, [205] = 38, + [206] = 39, [207] = 40, [210] = 41, [213] = 42, + [216] = 43, [219] = 44, [222] = 45, [223] = 46, + [224] = 47, [225] = 48, [226] = 49, [228] = 50, + [230] = 51, [232] = 52, [233] = 53, [234] = 54, + [235] = 55, [236] = 56, [237] = 57, [239] = 58, + [172] = 59, [174] = 60, [176] = 61, [178] = 62, + [180] = 63, [182] = 64, [184] = 65, [186] = 66, + [188] = 67, [190] = 68, [192] = 69, [194] = 70, + [197] = 71, [199] = 72, [201] = 73, [208] = 74, + [211] = 75, [214] = 76, [217] = 77, [220] = 78, + [209] = 79, [212] = 80, [215] = 81, [218] = 82, + [221] = 83, + }; + + /* + * Conversion loop: for each character (or UTF-8 sequence) + * we search for the corresponding byte in the n64 codepage. + */ + while (*input && input < end_input) { + // Check if we have space in output buffer + if (out >= end_out) { + if (error_input) *error_input = input; + return -2; // Buffer overflow + } + + unsigned char ch = (unsigned char)*input; + + /* Uppercase letters: 'A'-'Z' */ + if (ch >= 'A' && ch <= 'Z') { + *out++ = 0x1A + (ch - 'A'); + input++; + continue; + } + + /* Lowercase letters: 'a'-'z' - convert to uppercase */ + if (ch >= 'a' && ch <= 'z') { + *out++ = 0x1A + (ch - 'a'); + input++; + continue; + } + + /* Numbers: '0'-'9' */ + if (ch >= '0' && ch <= '9') { + *out++ = 0x10 + (ch - '0'); + input++; + continue; + } + + /* Direct handling of symbols (corresponds to the "miscellaneous" part of the n64->utf8 function) */ + if (ch < 0x80) switch (ch) { + case ' ': *out++ = 0x0F; input++; continue; + case '!': *out++ = 0x34; input++; continue; + case '"': *out++ = 0x35; input++; continue; + case '#': *out++ = 0x36; input++; continue; + case '`': *out++ = 0x37; input++; continue; + case '*': *out++ = 0x38; input++; continue; + case '+': *out++ = 0x39; input++; continue; + case ',': *out++ = 0x3A; input++; continue; + case '-': *out++ = 0x3B; input++; continue; + case '.': *out++ = 0x3C; input++; continue; + case '/': *out++ = 0x3D; input++; continue; + case ':': *out++ = 0x3E; input++; continue; + case '=': *out++ = 0x3F; input++; continue; + case '?': *out++ = 0x40; input++; continue; + case '@': *out++ = 0x41; input++; continue; + case 0x01: *out++ = 0x00; input++; continue; // special case for 0x01 + } + + /* Handling of CJK characters (3-byte UTF-8 sequences) */ + if (ch >= 0xE0) { + if ((input[0] & 0xF0) == 0xE0 && + (input[1] & 0xC0) == 0x80 && + (input[2] & 0xC0) == 0x80) + { + /* Decode the codepoint */ + uint16_t codepoint = ((input[0] & 0x0F) << 12) | + ((input[1] & 0x3F) << 6) | + (input[2] & 0x3F); + input += 3; + + /* If the codepoint is in the expected range for CJK conversion (based on 0x3000) */ + if (codepoint >= 0x3000) { + uint16_t offset = codepoint - 0x3000; + if (offset < 256) { + int8_t index = inv_cjk_map[offset]; + if (index > 0) { + *out++ = 0x42 + index - 1; + continue; + } + } + } + if (error_input) *error_input = input - 3; + return -1; + } else { + if (error_input) *error_input = input; + return -1; + } + } + if (error_input) *error_input = input; + return -1; + } + + return out - start_out; +} + +/** + * @brief Find separators and split path into segments using simple pointer array + */ +static cpakfs_parse_err_t split_path_segments(const char *input, char *segments[5], const char **error_pos) +{ + const char *p = input; + + // segments[0] = start of gamecode + segments[0] = (char*)p; + + // Find first dot (after gamecode) + while (*p && *p != '.') p++; + if (*p != '.') { + if (error_pos) *error_pos = p; + return CPAKFS_PARSE_ERR_GAMECODE_TOO_SHORT; + } + segments[1] = (char*)p; // segments[1] = end of gamecode (points to '.') + p++; // skip the dot + + // Find dash (after pubcode) + while (*p && *p != '-') p++; + if (*p != '-') { + if (error_pos) *error_pos = p; + return CPAKFS_PARSE_ERR_PUBCODE_TOO_SHORT; + } + segments[2] = (char*)p; // segments[2] = end of pubcode (points to '-') + p++; // skip the dash + + // Find last dot in a single scan - use NULL as marker for "no dot found yet" + segments[3] = NULL; // Initially no extension found + while (*p) { + if (*p == '.') segments[3] = (char*)p; // Update to point to the last dot found + p++; + } + segments[4] = (char*)p; // segments[4] = end of string (points to '\0') + + // If no dot was found, filename extends to end of string + if (!segments[3]) segments[3] = segments[4]; + + return CPAKFS_PARSE_OK; +} + +/** + * @brief Parse a code segment with unified logic using segment boundaries + */ +static cpakfs_parse_err_t parse_code_segment(char *start, char *end, uint8_t *output, + int normal_len, int hex_len, + cpakfs_parse_err_t err_too_short, + cpakfs_parse_err_t err_too_long, + cpakfs_parse_err_t err_char, + const char **error_pos) +{ + int length = end - start; + + if (length < normal_len) { + if (error_pos) *error_pos = end; + return err_too_short; + } + + if (length == normal_len) { + // Normal alphanumeric format + for (int i = 0; i < normal_len; i++) { + char c = start[i]; + if (!is_valid_gamecode_char(c)) { + if (error_pos) *error_pos = start + i; + return err_char; + } + // Convert to uppercase + output[i] = (c >= 'a' && c <= 'z') ? c - 'a' + 'A' : c; + } + return CPAKFS_PARSE_OK; + } + + if (length == hex_len) { + // Check if all characters are hex + for (int i = 0; i < hex_len; i++) { + if (!is_hex_char(start[i])) { + if (error_pos) *error_pos = start + normal_len; + return err_too_long; + } + } + // Parse as hex + if (!parse_hex_string(start, hex_len, output, normal_len)) { + if (error_pos) *error_pos = start; + return err_char; + } + return CPAKFS_PARSE_OK; + } + + // Wrong length + if (error_pos) *error_pos = start + normal_len; + return err_too_long; +} + +cpakfs_parse_err_t cpakfs_path_parse(const char *utf8_fullname, cpakfs_path_t *path, const char **error_pos) +{ + if (!utf8_fullname || !path) { + if (error_pos) *error_pos = utf8_fullname; + return CPAKFS_PARSE_ERR_GAMECODE_TOO_SHORT; + } + + // Clear the output structure + memset(path, 0, sizeof(*path)); + + // Split path into segments: + // [0]=gamecode_start, [1]=pubcode_start, [2]=filename_start, [3]=extension_start, [4]=end + char *segments[5]; + cpakfs_parse_err_t result = split_path_segments(utf8_fullname, segments, error_pos); + if (result != CPAKFS_PARSE_OK) { + return result; + } + + // Parse gamecode (segments[0] to segments[1]) + result = parse_code_segment(segments[0], segments[1], path->gamecode, 4, 8, + CPAKFS_PARSE_ERR_GAMECODE_TOO_SHORT, + CPAKFS_PARSE_ERR_GAMECODE_TOO_LONG, + CPAKFS_PARSE_ERR_GAMECODE_CHAR, + error_pos); + if (result != CPAKFS_PARSE_OK) { + return result; + } + + // Parse pubcode (segments[1]+1 to segments[2], skip the '.') + result = parse_code_segment(segments[1] + 1, segments[2], path->pubcode, 2, 4, + CPAKFS_PARSE_ERR_PUBCODE_TOO_SHORT, + CPAKFS_PARSE_ERR_PUBCODE_TOO_LONG, + CPAKFS_PARSE_ERR_PUBCODE_CHAR, + error_pos); + if (result != CPAKFS_PARSE_OK) { + return result; + } + + // Parse filename (segments[2]+1 to segments[3], skip the '-') + char *filename_start = segments[2] + 1; + char *filename_end = segments[3]; + int filename_len = filename_end - filename_start; + + if (filename_len == 0) { + if (error_pos) *error_pos = filename_start; + return CPAKFS_PARSE_ERR_FILENAME_TOO_SHORT; + } + + int n64_filename_len = utf8_to_n64(filename_start, filename_len, + path->filename, 16, error_pos); + if (n64_filename_len < 0) { + if (n64_filename_len == -2) { + return CPAKFS_PARSE_ERR_FILENAME_TOO_LONG; + } else { + return CPAKFS_PARSE_ERR_FILENAME_CHAR; + } + } + + // Parse extension if present (segments[3]+1 to segments[4], skip the '.' if any) + if (segments[3] < segments[4] && *segments[3] == '.') { + char *ext_start = segments[3] + 1; + char *ext_end = segments[4]; + int ext_len = ext_end - ext_start; + + if (ext_len > 0) { + int n64_ext_len = utf8_to_n64(ext_start, ext_len, + path->ext, 4, error_pos); + if (n64_ext_len < 0) { + if (n64_ext_len == -2) { + return CPAKFS_PARSE_ERR_EXTENSION_TOO_LONG; + } else { + return CPAKFS_PARSE_ERR_EXTENSION_CHAR; + } + } + } + } + + return CPAKFS_PARSE_OK; +} + +/** + * @brief Append a single character to output buffer with bounds checking + */ +static inline bool append_char(char **out, char *end, char c) +{ + if (*out >= end) return false; + **out = c; + (*out)++; + return true; +} + +/** + * @brief Append a hex digit to output buffer + */ +static inline bool append_hex(char **out, char *end, uint8_t value) +{ + char hex_chars[] = "0123456789ABCDEF"; + return append_char(out, end, hex_chars[value & 0xF]); +} + +/** + * @brief Format a code (gamecode/pubcode) trying ASCII first, fallback to hex + */ +static bool append_code(char **out, char *end, const uint8_t *code, int len) +{ + // Try ASCII first + bool is_ascii = true; + for (int i = 0; i < len; i++) { + if (!is_valid_gamecode_char(code[i])) { + is_ascii = false; + break; + } + } + + if (is_ascii) { + for (int i = 0; i < len; i++) { + if (!append_char(out, end, code[i])) return false; + } + } else { + for (int i = 0; i < len; i++) { + if (!append_hex(out, end, code[i] >> 4)) return false; + if (!append_hex(out, end, code[i])) return false; + } + } + return true; +} + +/** + * @brief Format a N64 string (filename/extension) to UTF-8 + */ +static bool append_n64_string(char **out, char *end, const uint8_t *str, int max_len) +{ + int len = n64_string_length(str, max_len); + for (int i = 0; i < len; i++) { + char utf8_buf[4]; + int utf8_len = n64_to_utf8(str[i], utf8_buf); + for (int j = 0; j < utf8_len; j++) { + if (!append_char(out, end, utf8_buf[j])) return false; + } + } + return true; +} + +int cpakfs_path_format(const cpakfs_path_t *path, char *utf8_fullname, int buflen) +{ + if (!path || !utf8_fullname || buflen <= 1) { + if (utf8_fullname && buflen > 0) utf8_fullname[0] = '\0'; + return -1; + } + + char *out = utf8_fullname; + char *end = utf8_fullname + buflen - 1; // Reserve space for null terminator + + if (!append_code(&out, end, path->gamecode, 4)) goto truncate; + if (!append_char(&out, end, '.')) goto truncate; + if (!append_code(&out, end, path->pubcode, 2)) goto truncate; + if (!append_char(&out, end, '-')) goto truncate; + if (!append_n64_string(&out, end, path->filename, 16)) goto truncate; + int ext_len = n64_string_length(path->ext, 4); + if (ext_len > 0) { + if (!append_char(&out, end, '.')) goto truncate; + if (!append_n64_string(&out, end, path->ext, 4)) goto truncate; + } else { + // If no extension is present but there is a dot in the filename, + // append a dot to the filename so that it is later parsed correctly. + int name_len = n64_string_length(path->filename, 16); + for (int i = 0; i < name_len; i++) { + if (path->filename[i] == 0x3C) { // dot character + if (!append_char(&out, end, '.')) goto truncate; + break; + } + } + } + + // Success - no truncation occurred + *out = '\0'; + return 0; + +truncate: + // Buffer was too small - ensure null termination and return error + *out = '\0'; + return -1; +} diff --git a/src/joybus/eeprom.c b/src/joybus/eeprom.c index 94e747423..a53168612 100644 --- a/src/joybus/eeprom.c +++ b/src/joybus/eeprom.c @@ -1,12 +1,68 @@ /** * @file eeprom.c * @author Giovanni Bajo + * @author Christopher Bonhage * @brief EEPROM support * @ingroup eeprom + * + * + * EEPROM implementation overview + * ============================== + * + * This module implements EEPROM access using a RAM write-back cache. EEPROM + * writes in fact tend to be extremely slow, while reads are quite fast. + * + * Public API remains synchronous and unchanged from the historical one, but + * persistence to hardware is decoupled from logical writes. This achieves what + * is normally called "eventual consistency". + * + * High-level model + * ---------------- + * The RAM cache is the authoritative copy seen by callers, while EEPROM + * hardware acts as a backing store flushed in background. The cache is + * allocated lazily on first EEPROM access and then populated lazily per block, + * so blocks are fetched from hardware only when they are actually needed. + * + * Read path + * --------- + * eeprom_read and eeprom_read_bytes serve data from RAM cache. When a cache + * miss happens, the module performs a synchronous single-block fetch from + * hardware. If the flusher is currently writing one block, read-miss logic + * waits for that single in-flight write to complete before fetching. + * + * Write path + * ---------- + * eeprom_write and eeprom_write_bytes immediately update cache contents and + * mark touched blocks as dirty in a bitmap. For partial writes, required + * blocks are validated first so read-modify-write semantics are preserved. + * + * Background flush + * ---------------- + * A single async state machine persists one dirty block at a time. On each + * completion, it schedules the next dirty block. Dirty selection starts from + * a randomized index to avoid always favoring low-index blocks. If a write + * fails, the block is marked dirty again and retried later. + * + * Read priority over flush + * ------------------------ + * When a read is requested while background writes are active, this module + * tries to keep blocking time low. A read miss requests temporary flush yield, + * so the write callback stops chaining while the miss is being resolved. Once + * the missing block has been fetched, flush resumes if it was active before. + * + * Busy semantics + * -------------- + * eeprom_is_busy reports whether a flush write is currently in progress. + * Hardware busy is still honored for direct synchronous read-block fetches. */ #include #include +#include "debug.h" +#include "interrupt.h" +#include "kernel/kernel_internal.h" +#include "kirq.h" +#include "../rand_internal.h" #include "eeprom.h" #include "joybus.h" #include "joybus_commands.h" @@ -16,132 +72,352 @@ static bool eeprom_maybe_busy = false; -static void eeprom_maybe_wait( void ) +/** @brief EEPROM background flushing state machine */ +typedef enum { + EEPROM_FLUSH_IDLE = 0, ///< No background flush is in progress + EEPROM_FLUSH_WRITE_BLOCK, ///< A block is being flushed asynchronously +} eeprom_flush_state_t; + +static volatile eeprom_flush_state_t eeprom_flush_state = EEPROM_FLUSH_IDLE; + +static uint8_t eeprom_flush_block = 0; ///< The block currently being flushed +static uint8_t eeprom_last_hw_status = 0x00; ///< The last hardware status byte received +static volatile bool eeprom_read_miss_pending = false; ///< Whether a read miss is pending + +static eeprom_type_t eeprom_cached_type = EEPROM_NONE; +static size_t eeprom_num_blocks = 0; +static uint8_t *eeprom_cache; +static uint8_t *eeprom_valid_bitmap; +static uint8_t *eeprom_dirty_bitmap; + +static void eeprom_flush_write_callback(uint64_t *out_dwords, void *ctx); + +/** Return one bit from a compact bitmap. */ +static inline bool bitmap_get(const uint8_t *bitmap, size_t idx) { - if ( eeprom_maybe_busy ) - { - joybus_cmd_identify_port_t cmd = { .send = { - .command = JOYBUS_COMMAND_ID_IDENTIFY, - } }; - do { joybus_exec_cmd_struct( EEPROM_PORT, cmd ); } - while ( cmd.recv.status & JOYBUS_IDENTIFY_STATUS_EEPROM_BUSY ); - eeprom_maybe_busy = false; - } + return (bitmap[idx >> 3] & (1u << (idx & 7))) != 0; } -eeprom_type_t eeprom_present( void ) +/** Set one bit in a compact bitmap. */ +static inline void bitmap_set(uint8_t *bitmap, size_t idx) { - joybus_cmd_identify_port_t cmd = { .send = { - .command = JOYBUS_COMMAND_ID_IDENTIFY, + bitmap[idx >> 3] |= (1u << (idx & 7)); +} + +/** Clear one bit in a compact bitmap. */ +static inline void bitmap_clear(uint8_t *bitmap, size_t idx) +{ + bitmap[idx >> 3] &= ~(1u << (idx & 7)); +} + +/** Send one asynchronous EEPROM block write on Joybus. */ +static void eeprom_write_block_async_hw(uint8_t block, const uint8_t *src, joybus_callback_t callback) +{ + joybus_cmd_eeprom_write_block_t cmd = { .send = { + .command = JOYBUS_COMMAND_ID_EEPROM_WRITE_BLOCK, + .block = block, } }; - joybus_exec_cmd_struct( EEPROM_PORT, cmd ); - switch( cmd.recv.identifier ) - { - case JOYBUS_IDENTIFIER_CART_EEPROM_16KBIT: return EEPROM_16K; - case JOYBUS_IDENTIFIER_CART_EEPROM_4KBIT: return EEPROM_4K; - default: return EEPROM_NONE; - } + uint8_t input[JOYBUS_BLOCK_SIZE] = {0}; + size_t i = EEPROM_PORT; + input[i++] = sizeof(cmd.send); + input[i++] = sizeof(cmd.recv); + const size_t data_offset = offsetof(typeof(cmd.send), data); + memcpy(&input[i], (void *)&cmd.send, data_offset); + memcpy(&input[i + data_offset], src, sizeof(cmd.send.data)); + i += sizeof(cmd.send) + sizeof(cmd.recv); + input[i] = 0xFE; + input[sizeof(input) - 1] = 0x01; + joybus_exec_async(input, callback, NULL); } -size_t eeprom_total_blocks( void ) +/** Block until EEPROM busy bit is cleared, if a write happened before. */ +static void eeprom_wait_ready(void) { - switch ( eeprom_present() ) - { - case EEPROM_16K: return 256; - case EEPROM_4K: return 64; - default: return 0; - } + if (!eeprom_maybe_busy) return; + + joybus_cmd_identify_port_t cmd = { .send = { + .command = JOYBUS_COMMAND_ID_IDENTIFY, + } }; + do { joybus_exec_cmd_struct(EEPROM_PORT, cmd); } + while (cmd.recv.status & JOYBUS_IDENTIFY_STATUS_EEPROM_BUSY); + eeprom_maybe_busy = false; } -void eeprom_read( uint8_t block, uint8_t * dest ) +/** Read one EEPROM block synchronously from hardware. */ +static void eeprom_read_block_hw(uint8_t block, uint8_t *dest) { - eeprom_maybe_wait(); + eeprom_wait_ready(); joybus_cmd_eeprom_read_block_t cmd = { .send = { .command = JOYBUS_COMMAND_ID_EEPROM_READ_BLOCK, .block = block, } }; - joybus_exec_cmd_struct( EEPROM_PORT, cmd ); - memcpy( dest, cmd.recv.data, EEPROM_BLOCK_SIZE ); + joybus_exec_cmd_struct(EEPROM_PORT, cmd); + memcpy(dest, cmd.recv.data, EEPROM_BLOCK_SIZE); } -uint8_t eeprom_write( uint8_t block, const uint8_t * src ) +/** Probe EEPROM type/capacity once and cache the result. */ +static void eeprom_probe_capacity_if_needed(void) { - eeprom_maybe_wait(); + if (eeprom_num_blocks) return; - joybus_cmd_eeprom_write_block_t cmd = { .send = { - .command = JOYBUS_COMMAND_ID_EEPROM_WRITE_BLOCK, - .block = block, + joybus_cmd_identify_port_t cmd = { .send = { + .command = JOYBUS_COMMAND_ID_IDENTIFY, } }; - memcpy( cmd.send.data, src, EEPROM_BLOCK_SIZE ); - joybus_exec_cmd_struct( EEPROM_PORT, cmd ); + joybus_exec_cmd_struct(EEPROM_PORT, cmd); + switch (cmd.recv.identifier) + { + case JOYBUS_IDENTIFIER_CART_EEPROM_16KBIT: + eeprom_cached_type = EEPROM_16K; + eeprom_num_blocks = 256; + break; + case JOYBUS_IDENTIFIER_CART_EEPROM_4KBIT: + eeprom_cached_type = EEPROM_4K; + eeprom_num_blocks = 64; + break; + default: + eeprom_cached_type = EEPROM_NONE; + eeprom_num_blocks = 0; + break; + } +} + +/** Lazily allocate cache and bitmaps on first EEPROM use. */ +static void eeprom_cache_alloc_if_needed(void) +{ + if (eeprom_cache) return; + + eeprom_probe_capacity_if_needed(); + if (!eeprom_num_blocks) return; + + size_t bitmap_bytes = (eeprom_num_blocks + 7) / 8; + eeprom_cache = malloc(eeprom_num_blocks * EEPROM_BLOCK_SIZE); + assertf(eeprom_cache, "Out of memory"); + eeprom_valid_bitmap = calloc(bitmap_bytes, 1); + assertf(eeprom_valid_bitmap, "Out of memory"); + eeprom_dirty_bitmap = calloc(bitmap_bytes, 1); + assertf(eeprom_dirty_bitmap, "Out of memory"); - eeprom_maybe_busy = true; - assert(cmd.recv.status == 0x00); - return cmd.recv.status; } -void eeprom_read_bytes( uint8_t * dest, size_t start, size_t len ) +/** Try to schedule the next dirty block flush. */ +static void eeprom_flush_kick(void); + +/** Find a dirty block using randomized scan start to avoid bias. */ +static int eeprom_find_next_dirty_block(void) { - size_t bytes_left = len; - uint8_t buf[EEPROM_BLOCK_SIZE]; - uint8_t current_block = start / EEPROM_BLOCK_SIZE; - // If we need to read a partial block to start off... - size_t block_offset = start % EEPROM_BLOCK_SIZE; - if (block_offset) + if (!eeprom_num_blocks) return -1; + + size_t block = __randn(eeprom_num_blocks); + for (size_t i = 0; i < eeprom_num_blocks; i++) { - eeprom_read( current_block++, buf ); - bytes_left -= (EEPROM_BLOCK_SIZE - block_offset); - while (block_offset < EEPROM_BLOCK_SIZE) - { - *dest++ = buf[block_offset++]; - } + block++; + if (block == eeprom_num_blocks) block = 0; + if (bitmap_get(eeprom_dirty_bitmap, block)) + return block; } - // Read whole blocks at a time - while ( bytes_left >= EEPROM_BLOCK_SIZE ) + return -1; +} + +/** Handle completion of one async write and chain next flush step. */ +static void eeprom_flush_write_callback(uint64_t *out_dwords, void *ctx) +{ + (void)ctx; + if (eeprom_flush_state != EEPROM_FLUSH_WRITE_BLOCK) return; + + const uint8_t *out_bytes = (void *)out_dwords; + const joybus_cmd_eeprom_write_block_t *cmd = (void *)&out_bytes[EEPROM_PORT + JOYBUS_COMMAND_METADATA_SIZE]; + eeprom_last_hw_status = cmd->recv.status; + eeprom_maybe_busy = true; + + if (cmd->recv.status != 0x00) { - eeprom_read( current_block++, dest ); - dest += EEPROM_BLOCK_SIZE; - bytes_left -= EEPROM_BLOCK_SIZE; + // Keep data pending so a later kick can retry persistence. + bitmap_set(eeprom_dirty_bitmap, eeprom_flush_block); } - // If we need to read a partial block at the end... - if (bytes_left) + + eeprom_flush_state = EEPROM_FLUSH_IDLE; + // Yield to an outstanding read miss before continuing the flush chain. + if (eeprom_read_miss_pending) return; + eeprom_flush_kick(); +} + +/** Start one async write for a dirty block (if flusher is idle). */ +static void eeprom_flush_kick(void) +{ + uint8_t block; + const uint8_t *src; + + // Atomically claim one dirty block and transition the flusher state. + disable_interrupts(); + if (eeprom_flush_state != EEPROM_FLUSH_IDLE) + { + enable_interrupts(); + return; + } + + int next_block = eeprom_find_next_dirty_block(); + if (next_block < 0) { - eeprom_read( current_block, buf ); - memcpy( dest, buf, bytes_left ); + enable_interrupts(); + return; } + + block = (uint8_t)next_block; + eeprom_flush_block = block; + bitmap_clear(eeprom_dirty_bitmap, block); + eeprom_flush_state = EEPROM_FLUSH_WRITE_BLOCK; + src = &eeprom_cache[(size_t)block * EEPROM_BLOCK_SIZE]; + enable_interrupts(); + + eeprom_write_block_async_hw( + block, + src, + eeprom_flush_write_callback + ); } -void eeprom_write_bytes( const uint8_t * src, size_t start, size_t len ) +/** Wait until there is no async write currently in-flight. */ +static void eeprom_flush_wait_idle(void) { - size_t bytes_left = len; - uint8_t buf[EEPROM_BLOCK_SIZE]; - uint8_t current_block = start / EEPROM_BLOCK_SIZE; - // If we need to write a partial block to start off... - size_t block_offset = start % EEPROM_BLOCK_SIZE; - if (block_offset) + kirq_wait_t w = kirq_begin_wait_si(); + while (eeprom_flush_state != EEPROM_FLUSH_IDLE) { - eeprom_read( current_block, buf ); - bytes_left -= (EEPROM_BLOCK_SIZE - block_offset); - while (block_offset < EEPROM_BLOCK_SIZE) - { - buf[block_offset++] = *src++; - } - eeprom_write( current_block++, buf ); + if (__kernel) kirq_wait(&w); + } +} + +/** + * Pause flush chaining while serving foreground cache misses. + * + * Returns true if a flush write was already active and had to be drained first. + */ +static bool eeprom_flush_pause(void) +{ + bool had_flush_in_progress; + disable_interrupts(); + had_flush_in_progress = (eeprom_flush_state != EEPROM_FLUSH_IDLE); + eeprom_read_miss_pending = true; + enable_interrupts(); + + // At most one block can be in-flight, wait for it once. + if (had_flush_in_progress) eeprom_flush_wait_idle(); + return had_flush_in_progress; +} + +/** Resume flush chaining after foreground cache-miss handling. */ +static void eeprom_flush_resume(bool had_flush_in_progress) +{ + disable_interrupts(); + eeprom_read_miss_pending = false; + enable_interrupts(); + + if (had_flush_in_progress) eeprom_flush_kick(); +} + +/** Ensure one block is valid in cache, fetching it lazily if needed. */ +static void eeprom_cache_ensure_block_valid(size_t block) +{ + if (bitmap_get(eeprom_valid_bitmap, block) || bitmap_get(eeprom_dirty_bitmap, block)) + return; + + eeprom_read_block_hw((uint8_t)block, &eeprom_cache[block * EEPROM_BLOCK_SIZE]); + bitmap_set(eeprom_valid_bitmap, block); +} + +eeprom_type_t eeprom_present( void ) +{ + eeprom_probe_capacity_if_needed(); + return eeprom_cached_type; +} + +size_t eeprom_total_blocks( void ) +{ + eeprom_probe_capacity_if_needed(); + return eeprom_num_blocks; +} + +void eeprom_read( uint8_t block, void * dest ) +{ + eeprom_read_bytes(dest, (size_t)block * EEPROM_BLOCK_SIZE, EEPROM_BLOCK_SIZE); +} + +uint8_t eeprom_write( uint8_t block, const void * src ) +{ + eeprom_write_bytes(src, (size_t)block * EEPROM_BLOCK_SIZE, EEPROM_BLOCK_SIZE); + return eeprom_last_hw_status; +} + +void eeprom_read_bytes( void * dest, size_t start, size_t len ) +{ + eeprom_cache_alloc_if_needed(); + assertf(eeprom_num_blocks > 0, "EEPROM not present"); + if (len == 0) return; + assertf((start + len) <= (eeprom_num_blocks * EEPROM_BLOCK_SIZE), + "EEPROM read out of bounds: start=%zu, len=%zu, total_size=%zu", + start, len, eeprom_num_blocks * EEPROM_BLOCK_SIZE); + + size_t first_block = start / EEPROM_BLOCK_SIZE; + size_t last_block = (start + len - 1) / EEPROM_BLOCK_SIZE; + bool had_flush_in_progress = eeprom_flush_pause(); + for (size_t block = first_block; block <= last_block; block++) + eeprom_cache_ensure_block_valid(block); + eeprom_flush_resume(had_flush_in_progress); + + memcpy(dest, &eeprom_cache[start], len); +} + +void eeprom_write_bytes( const void * src, size_t start, size_t len ) +{ + eeprom_cache_alloc_if_needed(); + assertf(eeprom_num_blocks > 0, "EEPROM not present"); + if (len == 0) return; + assertf((start + len) <= (eeprom_num_blocks * EEPROM_BLOCK_SIZE), + "EEPROM write out of bounds: start=%zu, len=%zu, total_size=%zu", + start, len, eeprom_num_blocks * EEPROM_BLOCK_SIZE); + + // For partial writes we need existing data to preserve untouched bytes. + size_t first_block = start / EEPROM_BLOCK_SIZE; + size_t last_block = (start + len - 1) / EEPROM_BLOCK_SIZE; + size_t first_offset = start % EEPROM_BLOCK_SIZE; + size_t end_offset = (start + len) % EEPROM_BLOCK_SIZE; + bool need_rmw_prefix = first_offset || (last_block != first_block && end_offset); + if (need_rmw_prefix) { + bool had_flush_in_progress = eeprom_flush_pause(); + if (first_offset) + eeprom_cache_ensure_block_valid(first_block); + if (last_block != first_block && end_offset) + eeprom_cache_ensure_block_valid(last_block); + eeprom_flush_resume(had_flush_in_progress); } - // Write whole blocks at a time - while (bytes_left >= EEPROM_BLOCK_SIZE) + + // Keep cache write + dirty/valid bitmap updates coherent vs flush callback. + disable_interrupts(); + memcpy(&eeprom_cache[start], src, len); + for (size_t block = first_block; block <= last_block; block++) { - memcpy( buf, src, EEPROM_BLOCK_SIZE ); - eeprom_write( current_block++, buf ); - src += EEPROM_BLOCK_SIZE; - bytes_left -= EEPROM_BLOCK_SIZE; + bitmap_set(eeprom_valid_bitmap, block); + bitmap_set(eeprom_dirty_bitmap, block); } - // If we need to write a partial block at the end... - if (bytes_left) + enable_interrupts(); + + eeprom_flush_kick(); +} + +bool eeprom_is_busy(void) +{ + bool busy; + disable_interrupts(); + busy = (eeprom_flush_state != EEPROM_FLUSH_IDLE); + enable_interrupts(); + return busy; +} + +void eeprom_wait_idle(void) +{ + kirq_wait_t w = kirq_begin_wait_si(); + while (eeprom_is_busy()) { - eeprom_read( current_block, buf ); - memcpy( buf, src, bytes_left ); - eeprom_write( current_block, buf ); + if (__kernel) kirq_wait(&w); } } diff --git a/src/joybus/eepromfs.c b/src/joybus/eepromfs.c index 37f8c13c6..ddc2a504a 100644 --- a/src/joybus/eepromfs.c +++ b/src/joybus/eepromfs.c @@ -11,6 +11,12 @@ #include "utils.h" #include "eeprom.h" #include "eepromfs.h" +#include "debug.h" + +/** @brief Flags for the file */ +#define EEPFS_FLAGS_CHECKSUM (1<<0) +/** @brief Flags for the file */ +#define EEPFS_FLAGS_BACKUP (1<<1) /** * @brief EEPROM Filesystem file descriptor. @@ -32,11 +38,13 @@ typedef struct eepfs_file_t { /** @brief File path */ - const char * const path; + const char *path; /** @brief Files must start on a block boundary */ - const size_t start_block; + uint16_t start_block; /** @brief Size of the file (in bytes) */ - const size_t num_bytes; + uint16_t num_bytes; + /** @brief Flags for the file */ + uint16_t flags; } eepfs_file_t; /** @@ -66,6 +74,9 @@ static eepfs_file_t * eepfs_files = NULL; */ static uint16_t eepfs_files_checksum = 0; +/** @brief Initial value for the CRC-16 checksum */ +#define CRC16_INIT 0xFFFF + /** * @brief Calculates a CRC-16 checksum from an array of bytes. * @@ -74,14 +85,11 @@ static uint16_t eepfs_files_checksum = 0; * * @see https://stackoverflow.com/a/23726131 */ -static uint16_t calculate_crc16(const uint8_t * data, size_t len) +static uint16_t calculate_crc16(uint16_t crc, const uint8_t * data, size_t len) { - uint8_t x; - uint16_t crc = 0xFFFF; - while ( len-- ) { - x = crc >> 8 ^ *(data++); + uint8_t x = crc >> 8 ^ *(data++); x ^= x>>4; crc = ( (crc << 8) ^ @@ -90,7 +98,6 @@ static uint16_t calculate_crc16(const uint8_t * data, size_t len) ((uint16_t)x) ); } - return crc; } @@ -243,8 +250,10 @@ int eepfs_init(const eepfs_entry_t * entries, size_t count) /* Configure a file descriptor for each entry */ for ( size_t i = 1; i < eepfs_files_count; ++i ) { + uint16_t flags = 0; file_path = entries[i - 1].path; file_size = entries[i - 1].size; + uint16_t file_total_size = file_size; /* Sanity check the file details */ if ( file_path == NULL || file_size == 0 ) @@ -253,19 +262,35 @@ int eepfs_init(const eepfs_entry_t * entries, size_t count) return EEPFS_EBADINPUT; } + /* If the file has a checksum, add 2 bytes for the checksum */ + if ( entries[i - 1].checksum ) + { + file_total_size += 2; + flags |= EEPFS_FLAGS_CHECKSUM; + } + /* If the file has a backup, add the generation count plus the copy */ + if ( entries[i - 1].backup ) + { + file_total_size += 1; + file_total_size = ROUND_UP(file_total_size, EEPROM_BLOCK_SIZE); + file_total_size *= 2; + flags |= EEPFS_FLAGS_BACKUP; + } + /* Strip the leading '/' on paths for consistency */ file_path += ( file_path[0] == '/' ); /* Create a file descriptor and copy it into the table */ const eepfs_file_t entry_file = { - file_path, - total_blocks, - file_size, + .path = file_path, + .start_block = total_blocks, + .num_bytes = file_size, + .flags = flags, }; memcpy(&eepfs_files[i], &entry_file, sizeof(entry_file)); /* Files must start on a block boundary */ - total_blocks += DIVIDE_CEIL(file_size, EEPROM_BLOCK_SIZE); + total_blocks += DIVIDE_CEIL(file_total_size, EEPROM_BLOCK_SIZE); } /* Ensure the filesystem will actually fit in available EEPROM */ @@ -276,8 +301,15 @@ int eepfs_init(const eepfs_entry_t * entries, size_t count) } /* Calculate and store the CRC-16 checksum for the declared entries */ - const size_t entries_size = sizeof(eepfs_entry_t) * count; - eepfs_files_checksum = calculate_crc16((void *)entries, entries_size); + uint16_t crc = CRC16_INIT; + for ( size_t i = 0; i < count; ++i ) + { + crc = calculate_crc16(crc, (uint8_t *)entries[i].path, strlen(entries[i].path)); + crc = calculate_crc16(crc, (uint8_t *)&entries[i].size, sizeof(entries[i].size)); + crc = calculate_crc16(crc, (uint8_t *)&entries[i].checksum, sizeof(entries[i].checksum)); + crc = calculate_crc16(crc, (uint8_t *)&entries[i].backup, sizeof(entries[i].backup)); + } + eepfs_files_checksum = crc; return EEPFS_ESUCCESS; } @@ -287,7 +319,7 @@ int eepfs_close(void) /* If eepfs was not initialized, don't do anything. */ if ( eepfs_files == NULL || eepfs_files_count == 0 ) { - return EEPFS_EBADFS; + return EEPFS_EBADINPUT; } /* Clear the file descriptor table */ @@ -299,6 +331,69 @@ int eepfs_close(void) return EEPFS_ESUCCESS; } +static int read_file(size_t start_bytes, void * dest, size_t size, int flags) +{ + uint16_t checksum, computed_checksum = CRC16_INIT; + if ( flags & EEPFS_FLAGS_CHECKSUM ) + { + eeprom_read_bytes(&checksum, start_bytes, 2); + start_bytes += 2; + } + + if ( flags & EEPFS_FLAGS_BACKUP ) + { + if ( flags & EEPFS_FLAGS_CHECKSUM ) + { + uint8_t gen; + eeprom_read_bytes(&gen, start_bytes, 1); + computed_checksum = calculate_crc16(computed_checksum, (uint8_t *)&gen, 1); + } + + start_bytes += 1; + } + + eeprom_read_bytes(dest, start_bytes, size); + + if ( flags & EEPFS_FLAGS_CHECKSUM ) + { + computed_checksum = calculate_crc16(computed_checksum, (uint8_t *)dest, size); + if ( computed_checksum != checksum ) + { + return EEPFS_CORRUPTED; + } + } + + return EEPFS_ESUCCESS; +} + +static uint8_t file_get_start(const eepfs_file_t * file, size_t * main_start_bytes, size_t * backup_start_bytes) +{ + size_t csize = file->flags & EEPFS_FLAGS_CHECKSUM ? 2 : 0; + size_t start1_bytes = file->start_block * EEPROM_BLOCK_SIZE; + size_t start2_bytes = 0; + uint8_t gen1 = 0, gen2 = 0; + + if ( file->flags & EEPFS_FLAGS_BACKUP ) + { + start2_bytes = start1_bytes + file->num_bytes + csize + 1; + start2_bytes = ROUND_UP(start2_bytes, EEPROM_BLOCK_SIZE); + + // Read the generation counts + eeprom_read_bytes(&gen1, start1_bytes + csize, 1); + eeprom_read_bytes(&gen2, start2_bytes + csize, 1); + + if ( (int8_t)(gen1 - gen2) < 0 ) + { + SWAP(start1_bytes, start2_bytes); + SWAP(gen1, gen2); + } + } + + *main_start_bytes = start1_bytes; + *backup_start_bytes = start2_bytes; + return gen1 + 1; +} + int eepfs_read(const char * path, void * dest, size_t size) { const int handle = eepfs_find_handle(path); @@ -315,10 +410,19 @@ int eepfs_read(const char * path, void * dest, size_t size) return EEPFS_EBADINPUT; } - const size_t start_bytes = file->start_block * EEPROM_BLOCK_SIZE; - eeprom_read_bytes(dest, start_bytes, file->num_bytes); + /* Get the start bytes for the main and backup copies */ + size_t main_start_bytes, backup_start_bytes; + file_get_start(file, &main_start_bytes, &backup_start_bytes); - return EEPFS_ESUCCESS; + /* Read the main copy */ + if ( read_file(main_start_bytes, dest, size, file->flags) == EEPFS_ESUCCESS ) + return EEPFS_ESUCCESS; + /* Read the backup copy if it exists */ + if ( backup_start_bytes != 0 && read_file(backup_start_bytes, dest, size, file->flags) == EEPFS_ESUCCESS ) + return EEPFS_ESUCCESS; + + /* If both copies are corrupted, return an error */ + return EEPFS_CORRUPTED; } int eepfs_write(const char * path, const void * src, size_t size) @@ -337,8 +441,31 @@ int eepfs_write(const char * path, const void * src, size_t size) return EEPFS_EBADINPUT; } - const size_t start_bytes = file->start_block * EEPROM_BLOCK_SIZE; - eeprom_write_bytes(src, start_bytes, file->num_bytes); + /* Get the start bytes for the main and backup copies */ + size_t main_start_bytes, backup_start_bytes; + uint8_t next_gen = file_get_start(file, &main_start_bytes, &backup_start_bytes); + + if (backup_start_bytes == 0) + backup_start_bytes = main_start_bytes; + + /* Prepare the header bytes */ + uint8_t header_bytes[3]; int header_count = 0; + + if (file->flags & EEPFS_FLAGS_CHECKSUM) + { + uint16_t checksum = CRC16_INIT; + if ( file->flags & EEPFS_FLAGS_BACKUP ) + checksum = calculate_crc16(checksum, (uint8_t *)&next_gen, 1); + checksum = calculate_crc16(checksum, (uint8_t *)src, size); + header_bytes[header_count++] = (uint8_t)(checksum >> 8); + header_bytes[header_count++] = (uint8_t)(checksum & 0xFF); + } + + if (file->flags & EEPFS_FLAGS_BACKUP) + header_bytes[header_count++] = next_gen; + + eeprom_write_bytes(src, backup_start_bytes + header_count, file->num_bytes); + eeprom_write_bytes(header_bytes, backup_start_bytes, header_count); return EEPFS_ESUCCESS; } @@ -354,6 +481,17 @@ int eepfs_erase(const char * path) return EEPFS_ENOFILE; } + /* Calculate the total number of blocks to erase */ + int num_bytes = file->num_bytes; + if (file->flags & EEPFS_FLAGS_CHECKSUM) + num_bytes += 2; + if (file->flags & EEPFS_FLAGS_BACKUP) + { + num_bytes += 1; + num_bytes = ROUND_UP(num_bytes, EEPROM_BLOCK_SIZE); + num_bytes *= 2; + } + const size_t num_blocks = DIVIDE_CEIL(file->num_bytes, EEPROM_BLOCK_SIZE); size_t current_block = file->start_block; diff --git a/src/joybus/joybus.c b/src/joybus/joybus.c index 5eb5a0999..94eaf59a5 100644 --- a/src/joybus/joybus.c +++ b/src/joybus/joybus.c @@ -10,12 +10,16 @@ #include #include "debug.h" +#include "timer.h" #include "interrupt.h" #include "joybus.h" -#include "joybus_internal.h" +#include "joybus_commands.h" #include "n64sys.h" #include "mi.h" +#include "kernel/kernel_internal.h" #include "regsinternal.h" +#include "kirq.h" +#include "accounting_internal.h" /** * @name SI status register bit definitions @@ -43,11 +47,12 @@ static void * const PIF_RAM = (void *)0x1fc007c0; */ typedef struct { uint64_t input[JOYBUS_BLOCK_DWORDS] __attribute__((aligned(16))); ///< input message - void (*callback)(uint64_t *output, void *ctx); ///< callback for completion + joybus_callback_t callback; ///< callback for completion void *context; ///< callback context } joybus_msg_t; -#define MAX_JOYBUS_MSGS 8 ///< Maximum number of pending joybus messages +#define MAX_JOYBUS_MSGS 8 ///< Maximum number of pending joybus messages +#define MAX_JOYBUS_DETECTION_CALLBACKS 8 ///< Maximum number of registered detection callbacks /** * @anchor JOYBUS_STATE @@ -70,33 +75,57 @@ static joybus_msg_t joybus_msgs[MAX_JOYBUS_MSGS]; static volatile int msgs_widx; /** @brief Pending messages read index */ static volatile int msgs_ridx; +/** @brief Init counter */ +static int joybus_init_count = 0; +/** @brief Are we in the middle of identifying the devices? */ +static volatile bool joybus_identify_pending = false; +/** @brief Is the cached Joybus input block for identifying Joypads valid? */ +static volatile uint8_t joybus_identify_input_valid = false; +/** @brief Cached Joybus input block for identifying all Joypads. */ +static volatile uint8_t joybus_identify_input[JOYBUS_BLOCK_SIZE] = {0}; +/** @brief Joybus identifiers for each port. */ +volatile joybus_identifier_t joybus_identifiers_hot[JOYBUS_PORT_COUNT] = {0}; +/** @brief Joybus identify status bytes for each port. */ +volatile uint8_t joybus_identify_status_hot[JOYBUS_PORT_COUNT] = {0}; +/** @brief Timer used to periodically identify the devices. */ +static timer_link_t *identify_timer = NULL; + +/** + * @brief Number of ticks between Joybus identify commands. + * + * During VI interrupt, the Joypad subsystem will periodically re-identify + * the connected devices to check if the identifier has changed or if any + * accessories have been connected/disconnected. + */ +#define JOYBUS_IDENTIFY_INTERVAL_TICKS (1 * TICKS_PER_SECOND) + +/** @brief Detection callback entry */ +typedef struct { + joybus_detection_callback_t callback; ///< callback function + void *ctx; ///< callback context +} detection_entry_t; + +/** @brief Callbacks for detection events */ +static detection_entry_t detection_callbacks[MAX_JOYBUS_DETECTION_CALLBACKS]; static void si_interrupt(void); /** * @brief Initialize the joybus subsystem + * + * NOTE: historically, the low-level part of the joybus subsystem (eg: joybus_exec) + * has always worked without an init function. So even if a proper #joybus_init + * exists now, we still need to initialize the low-level part of the subsystem + * automatically before the first use. This is done via a constructor function. */ __attribute__((constructor)) void __joybus_init(void) { - // FIXME: this constructor requires the __init_interrupts constructor to be - // already run. Since we are not 100% sure of how GCC handles constructor - // ordering, we call it explicitly here (there's no harm in calling it - // multiple times anyway). Revisit this after gathering more information - // on constructor ordering. - extern void __init_interrupts(void); - __init_interrupts(); - // Initialize the message ring buffer msgs_widx = 0; msgs_ridx = 0; joybus_state = JOYBUS_STATE_IDLE; - // Wait for any pending SI write. This can happen mainly because of the - // write made by entrypoint.S to complete the PIF boot. If the write is still - // pending, it would trigger a SI interrupt later and cause a crash. - while (SI_regs->status & (SI_STATUS_DMA_BUSY | SI_STATUS_IO_BUSY)) {} - // Acknowledge any pending SI interrupt SI_regs->status = 0; @@ -195,36 +224,7 @@ static void si_interrupt(void) { } } -/** - * @brief Execute an asynchronous joybus message. - * - * This function executes an asynchronous joybus protocol exchange, sending - * a message block (input), and receiving a reply (output). The message is - * sent in background and a completion function "callback" is called when - * the output is ready to be processed. - * - * It is possible to schedule multiple joybus messages by calling this - * function multiple times. They will be automatically executed in order. - * The maximum number of pending messages at any given time is #MAX_JOYBUS_MSGS. - * - * @note The callback function will be called under interrupt. - * - * @note This function is not part of the public API yet because we want - * to evaluate the use of threading rather than asynchronous programming. - * It should only be used internally without exposing a asynchronous - * API externally. - * - * @param[in] input The input block (must be of JOYBUS_BLOCK_SIZE bytes). - * No specific alignment is required for this data block. - * @param[in] callback A callback completion function that will be called - * when the joybus command is finished. The function - * will receive a pointer to the output buffer and the - * opaque pointer to the callback's context. - * Can be NULL if no callback is required. - * @param[in] ctx Context opaque pointer to pass to the callback. - * Can be NULL if no context is required. - */ -void joybus_exec_async(const void * input, void (*callback)(uint64_t *output, void *ctx), void *ctx) +void joybus_exec_async(const void * input, joybus_callback_t callback, void *ctx) { // Make sure that the task queue is not full. If it is, just assert for now. // It is not easy to understand what we should do when the queue is full; @@ -259,17 +259,255 @@ void joybus_exec( const void * input, void * output ) done = true; } + kirq_wait_t w = kirq_begin_wait_si(); + joybus_exec_async(input, callback, NULL); - while (!done) { - // We want the blocking function to also work with interrupts disabled. - // So while we spin loop, poll SI interrupts manually in case they - // are disabled. - disable_interrupts(); - unsigned long status = *MI_INTERRUPT & *MI_MASK; - if (status & MI_INTERRUPT_SI) { - SI_regs->status = 0; // clear interrupt - si_interrupt(); + + ACCT_SCOPE(ACCT_CAT_JOYBUS) while (!done) { + if (__kernel) { + kirq_wait(&w); + } else { + // We want the blocking function to also work with interrupts disabled. + // So while we spin loop, poll SI interrupts manually in case they + // are disabled. + disable_interrupts(); + unsigned long status = *MI_INTERRUPT & *MI_MASK; + if (status & MI_INTERRUPT_SI) { + SI_regs->status = 0; // clear interrupt + si_interrupt(); + } + enable_interrupts(); + } + } +} + +static void send_detection_event(joybus_identifier_t identifier, joybus_detection_event_t event, int port, uint8_t device_status) +{ + for (int i = 0; i < MAX_JOYBUS_DETECTION_CALLBACKS; i++) { + if (detection_callbacks[i].callback) { + detection_callbacks[i].callback(identifier, event, port, device_status, detection_callbacks[i].ctx); + } + } +} + +void joybus_register_detection_callback(joybus_detection_callback_t callback, void *ctx) +{ + disable_interrupts(); + for (int i = 0; i < MAX_JOYBUS_DETECTION_CALLBACKS; i++) { + if (detection_callbacks[i].callback == NULL) { + detection_callbacks[i].callback = callback; + detection_callbacks[i].ctx = ctx; + + // Notify the callback of all currently connected devices + for (int port = 0; port < JOYBUS_PORT_COUNT; port++) { + joybus_identifier_t identifier = joybus_identifiers_hot[port]; + uint8_t device_status = joybus_identify_status_hot[port]; + if (identifier != JOYBUS_IDENTIFIER_NONE) { + callback(identifier, JOYBUS_DETECT_CONNECTED, port, device_status, ctx); + } + } + + enable_interrupts(); + return; } - enable_interrupts(); } + assertf(false, "Too many detection callbacks registered"); + enable_interrupts(); } + +void joybus_unregister_detection_callback(joybus_detection_callback_t callback, void *ctx) +{ + disable_interrupts(); + for (int i = 0; i < MAX_JOYBUS_DETECTION_CALLBACKS; i++) { + if (detection_callbacks[i].callback == callback && detection_callbacks[i].ctx == ctx) { + detection_callbacks[i].callback = NULL; + detection_callbacks[i].ctx = NULL; + enable_interrupts(); + return; + } + } + assertf(false, "Callback was not registered"); + enable_interrupts(); +} + +static void joybus_input_identify( uint8_t input[JOYBUS_BLOCK_SIZE], bool reset ) +{ + const joybus_cmd_identify_port_t cmd = { .send = { + .command = reset ? JOYBUS_COMMAND_ID_RESET : JOYBUS_COMMAND_ID_IDENTIFY, + } }; + const size_t recv_offset = offsetof(typeof(cmd), recv); + size_t i = 0; + + // Populate the Joybus commands on each port + memset(input, 0x00, JOYBUS_BLOCK_SIZE); + for (int port = 0; port < JOYBUS_PORT_COUNT-1; port++) + { + // iQue PIF requires a NOP (0xFF) before each command + if (sys_bbplayer()) input[i++] = 0xFF; + // Set the command metadata + input[i++] = sizeof(cmd.send); + input[i++] = sizeof(cmd.recv); + // Micro-optimization: Minimize copy length + memcpy(&input[i], &cmd, recv_offset); + i += sizeof(cmd); + // iQue PIF requires commands to be 8-byte aligned + if (sys_bbplayer()) while (i & 7) input[i++] = 0xFF; + } + + // Close out the Joybus operation block + input[i] = 0xFE; + input[JOYBUS_BLOCK_SIZE - 1] = 0x01; +} + +/** + * @brief Callback for identifying Joypads. + * + * @param[in] out_dwords Joybus output block. + * @param[in,out] ctx Not used. + */ +static void joybus_identify_callback(uint64_t *out_dwords, void *ctx) +{ + const uint8_t *out_bytes = (void *)out_dwords; + const joybus_cmd_identify_port_t *cmd; + size_t i = 0; + + for (int port = 0; port < JOYBUS_PORT_COUNT; port++) + { + if (sys_bbplayer()) { + // iQue has a very fixed layout for commands, and it also tends + // to corrupt other parts of PIF-RAM. So better jump to fixed positions + // while parsing. + if (port == 4) break; + i = (port * 8) + 1; + } + + volatile joybus_identifier_t *old_identifier = &joybus_identifiers_hot[port]; + volatile uint8_t *old_status = &joybus_identify_status_hot[port]; + + cmd = (void *)&out_bytes[i + JOYBUS_COMMAND_METADATA_SIZE]; + + joybus_identifier_t new_identifier = cmd->recv.identifier; + uint8_t new_status = cmd->recv.status; + if (out_bytes[i+1] & 0x80) { + // If the error flag is set, no device is connected here + new_identifier = JOYBUS_IDENTIFIER_NONE; + new_status = 0; + } + + // Now check if the identifier has changed since last time + if (new_identifier != *old_identifier) + { + if (*old_identifier != JOYBUS_IDENTIFIER_NONE) + send_detection_event(*old_identifier, JOYBUS_DETECT_DISCONNECTED, port, *old_status); + if (new_identifier != JOYBUS_IDENTIFIER_NONE) + send_detection_event(new_identifier, JOYBUS_DETECT_CONNECTED, port, new_status); + } + else if (new_identifier != JOYBUS_IDENTIFIER_NONE) + { + send_detection_event(new_identifier, JOYBUS_DETECT_POLLED, port, new_status); + } + + *old_identifier = new_identifier; + *old_status = new_status; + i += JOYBUS_COMMAND_METADATA_SIZE + sizeof(*cmd); + } + + joybus_identify_pending = false; +} + +/** + * @brief Identify Joypads asynchronously. + * + * @param reset Whether to reset the devices. + */ +static void joybus_identify_async(bool reset) +{ + // Async operations are disabled during reset + if( exception_reset_time() > 0 ) { return; } + + // Bail if this operation is already in-progress + if (joybus_identify_pending) { return; } + joybus_identify_pending = true; + + uint8_t * const input = (void *)joybus_identify_input; + // Reset invalidates the cached input block + if (!joybus_identify_input_valid || reset) + { + joybus_input_identify(input, reset); + // Identify is more common than reset, so don't cache resets + joybus_identify_input_valid = !reset; + } + + joybus_exec_async(input, joybus_identify_callback, NULL); +} + +static void joybus_identify_wait(void) +{ + kirq_wait_t w = kirq_begin_wait_si(); + while (joybus_identify_pending) { + if (__kernel) + kirq_wait(&w); + } +} + +static void timer_callback(int ovfl, void *ctx) +{ + bool reset = (bool)ctx; + joybus_identify_async(reset); +} + +void joybus_init(void) +{ + // NOTE: the actual SI/interrupt initialization is done in __joybus_init() + // which is a constructor function that runs before main(). + // This function effectively just initializes the periodic polling and + // background detection system. + if (joybus_init_count++ > 0) { return; } + + // Start a timer (period 1 second) to periodically identify the devices + timer_init(); + identify_timer = new_timer_context(JOYBUS_IDENTIFY_INTERVAL_TICKS, TF_CONTINUOUS, timer_callback, (void*)false); + + // Run a first detection pass immediately, so that after joybus_init(), + // at least we have a first snapshot of the connected devices. + joybus_identify_async(true); + joybus_identify_wait(); +} + +void joybus_close(void) +{ + if (--joybus_init_count > 0) { return; } + + // Stop the identify timer, and deinitialize the timer subsystem + delete_timer(identify_timer); identify_timer = NULL; + timer_close(); + + // Make sure we are not in the middle of an identify operation, otherwise + // we must wait until it's finished + joybus_identify_wait(); + + // Clear all registered detection callbacks. + memset(detection_callbacks, 0, sizeof(detection_callbacks)); +} + +void joybus_detect_now(void) +{ + joybus_identify_async(false); + joybus_identify_wait(); +} + +joybus_identifier_t joybus_get_identifier(int port, uint8_t *status) +{ + assert((port >= 0) && (port < JOYBUS_PORT_COUNT)); + joybus_identifier_t identifier; + + // Return a consistent snapshot of identifier and status + disable_interrupts(); + if (status) *status = joybus_identify_status_hot[port]; + identifier = joybus_identifiers_hot[port]; + enable_interrupts(); + + return identifier; +} + +extern inline void joybus_exec_cmd(int port, size_t send_len, size_t recv_len, const void *send_data, void *recv_data); diff --git a/src/joybus/joybus_internal.h b/src/joybus/joybus_internal.h deleted file mode 100644 index 1f6e0766e..000000000 --- a/src/joybus/joybus_internal.h +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @file joybus_internal.h - * @author Christopher Bonhage - * @brief Joybus internal API - * @ingroup joybus - */ - -#ifndef __LIBDRAGON_JOYBUS_INTERNAL_H -#define __LIBDRAGON_JOYBUS_INTERNAL_H - -#include - -void joybus_exec_async(const void * input, void (*callback)(uint64_t *output, void *ctx), void *ctx); - -#endif diff --git a/src/joybus/joybus_rtc.c b/src/joybus/joybus_rtc.c new file mode 100644 index 000000000..1336ac489 --- /dev/null +++ b/src/joybus/joybus_rtc.c @@ -0,0 +1,557 @@ +/** + * @file joybus_rtc.c + * @author Christopher Bonhage + * @brief Joybus Real-Time Clock Utilities + * @ingroup rtc + */ + +#include "debug.h" +#include "ed64.h" +#include "ed64x.h" +#include "joybus.h" +#include "joybus_commands.h" +#include "joybus_rtc.h" +#include "libcart/cart.h" +#include "n64sys.h" +#include "rtc_internal.h" + +/** @brief Set to 1 to activate debug logs */ +#define JOYBUS_RTC_TRACE 0 + +#if JOYBUS_RTC_TRACE +/** @brief like debugf(), but writes only if #JOYBUS_RTC_TRACE is not 0 */ +#define tracef(fmt, ...) debugf(fmt, ##__VA_ARGS__) +#else +/** @brief like debugf(), but writes only if #JOYBUS_RTC_TRACE is not 0 */ +#define tracef(fmt, ...) ({ }) +#endif + +/** + * @addtogroup joybus_rtc + * @{ + */ + +// MARK: Types + +/** @brief Joybus RTC Block Types */ +typedef enum +{ + /** @brief Control block */ + JOYBUS_RTC_BLOCK_CONTROL = 0, + /** @brief Unused block */ + JOYBUS_RTC_BLOCK_UNUSED = 1, + /** @brief Time block */ + JOYBUS_RTC_BLOCK_TIME = 2, + /** @brief Empty block */ + JOYBUS_RTC_BLOCK_EMPTY = 3, +} joybus_rtc_block_t; + +/** @brief Joybus RTC Data Chunk */ +typedef union +{ + /** @brief double-word access of data */ + uint64_t dword; + /** @brief byte-wise access of data */ + uint8_t bytes[8]; +} joybus_rtc_data_t; + +/** @brief Joybus RTC Control Data */ +typedef union +{ + /** @brief Raw data chunk */ + joybus_rtc_data_t data; + /// @cond + struct __attribute__((packed)) + { + /// @endcond + unsigned : 6; + /** @brief Lock Block 1 bit */ + unsigned lock_block1 : 1; + /** @brief Lock Block 2 bit */ + unsigned lock_block2 : 1; + unsigned : 5; + /** @brief Stop bit */ + unsigned stop : 1; + unsigned : 2; + /// @cond + }; + /// @endcond +} joybus_rtc_control_t; + +// MARK: Constants + +/** @brief Joybus RTC port number */ +#define JOYBUS_RTC_PORT 4 + +/** + * @brief Duration (in milliseconds) to wait after writing a Joybus RTC block. + * + * The software should wait for the previous RTC write to finish before issuing + * another Joybus RTC command. Ideally, you could read the RTC status byte to + * determine when to proceed, but some RTC reproductions do not correctly + * implement the RTC status response, so a delay is used for compatibility. + */ +#define JOYBUS_RTC_WRITE_BLOCK_DELAY 20 + +// MARK: Static functions + +/** @brief Decode the Joybus RTC date/time block data into a struct tm */ +static time_t joybus_rtc_decode_time( const joybus_rtc_data_t * data ) +{ + // Super quick sanity check + if (data->dword == 0) return RTC_EBADTIME; + + int sec = bcd_decode( data->bytes[0] ); + int min = bcd_decode( data->bytes[1] ); + int hour = bcd_decode( data->bytes[2] - 0x80 ); + int day = bcd_decode( data->bytes[3] ); + //int dow = bcd_decode( data->bytes[4] ); + int month = bcd_decode( data->bytes[5] ); + int year = bcd_decode( data->bytes[6] ); + int century = bcd_decode( data->bytes[7] ); + + // Extremely basic sanity-check on the date and time + // We don't use day-of-week so don't validate it. Also 64drive + // emulates it with 0=Sun 1=Mon, while ED64 OS uses 1=Mon 7=Sun, + // so they are even incompatible. + if( + century > 1 || + month == 0 || month > 12 || + day == 0 || day > 31 || + hour >= 24 || min >= 60 || sec >= 60 + ) + { + return RTC_EBADTIME; + } + + struct tm rtc_time = (struct tm){ + .tm_sec = sec, + .tm_min = min, + .tm_hour = hour, + .tm_mday = day, + .tm_mon = month - 1, + .tm_year = year + (century * 100), + }; + return mktime( &rtc_time ); +} + +static void joybus_rtc_read_async( + joybus_rtc_block_t block, + joybus_callback_t callback, + void *ctx +) +{ + joybus_cmd_rtc_read_block_t cmd = { .send = { + .command = JOYBUS_COMMAND_ID_RTC_READ_BLOCK, + .block = block, + } }; + // Allocate the Joybus operation block input and output buffers + uint8_t input[JOYBUS_BLOCK_SIZE] = {0}; + // Skip commands on ports before the desired port offset + size_t i = JOYBUS_RTC_PORT; + // Set the command metadata + input[i++] = sizeof(cmd.send); + input[i++] = sizeof(cmd.recv); + // Copy the send_data into the input buffer + memcpy(&input[i], (void *)&cmd.send, sizeof(cmd.send)); + i += sizeof(cmd.send) + sizeof(cmd.recv); + // Close out the Joybus operation block + input[i] = 0xFE; + input[sizeof(input) - 1] = 0x01; + // Execute the Joybus operation asynchronously + joybus_exec_async(input, callback, ctx); +} + +static void joybus_rtc_write_async( + joybus_rtc_block_t block, + const joybus_rtc_data_t * data, + joybus_callback_t callback, + void *ctx +) +{ + joybus_cmd_rtc_write_block_t cmd = { .send = { + .command = JOYBUS_COMMAND_ID_RTC_WRITE_BLOCK, + .block = block, + .dword = data->dword + } }; + // Allocate the Joybus operation block input and output buffers + uint8_t input[JOYBUS_BLOCK_SIZE] = {0}; + // Skip commands on ports before the desired port offset + size_t i = JOYBUS_RTC_PORT; + // Set the command metadata + input[i++] = sizeof(cmd.send); + input[i++] = sizeof(cmd.recv); + // Copy the send_data into the input buffer + memcpy(&input[i], (void *)&cmd.send, sizeof(cmd.send)); + i += sizeof(cmd.send) + sizeof(cmd.recv); + // Close out the Joybus operation block + input[i] = 0xFE; + input[sizeof(input) - 1] = 0x01; + // Execute the Joybus operation asynchronously + joybus_exec_async(input, callback, ctx); +} + +/** + * @brief Read a Joybus RTC block. + * + * @param block the RTC block to read + * @param[out] data destination buffer + * + * @return Joybus RTC status byte + */ +static void joybus_rtc_read( joybus_rtc_block_t block, joybus_rtc_data_t * data ) +{ + joybus_cmd_rtc_read_block_t cmd = { .send = { + .command = JOYBUS_COMMAND_ID_RTC_READ_BLOCK, + .block = block, + } }; + joybus_exec_cmd_struct(JOYBUS_RTC_PORT, cmd); + data->dword = cmd.recv.dword; + // DO NOT RELY ON THE STATUS BYTE: Some Joybus RTC implementations don't set it! +} + +/** + * @brief Write a Joybus RTC block. + * + * @param block the RTC block to write + * @param[out] data source buffer + * + * @return Joybus RTC status byte + */ +static void joybus_rtc_write( joybus_rtc_block_t block, const joybus_rtc_data_t * data ) +{ + joybus_cmd_rtc_write_block_t cmd = { .send = { + .command = JOYBUS_COMMAND_ID_RTC_WRITE_BLOCK, + .block = block, + .dword = data->dword + } }; + joybus_exec_cmd_struct(JOYBUS_RTC_PORT, cmd); + // DO NOT RELY ON THE STATUS BYTE: Some Joybus RTC implementations don't set it! +} + +// MARK: Detect functions + +/** @brief Joybus RTC detect state enumeration */ +typedef enum { + JOYBUS_RTC_DETECT_INIT = 0, ///< Initial state + JOYBUS_RTC_DETECT_PENDING, ///< Detection is pending + JOYBUS_RTC_DETECTED, ///< RTC detected + JOYBUS_RTC_NOT_DETECTED, ///< RTC not detected +} joybus_rtc_detect_state_t; + +static volatile joybus_rtc_detect_state_t joybus_rtc_detect_state = JOYBUS_RTC_DETECT_INIT; +static volatile joybus_rtc_status_t joybus_rtc_detect_status = {0}; + +static void joybus_rtc_detect_callback( uint64_t *out_dwords, void *ctx ) +{ + const uint8_t *out_bytes = (void *)out_dwords; + joybus_rtc_detect_callback_t callback = ctx; + joybus_cmd_identify_port_t *cmd = (void *)&out_bytes[JOYBUS_RTC_PORT + JOYBUS_COMMAND_METADATA_SIZE]; + bool detected = cmd->recv.identifier == JOYBUS_IDENTIFIER_CART_RTC; + joybus_rtc_status_t status = { .byte = cmd->recv.status }; + joybus_rtc_detect_state = detected ? JOYBUS_RTC_DETECTED : JOYBUS_RTC_NOT_DETECTED; + joybus_rtc_detect_status.byte = status.byte; + + tracef("joybus_rtc_detect_async: %s %s %s %s\n", + detected ? "detected" : "not detected", + status.stopped ? "stopped" : "running", + status.crystal_bad ? "crystal:BAD" : "crystal:OK", + status.battery_bad ? "battery:BAD" : "battery:OK" + ); + + callback( detected, status ); +} + +void joybus_rtc_detect_async( joybus_rtc_detect_callback_t callback ) +{ + joybus_rtc_detect_state = JOYBUS_RTC_DETECT_PENDING; + joybus_cmd_identify_port_t cmd = { .send = { + .command = JOYBUS_COMMAND_ID_RTC_IDENTIFY, + } }; + // Allocate the Joybus operation block input and output buffers + uint8_t input[JOYBUS_BLOCK_SIZE] = {0}; + // Skip commands on ports before the desired port offset + size_t i = JOYBUS_RTC_PORT; + // Set the command metadata + input[i++] = sizeof(cmd.send); + input[i++] = sizeof(cmd.recv); + // Copy the send_data into the input buffer + memcpy( &input[i], (void *)&cmd.send, sizeof(cmd.send) ); + i += sizeof(cmd.send) + sizeof(cmd.recv); + // Close out the Joybus operation block + input[i] = 0xFE; + input[sizeof(input) - 1] = 0x01; + // Execute the Joybus operation asynchronously + tracef("joybus_rtc_detect_async: probing\n"); + joybus_exec_async( input, joybus_rtc_detect_callback, callback ); +} + +bool joybus_rtc_detect( void ) +{ + switch( joybus_rtc_detect_state ) + { + case JOYBUS_RTC_DETECTED: + return !joybus_rtc_detect_status.crystal_bad; + case JOYBUS_RTC_NOT_DETECTED: + return false; + case JOYBUS_RTC_DETECT_INIT: + joybus_rtc_detect_async(NULL); + break; + case JOYBUS_RTC_DETECT_PENDING: + break; + } + while( joybus_rtc_detect_state == JOYBUS_RTC_DETECT_PENDING ){ /* Spinlock! */} + return joybus_rtc_detect_state == JOYBUS_RTC_DETECTED && !joybus_rtc_detect_status.crystal_bad; +} + +// MARK: Control functions + +static void joybus_rtc_set_stopped_write_callback( uint64_t *out_dwords, void *ctx ) +{ + tracef("joybus_rtc_set_stopped_async: done\n"); + joybus_rtc_set_stopped_callback_t callback = ctx; + if( callback ) callback(); +} + +static void joybus_rtc_set_stopped_read_callback( uint64_t *out_dwords, void *ctx ) +{ + // Parse the Joybus command response + const uint8_t *out_bytes = (void *)out_dwords; + joybus_cmd_rtc_read_block_t *cmd = (void *)&out_bytes[JOYBUS_RTC_PORT + JOYBUS_COMMAND_METADATA_SIZE]; + joybus_rtc_control_t control = { .data = { .dword = cmd->recv.dword } }; + tracef("joybus_rtc_set_stopped_async: control state (0x%llx)\n", control.data.dword); + + // Unpack the stop bit and the callback pointer from the context + bool stop = ((uint32_t)ctx & 0x1); + joybus_rtc_set_stopped_callback_t callback = (void *)((uint32_t)ctx & ~0x1); + + control.stop = stop; + control.lock_block1 = !stop; + control.lock_block2 = !stop; + tracef("joybus_rtc_set_stopped_async: writing control block (0x%llx)\n", control.data.dword); + joybus_rtc_write_async( + JOYBUS_RTC_BLOCK_CONTROL, + &control.data, + joybus_rtc_set_stopped_write_callback, + callback + ); +} + +void joybus_rtc_set_stopped_async( bool stop, joybus_rtc_set_stopped_callback_t callback ) +{ + // Pack the stop bit and the callback pointer into the context value + void *ctx = (void *)((uint32_t)callback | (stop & 0x1)); + tracef("joybus_rtc_set_stopped_async: reading control block\n"); + joybus_rtc_read_async( JOYBUS_RTC_BLOCK_CONTROL, joybus_rtc_set_stopped_read_callback, ctx ); +} + +void joybus_rtc_set_stopped( bool stop ) +{ + joybus_rtc_control_t control; + tracef("joybus_rtc_set_stopped: reading control block\n"); + joybus_rtc_read( JOYBUS_RTC_BLOCK_CONTROL, &control.data ); + tracef("joybus_rtc_set_stopped: control state (0x%llx)\n", control.data.dword); + + control.stop = stop; + control.lock_block1 = !stop; + control.lock_block2 = !stop; + tracef("joybus_rtc_set_stopped: writing control block (0x%llx)\n", control.data.dword); + joybus_rtc_write( JOYBUS_RTC_BLOCK_CONTROL, &control.data ); +} + +bool joybus_rtc_is_stopped( void ) +{ + joybus_cmd_identify_port_t cmd = { .send = { + .command = JOYBUS_COMMAND_ID_RTC_IDENTIFY, + } }; + joybus_exec_cmd_struct(JOYBUS_RTC_PORT, cmd); + joybus_rtc_status_t status = { .byte = cmd.recv.status }; + tracef("joybus_rtc_is_stopped: status (0x%02x)\n", status.byte); + joybus_rtc_detect_status.byte = status.byte; + return status.stopped; +} + +// MARK: Time functions + +static void joybus_rtc_get_time_callback( uint64_t *out_dwords, void *ctx ) +{ + const uint8_t *out_bytes = (void *)out_dwords; + joybus_rtc_get_time_callback_t callback = ctx; + joybus_cmd_rtc_read_block_t *cmd = (void *)&out_bytes[JOYBUS_RTC_PORT + JOYBUS_COMMAND_METADATA_SIZE]; + joybus_rtc_data_t data = { .dword = cmd->recv.dword }; + tracef("joybus_rtc_get_time_async: raw time (0x%llx)\n", data.dword); + time_t decoded_time = joybus_rtc_decode_time( &data ); + if (decoded_time < 0) + { + tracef("joybus_rtc_get_time_async: invalid time\n"); + callback( (int)decoded_time, 0 ); + return; + } + + #if JOYBUS_RTC_TRACE + struct tm * parsed_tm = gmtime( &decoded_time ); + tracef("joybus_rtc_get_time_async: parsed time (%04d-%02d-%02d %02d:%02d:%02d)\n", + parsed_tm->tm_year + 1900, parsed_tm->tm_mon + 1, parsed_tm->tm_mday, + parsed_tm->tm_hour, parsed_tm->tm_min, parsed_tm->tm_sec); + #endif + + callback( RTC_ESUCCESS, decoded_time ); +} + +void joybus_rtc_get_time_async( joybus_rtc_get_time_callback_t callback ) +{ + if( joybus_rtc_detect_state != JOYBUS_RTC_DETECTED ) + { + tracef("joybus_rtc_get_time_async: RTC not detected; aborting!\n"); + callback( RTC_ENOCLOCK, 0 ); + return; + } + + if( joybus_rtc_detect_status.crystal_bad ) + { + tracef("joybus_rtc_get_time_async: crystal is bad; aborting!\n"); + callback( RTC_EBADCLOCK, 0 ); + return; + } + + tracef("joybus_rtc_get_time_async: reading time block\n"); + joybus_rtc_read_async( JOYBUS_RTC_BLOCK_TIME, joybus_rtc_get_time_callback, callback ); +} + +int joybus_rtc_get_time( time_t *out ) +{ + if( joybus_rtc_detect_state != JOYBUS_RTC_DETECTED ) + { + tracef("joybus_rtc_get_time: RTC not detected; aborting!\n"); + return RTC_ENOCLOCK; + } + + if( joybus_rtc_detect_status.crystal_bad ) + { + tracef("joybus_rtc_get_time: crystal is bad; aborting!\n"); + return RTC_EBADCLOCK; + } + + joybus_rtc_data_t data = {0}; + tracef("joybus_rtc_get_time: reading time block\n"); + joybus_rtc_read( JOYBUS_RTC_BLOCK_TIME, &data ); + tracef("joybus_rtc_get_time: raw time (0x%llx)\n", data.dword); + time_t time = joybus_rtc_decode_time( &data ); + if (time < 0) + return (int)time; + + *out = time; + #if JOYBUS_RTC_TRACE + struct tm * parsed_tm = gmtime( out ); + tracef("joybus_rtc_get_time: parsed time (%04d-%02d-%02d %02d:%02d:%02d)\n", + parsed_tm->tm_year + 1900, parsed_tm->tm_mon + 1, parsed_tm->tm_mday, + parsed_tm->tm_hour, parsed_tm->tm_min, parsed_tm->tm_sec + ); + #endif + + return RTC_ESUCCESS; +} + +int joybus_rtc_set_time( time_t new_time ) +{ + if( new_time < JOYBUS_RTC_TIMESTAMP_MIN || new_time > JOYBUS_RTC_TIMESTAMP_MAX ) + { + tracef("joybus_rtc_set_time: time out of range\n"); + return RTC_EBADTIME; + } + + if( joybus_rtc_detect_status.crystal_bad ) + { + tracef("joybus_rtc_set_time: crystal is bad; aborting!\n"); + return RTC_EBADCLOCK; + } + + if( joybus_rtc_detect_status.battery_bad ) + { + tracef("joybus_rtc_set_time: battery is bad; aborting!\n"); + return RTC_EBADCLOCK; + } + + if (cart_type == CART_ED || cart_type == CART_EDX) + { + uint8_t buf[7]; + ed64_rtc_encode( new_time, buf ); + + // Joybus RTC write is not supported on EverDrive 64. + // Attempt to use the ED64 i2c interface to set the time instead. + switch( cart_type ) + { + case CART_ED: + if( ed64_rtc_write( buf ) != 0 ) + { + tracef("joybus_rtc_set_time: EverDrive 64 i2c write failed!\n"); + return RTC_EBADCLOCK; + } + return RTC_ESUCCESS; + case CART_EDX: + if( ed64x_rtc_write( buf ) != 0 ) + { + tracef("joybus_rtc_set_time: EverDrive 64 X7 i2c write failed!\n"); + return RTC_EBADCLOCK; + } + return RTC_ESUCCESS; + } + } + + tracef("joybus_rtc_set_time: reading control block\n"); + joybus_rtc_control_t control; + joybus_rtc_read( JOYBUS_RTC_BLOCK_CONTROL, &control.data ); + tracef("joybus_rtc_get_time: control state (0x%llx)\n", control.data.dword); + + /* Prepare the RTC to write the time */ + control.stop = true; + control.lock_block1 = false; + control.lock_block2 = false; + tracef("joybus_rtc_set_time: writing control block (0x%llx)\n", control.data.dword); + joybus_rtc_write( JOYBUS_RTC_BLOCK_CONTROL, &control.data ); + wait_ms( JOYBUS_RTC_WRITE_BLOCK_DELAY ); + + /* Determine if the RTC implementation supports writes. */ + if( !joybus_rtc_is_stopped() ) + { + tracef("joybus_rtc_set_time: rtc did not stop; aborting!\n"); + return RTC_EBADCLOCK; + } + + /* Write the time block to RTC */ + struct tm * rtc_time = gmtime( &new_time ); + tracef("joybus_rtc_set_time: parsed time (%02d:%02d:%02d %02d/%02d/%04d)\n", + rtc_time->tm_hour, rtc_time->tm_min, rtc_time->tm_sec, + rtc_time->tm_mon + 1, rtc_time->tm_mday, rtc_time->tm_year + 1900 + ); + joybus_rtc_data_t data = { .bytes = { + bcd_encode( rtc_time->tm_sec ), + bcd_encode( rtc_time->tm_min ), + bcd_encode( rtc_time->tm_hour ) + 0x80, + bcd_encode( rtc_time->tm_mday ), + bcd_encode( rtc_time->tm_wday ), + bcd_encode( rtc_time->tm_mon + 1 ), + bcd_encode( rtc_time->tm_year ), + bcd_encode( rtc_time->tm_year / 100 ), + } }; + tracef("joybus_rtc_set_time: writing time block (0x%llx)\n", data.dword); + joybus_rtc_write( JOYBUS_RTC_BLOCK_TIME, &data ); + wait_ms( JOYBUS_RTC_WRITE_BLOCK_DELAY ); + + /* Put the RTC back into normal operating mode */ + control.stop = false; + control.lock_block1 = true; + control.lock_block2 = true; + tracef("joybus_rtc_set_time: writing control block (0x%llx)\n", control.data.dword); + joybus_rtc_write( JOYBUS_RTC_BLOCK_CONTROL, &control.data ); + + if( joybus_rtc_is_stopped() ) + { + tracef("joybus_rtc_set_time: rtc did not restart?\n"); + return RTC_EBADCLOCK; + } + + return RTC_ESUCCESS; +} + +/** @} */ /* joybus_rtc */ diff --git a/src/joybus/joybus_rtc.h b/src/joybus/joybus_rtc.h new file mode 100644 index 000000000..996ab15a0 --- /dev/null +++ b/src/joybus/joybus_rtc.h @@ -0,0 +1,202 @@ +/** + * @file joybus_rtc.h + * @author Christopher Bonhage + * @brief Joybus Real-Time Clock Utilities + * @ingroup rtc + */ + +#ifndef __LIBDRAGON_JOYBUS_RTC_H +#define __LIBDRAGON_JOYBUS_RTC_H + +#include +#include +#include + +/** + * @defgroup joybus_rtc Joybus Real-Time Clock + * @ingroup rtc + * @brief Joybus Real-Time Clock interface. + * @author Christopher Bonhage + * + * The Joybus real-time clock is a cartridge peripheral that uses a battery + * to power a clock that tracks the date, time, and day of the week. The + * real-time clock keeps running even when the N64 is powered-off. The + * Joybus RTC is accessed through the serial interface (SI) similar to EEPROM + * and controllers. The Joybus RTC was only ever available on one official + * cartridge that was only available in Japan: Dōbutsu no Mori (Animal Forest). + * Many emulators and flash carts include support for the Animal Forest RTC, + * which makes it possible to include real-time clock functionality in N64 + * homebrew! + * + * The Joybus RTC contains 3 "blocks" (or zones) which contain 8 bytes of data: + * Block 0 contains a half-word control register and opaque calibration data. + * Block 1 is unused and unsupported. See notes below. + * Block 2 contains the current date/time as packed binary-coded decimal. + * + * Animal Forest did not use block 1 at all, so most emulators do not bother to + * implement it. Theoretically, block 1 could be used as 8-bytes of SRAM-backed + * storage, but this is not supported by libdragon's Real-Time Clock Subsystem. + * If you need storage, consider using a standard cartridge save type or saving + * to a Controller Pak. + * + * Unfortunately, since only one game ever used Joybus RTC (and that game was + * later re-released on the GameCube in English), real-time clock support in + * emulators and flash carts can be incomplete, inaccurate, or non-existent. + * Many emulators do not actually implement the Joybus RTC write command and + * always respond with the host device's current local time. Some emulators + * and flash carts support writing to RTC but will not persist the date/time + * after resetting or powering-off. You can run the `rtctest` example ROM on + * your preferred emulator or flash cart to what RTC support is available. + * + * Due to the inaccurate and inconsistent behavior of RTC reproductions that + * currently exist, this subsystem trades-off complete accuracy with the actual + * Animal Forest RTC in favor of broader compatibility with the various quirks + * and bugs that exist in real-world scenarios like emulators and flash carts. + * + * Some notable examples of RTC support in the ecosystem (as of July 2021): + * + * 64drive hw2 fully implements Joybus RTC including writes, but requires + * a 500ms delay after setting the time in order to read it back correctly. + * + * EverDrive64 3.0 and X7 partially support Joybus RTC, with caveats: The RTC + * must be explicitly enabled in the OS or with a ROM header configuration; + * RTC writes are not supported over Joybus -- changing the time must be done + * in the EverDrive OS menu. + * + * UltraPIF fully implements an emulated Joybus RTC that can be accessed even + * when the cartridge does not include the real-time clock circuitry. + * + * Internally, Joybus RTC cannot represent dates before 1900-01-01, although + * some RTC implementations (like UltraPIF) only support dates after + * 2000-01-01. Joybus RTC cannot represent dates after 2099-12-31. If the + * clock goes beyond this range, it will wrap around to 1900-01-01. + * + * Special thanks to korgeaux, marshallh and jago85 for their hard work + * and research reverse-engineering and documenting the inner-workings + * of the Joybus RTC. + * + * @{ + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Joybus RTC minimum timestamp (1900-01-01 00:00:00) */ +#define JOYBUS_RTC_TIMESTAMP_MIN -2208988800 +/** @brief Joybus RTC maximum timestamp (2099-12-31 23:59:59) */ +#define JOYBUS_RTC_TIMESTAMP_MAX 4102444799 + +/** @brief Joybus RTC status byte */ +typedef union +{ + /** @brief raw byte value */ + uint8_t byte; + /// @cond + struct __attribute__((packed)) + { + /// @endcond + /** @brief Set if the RTC is stopped. */ + unsigned stopped : 1; + unsigned : 5; + /** @brief Set if RTC crystal is not working. */ + unsigned crystal_bad : 1; + /** @brief Set if the RTC battery voltage is too low. */ + unsigned battery_bad : 1; + /// @cond + }; + /// @endcond +} joybus_rtc_status_t; + +/** @brief Callback function signature for #joybus_rtc_detect_async */ +typedef void (*joybus_rtc_detect_callback_t)(bool detected, joybus_rtc_status_t status); + +/** @brief Callback function signature for #joybus_rtc_set_stopped_async */ +typedef void (*joybus_rtc_set_stopped_callback_t)(void); + +/** @brief Callback function signature for #joybus_rtc_get_time_async */ +typedef void (*joybus_rtc_get_time_callback_t)(int error, time_t time); + +/** + * @brief Detect the presence of the Joybus real-time clock asynchronously. + * + * @param callback function to call when detection is complete + */ +void joybus_rtc_detect_async( joybus_rtc_detect_callback_t callback ); + +/** + * @brief Detect the presence of the Joybus real-time clock. + * + * @return whether the Joybus RTC was detected and operational + */ +bool joybus_rtc_detect( void ); + +/** + * @brief Check if the Joybus real-time clock is stopped. + * + * The clock must be stopped in order to set the time, + * and should not be stopped during normal operation. + */ +bool joybus_rtc_is_stopped( void ); + +/** + * @brief Set the stop state of the Joybus real-time clock asynchronously. + * + * The clock must be stopped in order to set the time, + * and should not be stopped during normal operation. + * + * @param stop whether to stop the clock + * @param callback function to call when the stop state has been set + */ +void joybus_rtc_set_stopped_async( bool stop, joybus_rtc_set_stopped_callback_t callback ); + +/** + * @brief Change the stop state of the Joybus real-time clock. + * + * The clock must be stopped in order to set the time, + * and should not be stopped during normal operation. + * + * @param stop whether to stop the clock + */ +void joybus_rtc_set_stopped( bool stop ); + +/** + * @brief Read the date/time of the Joybus real-time clock asynchronously. + * + * @param callback function to call when the time has been read + */ +void joybus_rtc_get_time_async( joybus_rtc_get_time_callback_t callback ); + +/** + * @brief Read the current date/time from the Joybus real-time clock. + * + * @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 crystal is bad + * @retval RTC_EBADTIME if the RTC cannot represent the new time + */ +int joybus_rtc_get_time( time_t *out ); + +/** + * @brief Set the date/time on the Joybus real-time clock. + * + * Prepares the RTC for writing, sets the new time, and resumes the clock. + * + * @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 crystal is bad + * @retval RTC_EBADTIME if the RTC cannot represent the new time + */ +int joybus_rtc_set_time( time_t new_time ); + +#ifdef __cplusplus +} +#endif + +/** @} */ /* rtc */ + +#endif diff --git a/src/joybus/joyframe.c b/src/joybus/joyframe.c new file mode 100644 index 000000000..3e43c497f --- /dev/null +++ b/src/joybus/joyframe.c @@ -0,0 +1,59 @@ +/** + * @file joyframe.c + * @author Giovanni Bajo + */ +/* + * Joybus packet handling functions + */ + +#include "joyframe.h" + +joyframe_err_t joyframe_read(joyframe_t pkt, uint8_t *cmd, uint8_t **data, int *data_len) +{ + joyframe_err_t err = JOYFRAME_ERR_NONE; + + int idx = pkt[63]; + if (idx >= 63) return JOYFRAME_ERR_FINISHED; + + // Extract send length and check escape codes + int send_len; + while ((send_len = pkt[idx++]) == JOYFRAME_ESC_NOP) {} + if (send_len == JOYFRAME_ESC_SKIP) { err = JOYFRAME_SKIPPED; goto exit; } + if (send_len == JOYFRAME_ESC_RESET) { err = JOYFRAME_RESET; goto exit; } + if (send_len == JOYFRAME_ESC_END) { err = JOYFRAME_ERR_FINISHED; idx = 63; goto exit; } + + // Not an escape code. Check TX flags. + // We never encode these ourselves, but they are supported by PIF and this + // function could be called on an user-provided packet. + if (send_len & JOYFRAME_TX_RESET) { err = JOYFRAME_RESET; goto exit; } + if (send_len & JOYFRAME_TX_SKIP) { err = JOYFRAME_SKIPPED; goto exit; } + send_len &= 0x3F; + + // Read receive length and check RX flags + int recv_len = pkt[idx++]; + if (recv_len & JOYFRAME_RX_NO_DEVICE) { err = JOYFRAME_ERR_NO_DEVICE; goto exit; } + if (recv_len & JOYFRAME_RX_TIMEOUT) { err = JOYFRAME_ERR_TIMEOUT; goto exit; } + recv_len &= 0x3F; + + // Move index to the end of this command's data + if (cmd) *cmd = pkt[idx]; + if (data) *data = &pkt[idx + send_len]; + if (data_len) *data_len = recv_len; + idx += send_len + recv_len; + +exit: + assertf(idx < 64, "overflow in joybus frame parsing"); + pkt[63] = idx; + return err; +} + +/// @cond +extern inline void joyframe_write_RESET(joyframe_t pkt); +extern inline void joyframe_write_IDENTIFY(joyframe_t pkt); +extern inline void joyframe_write_N64_CONTROLLER_READ(joyframe_t pkt); +extern inline void joyframe_write_N64_ACCESSORY_READ(joyframe_t pkt, uint16_t address); +extern inline void joyframe_write_N64_ACCESSORY_WRITE(joyframe_t pkt, uint16_t address, const uint8_t data[32]); + +extern inline bool joyframe_read_N64_ACCESSORY_READ(joyframe_t pkt, uint8_t **data, uint8_t *checksum); +extern inline bool joyframe_read_N64_ACCESSORY_WRITE(joyframe_t pkt, uint8_t *checksum); +/// @endcond diff --git a/src/joybus/joypad.c b/src/joybus/joypad.c index c98872939..cf185f2bc 100644 --- a/src/joybus/joypad.c +++ b/src/joybus/joypad.c @@ -9,23 +9,18 @@ #include #include +#include "n64sys.h" +#include "vi.h" #include "debug.h" #include "interrupt.h" #include "joypad_internal.h" +#include "../entropy_internal.h" /** * @addtogroup joypad * @{ */ -/** - * @brief Number of ticks between Joybus identify commands. - * - * During VI interrupt, the Joypad subsystem will periodically re-identify - * the connected devices to check if the identifier has changed or if any - * accessories have been connected/disconnected. - */ -#define JOYPAD_IDENTIFY_INTERVAL_TICKS TICKS_PER_SECOND /** @brief Convenience macro to ensure Joypad subsystem is initialized. */ #define ASSERT_JOYPAD_INITIALIZED() \ @@ -57,15 +52,6 @@ static joypad_port_t bb_hack_flags_swap_port( joypad_port_t port ) * @name "Hot" (interrupt-driven) global state * @{ */ -/** @brief Timer ticks when the Joypads were last identified. */ -static volatile uint32_t joypad_identify_last_ticks = 0; -/** @brief Are we in the middle of identifying the Joypads? */ -static volatile bool joypad_identify_pending = false; -/** @brief Is the cached Joybus input block for identifying Joypads valid? */ -static volatile uint8_t joypad_identify_input_valid = false; -/** @brief Cached Joybus input block for identifying all Joypads. */ -static volatile uint8_t joypad_identify_input[JOYBUS_BLOCK_SIZE] = {0}; - /** @brief Are we in the middle of reading the Joypads? */ static volatile bool joypad_read_pending = false; /** @brief Is the cached Joybus input block for reading Joypads valid? */ @@ -84,14 +70,14 @@ static volatile bool joypad_gcn_origin_input_valid = false; /** @brief Cached Joybus input block for reading GameCube origins. */ static volatile uint8_t joypad_gcn_origin_input[JOYBUS_BLOCK_SIZE] = {0}; -/** @brief Joypad identifiers for each port. */ -volatile joybus_identifier_t joypad_identifiers_hot[JOYPAD_PORT_COUNT] = {0}; /** @brief Joypad "hot" devices for each port. */ volatile joypad_device_hot_t joypad_devices_hot[JOYPAD_PORT_COUNT] = {0}; /** @brief Joypad origins for each port. */ volatile joypad_gcn_origin_t joypad_origins_hot[JOYPAD_PORT_COUNT] = {0}; /** @brief Joypad accessories for each port. */ volatile joypad_accessory_t joypad_accessories_hot[JOYPAD_PORT_COUNT] = {0}; +/** @brief Accessory library hooks (set only when accessory module is linked). */ +const joypad_accessory_library_vtable_t *__joypad_accessory_vtable = NULL; /** @} */ /* joypad_hot_state */ /** @@ -105,50 +91,6 @@ static int joypad_init_refcount = 0; static joypad_device_cold_t joypad_devices_cold[JOYPAD_PORT_COUNT] = {0}; /** @} */ /* joypad_cold_state */ -/** - * @brief Reinitialize the state of a Joypad device when its identifier changes. - * - * @param port Joypad port to reinitialize. - * @param identifier New Joypad identifier for the port. - */ -static void joypad_device_changed(joypad_port_t port, joybus_identifier_t identifier) -{ - joypad_identifiers_hot[port] = identifier; - joypad_origins_hot[port] = JOYPAD_GCN_ORIGIN_INIT; - memset((void *)&joypad_devices_cold[port], 0, sizeof(joypad_devices_cold[port])); - memset((void *)&joypad_devices_hot[port], 0, sizeof(joypad_devices_hot[port])); - joypad_accessory_reset(port); -} - -static void joybus_input_identify( uint8_t input[JOYBUS_BLOCK_SIZE], bool reset ) -{ - const joybus_cmd_identify_port_t cmd = { .send = { - .command = reset ? JOYBUS_COMMAND_ID_RESET : JOYBUS_COMMAND_ID_IDENTIFY, - } }; - const size_t recv_offset = offsetof(typeof(cmd), recv); - size_t i = 0; - - // Populate the Joybus commands on each port - memset(input, 0x00, JOYBUS_BLOCK_SIZE); - JOYPAD_PORT_FOREACH (port) - { - // iQue PIF requires a NOP (0xFF) before each command - if (sys_bbplayer()) input[i++] = 0xFF; - // Set the command metadata - input[i++] = sizeof(cmd.send); - input[i++] = sizeof(cmd.recv); - // Micro-optimization: Minimize copy length - memcpy(&input[i], &cmd, recv_offset); - i += sizeof(cmd); - // iQue PIF requires commands to be 8-byte aligned - if (sys_bbplayer()) while (i & 7) input[i++] = 0xFF; - } - - // Close out the Joybus operation block - input[i] = 0xFE; - input[JOYBUS_BLOCK_SIZE - 1] = 0x01; -} - static void joybus_input_read_n64_controllers( uint8_t input[JOYBUS_BLOCK_SIZE] ) { const joybus_cmd_n64_controller_read_port_t cmd = { .send = { @@ -286,7 +228,7 @@ static joypad_inputs_t joypad_inputs_from_gcn_controller_read( * @param port Joypad port of the GameCube controller. * @param active Whether to enable (true) or disable (false) rumble motors. */ -static void joypad_gcn_controller_rumble_toggle(joypad_port_t port, bool active) +void __joypad_gcn_controller_rumble_toggle(joypad_port_t port, bool active) { volatile joypad_device_hot_t *device = &joypad_devices_hot[port]; device->rumble_active = active; @@ -387,45 +329,93 @@ static void joypad_gcn_origin_check_async(void) joybus_exec_async(input, joypad_gcn_origin_callback, NULL); } -/** - * @brief Callback for identifying Joypads. - * - * @param[in] out_dwords Joybus output block. - * @param[in,out] ctx Not used. - */ -static void joypad_identify_callback(uint64_t *out_dwords, void *ctx) +static void joypad_accessory_reset_optional(joypad_port_t port) { - const uint8_t *out_bytes = (void *)out_dwords; - const joybus_cmd_identify_port_t *cmd; - volatile joypad_device_hot_t *device; - volatile joypad_accessory_t *accessory; - bool devices_changed = false; - size_t i = 0; + const joypad_accessory_library_vtable_t *vtable = __joypad_accessory_vtable; + if (vtable && vtable->reset) + { + vtable->reset(port); + } + else + { + memset((void *)&joypad_accessories_hot[port], 0, sizeof(joypad_accessories_hot[port])); + } +} - JOYPAD_PORT_FOREACH (port) +static void joypad_device_reset(int port) +{ + memset((void *)&joypad_devices_hot[port], 0, sizeof(joypad_devices_hot[port])); + joypad_origins_hot[port] = JOYPAD_GCN_ORIGIN_INIT; + joypad_accessory_reset_optional(port); + joypad_read_input_valid = false; +} + +/** + * @brief Process a new status byte for an N64 controller to handle accessory changes. + */ +static void joypad_accessory_polled(int port, uint8_t new_status) +{ + volatile joypad_device_hot_t *device = &joypad_devices_hot[port]; + volatile joypad_accessory_t *accessory = &joypad_accessories_hot[port]; + assert(device->style == JOYPAD_STYLE_N64); // Only N64 controllers have accessories + + uint8_t prev_accessory_status = accessory->status; + uint8_t accessory_status = new_status & JOYBUS_IDENTIFY_STATUS_ACCESSORY_MASK; + // Work-around third-party controllers that don't correctly report accessory status + bool accessory_disconnected = ( + accessory_status != prev_accessory_status && + ( + accessory_status == JOYBUS_IDENTIFY_STATUS_ACCESSORY_ABSENT || + ( + accessory_status == JOYBUS_IDENTIFY_STATUS_ACCESSORY_UNSUPPORTED && + // Ignore 8BitDo receiver buggy Rumble Pak mode accessory status: + // Only the initial identify status is correct: subsequent identify status will incorrectly be unsupported. + prev_accessory_status != JOYBUS_IDENTIFY_STATUS_ACCESSORY_PRESENT + ) + ) + ); + bool accessory_changed = ( + accessory_status == JOYBUS_IDENTIFY_STATUS_ACCESSORY_CHANGED || + ( + accessory_status == JOYBUS_IDENTIFY_STATUS_ACCESSORY_PRESENT && + prev_accessory_status != JOYBUS_IDENTIFY_STATUS_ACCESSORY_PRESENT && + prev_accessory_status != JOYBUS_IDENTIFY_STATUS_ACCESSORY_CHANGED + ) || + ( + // Handle 8BitDo receiver buggy switching from Rumble Pak to Controller Pak mode + accessory_status == JOYBUS_IDENTIFY_STATUS_ACCESSORY_PRESENT && + prev_accessory_status == JOYBUS_IDENTIFY_STATUS_ACCESSORY_UNSUPPORTED + ) || + ( + // Handle 8BitDo receiver buggy switching from Controller Pak to Rumble Pak mode + accessory_status == JOYBUS_IDENTIFY_STATUS_ACCESSORY_UNSUPPORTED && + prev_accessory_status == JOYBUS_IDENTIFY_STATUS_ACCESSORY_PRESENT + ) + ); + if (accessory_changed) { - if (sys_bbplayer()) { - // iQue has a very fixed layout for commands, and it also tends - // to corrupt other parts of PIF-RAM. So better jump to fixed positions - // while parsing. - i = (port * 8) + 1; - } - device = &joypad_devices_hot[port]; - accessory = &joypad_accessories_hot[port]; - cmd = (void *)&out_bytes[i + JOYBUS_COMMAND_METADATA_SIZE]; - - joybus_identifier_t identifier = cmd->recv.identifier; - if (out_bytes[i+1] & 0x80) { - // If the error flag is set, no device is connected here - identifier = JOYBUS_IDENTIFIER_NONE; - } - if (joypad_identifiers_hot[port] != identifier) + const joypad_accessory_library_vtable_t *vtable = __joypad_accessory_vtable; + if (vtable && vtable->detect_async) { - // The identifier has changed; reinitialize device state - joypad_device_changed(port, identifier); - devices_changed = true; + vtable->detect_async(port); } + } + else if (accessory_disconnected) + { + joypad_accessory_reset_optional(port); + } + accessory->status = accessory_status; +} +/** @brief Callback when a new device is installed or removed. */ +static void joypad_detect_callback( + joybus_identifier_t identifier, joybus_detection_event_t event, + int port, uint8_t device_status, void *ctx +) { + volatile joypad_device_hot_t *device = &joypad_devices_hot[port]; + + switch (event) { + case JOYBUS_DETECT_CONNECTED: if ( (identifier & JOYBUS_IDENTIFIER_MASK_PLATFORM) == JOYBUS_IDENTIFIER_PLATFORM_GCN && (identifier & JOYBUS_IDENTIFIER_MASK_GCN_CONTROLLER) @@ -437,76 +427,31 @@ static void joypad_identify_callback(uint64_t *out_dwords, void *ctx) ? JOYPAD_RUMBLE_METHOD_GCN_CONTROLLER : JOYPAD_RUMBLE_METHOD_NONE; device->rumble_active = has_rumble && device->rumble_active; + joypad_read_input_valid = false; } else if (identifier == JOYBUS_IDENTIFIER_N64_CONTROLLER) { device->style = JOYPAD_STYLE_N64; - uint8_t prev_accessory_status = accessory->status; - uint8_t accessory_status = cmd->recv.status & JOYBUS_IDENTIFY_STATUS_ACCESSORY_MASK; - // Work-around third-party controllers that don't correctly report accessory status - bool accessory_absent = ( - accessory_status == JOYBUS_IDENTIFY_STATUS_ACCESSORY_ABSENT || - accessory_status == JOYBUS_IDENTIFY_STATUS_ACCESSORY_UNSUPPORTED - ); - bool accessory_changed = ( - accessory_status == JOYBUS_IDENTIFY_STATUS_ACCESSORY_CHANGED || - ( - accessory_status == JOYBUS_IDENTIFY_STATUS_ACCESSORY_PRESENT && - prev_accessory_status != JOYBUS_IDENTIFY_STATUS_ACCESSORY_PRESENT && - prev_accessory_status != JOYBUS_IDENTIFY_STATUS_ACCESSORY_CHANGED - ) - ); - if (accessory_absent || accessory_changed) - { - accessory->state = JOYPAD_ACCESSORY_STATE_IDLE; - accessory->type = JOYPAD_ACCESSORY_TYPE_NONE; - device->rumble_method = JOYPAD_RUMBLE_METHOD_NONE; - device->rumble_active = false; - } - if (accessory_changed) - { - accessory->type = JOYPAD_ACCESSORY_TYPE_UNKNOWN; - joypad_accessory_detect_async(port); - } - accessory->status = accessory_status; + joypad_accessory_polled(port, device_status); + joypad_read_input_valid = false; } else if (identifier == JOYBUS_IDENTIFIER_N64_MOUSE) { device->style = JOYPAD_STYLE_MOUSE; + joypad_read_input_valid = false; } + break; - i += JOYBUS_COMMAND_METADATA_SIZE + sizeof(*cmd); - } - - if (devices_changed) joypad_read_input_valid = false; - joypad_identify_last_ticks = TICKS_READ(); - joypad_identify_pending = false; -} - -/** - * @brief Identify Joypads asynchronously. - * - * @param reset Whether to reset the devices. - */ -static void joypad_identify_async(bool reset) -{ - // Async operations are disabled during reset - if( exception_reset_time() > 0 ) { return; } + case JOYBUS_DETECT_POLLED: + if (device->style == JOYPAD_STYLE_N64) + joypad_accessory_polled(port, device_status); + break; - // Bail if this operation is already in-progress - if (joypad_identify_pending) { return; } - joypad_identify_pending = true; + case JOYBUS_DETECT_DISCONNECTED: + joypad_device_reset(port); + break; - uint8_t * const input = (void *)joypad_identify_input; - // Reset invalidates the cached input block - if (!joypad_identify_input_valid || reset) - { - joybus_input_identify(input, reset); - // Identify is more common than reset, so don't cache resets - joypad_identify_input_valid = !reset; } - - joybus_exec_async(input, joypad_identify_callback, NULL); } /** @@ -519,6 +464,9 @@ static void joypad_read_callback(uint64_t *out_dwords, void *ctx) { memcpy((void *)joypad_read_output, out_dwords, JOYBUS_BLOCK_SIZE); joypad_read_pending = false; + + // Use some bytes from the joypad inputs to feed the entropy pool + __entropy_add(out_dwords[0]); } /** @@ -542,7 +490,6 @@ static void joypad_read_async(void) if (!joypad_read_input_valid) { volatile joypad_device_hot_t *device; - joybus_identifier_t identifier; size_t i = 0; // Populate Joybus controller read commands on each port @@ -551,10 +498,9 @@ static void joypad_read_async(void) { joypad_read_input_offsets[port] = i; device = &joypad_devices_hot[port]; - identifier = joypad_identifiers_hot[port]; - if (device->style == JOYPAD_STYLE_GCN) - { + switch (device->style) { + case JOYPAD_STYLE_GCN: { const joybus_cmd_gcn_controller_read_port_t cmd = { .send = { .command = JOYBUS_COMMAND_ID_GCN_CONTROLLER_READ, .mode = 3, // Most-compatible analog mode @@ -567,12 +513,9 @@ static void joypad_read_async(void) const size_t recv_offset = offsetof(typeof(cmd), recv); memcpy(&input[i], &cmd, recv_offset); i += sizeof(cmd); - } - else if ( - identifier == JOYBUS_IDENTIFIER_N64_CONTROLLER || - identifier == JOYBUS_IDENTIFIER_N64_MOUSE - ) - { + } break; + case JOYPAD_STYLE_N64: + case JOYPAD_STYLE_MOUSE: { const joybus_cmd_n64_controller_read_port_t cmd = { .send = { .command = JOYBUS_COMMAND_ID_N64_CONTROLLER_READ, } }; @@ -583,11 +526,11 @@ static void joypad_read_async(void) const size_t recv_offset = offsetof(typeof(cmd), recv); memcpy(&input[i], &cmd, recv_offset); i += sizeof(cmd); - } - else - { + } break; + default: // Skip this port i += JOYBUS_COMMAND_SKIP_SIZE; + break; } } @@ -604,68 +547,9 @@ static void joypad_read_async(void) /** * @brief Callback for VI interrupt to read and identify Joypads. */ -static void joypad_vi_interrupt_callback(void) +static void joypad_vi_interrupt_callback(void* ctx) { joypad_read_async(); - int32_t ticks_since_identify = TICKS_SINCE(joypad_identify_last_ticks); - if (ticks_since_identify > JOYPAD_IDENTIFY_INTERVAL_TICKS) - { - joypad_identify_async(false); - } -} - -/** - * @brief Callback for NMI/Reset interrupt to stop rumble motors. - */ -static void joypad_reset_interrupt_callback(void) -{ - // BBPlayer does not support rumble - if( sys_bbplayer() ) return; - - const joybus_cmd_n64_accessory_write_port_t n64_motor_cmd = { .send = { - .command = JOYBUS_COMMAND_ID_N64_ACCESSORY_WRITE, - .addr_checksum = joybus_accessory_calculate_addr_checksum(JOYBUS_ACCESSORY_ADDR_RUMBLE_MOTOR), - .data = { 0 }, - } }; - - const joybus_cmd_gcn_controller_read_port_t gcn_motor_cmd = { .send = { - .command = JOYBUS_COMMAND_ID_GCN_CONTROLLER_READ, - .mode = 3, - .rumble = false, - } }; - - JOYPAD_PORT_FOREACH (port) - { - if( !joypad_get_rumble_supported(port) ) continue; - switch( joypad_get_style(port) ) - { - case JOYPAD_STYLE_N64: - joybus_exec_cmd_struct(port, n64_motor_cmd); - break; - case JOYPAD_STYLE_GCN: - joybus_exec_cmd_struct(port, gcn_motor_cmd); - break; - default: - break; - } - } -} - -/** - * @brief Re-identify and reset all Joypads and wait for completion. - */ -static void joypad_reset(void) -{ - ASSERT_JOYPAD_INITIALIZED(); - // Async operations are disabled during reset - if( exception_reset_time() > 0 ) { return; } - - // Wait for pending identify/reset operation to resolve - while (joypad_identify_pending) { /* Spinlock */ } - // Enqueue this identify/reset operation - joypad_identify_async(true); - // Wait for the operation to finish - while (joypad_identify_pending) { /* Spinlock */ } } /** @@ -715,23 +599,33 @@ void joypad_init(void) // Just increment the refcount if already initialized if (joypad_init_refcount++ > 0) { return; } - // Initialize the timer subsystem (or increment its refcount) - timer_init(); + // We depend on the joybus subsystem + joybus_init(); - // Reinitialize all Joypad devices to unknown state + // Initialize the VI subsystem so that we can poll in sync with VI interrupts + vi_init(); + + const joypad_accessory_library_vtable_t *vtable = __joypad_accessory_vtable; + if (vtable && vtable->init) + { + vtable->init(); + } + + // Reset global structures JOYPAD_PORT_FOREACH (port) { - joypad_device_changed(port, JOYBUS_IDENTIFIER_UNKNOWN); + joypad_device_reset(port); } + // Register callbacks for device detection + joybus_register_detection_callback(joypad_detect_callback, NULL); + // Ensure the Joypads are ready immediately after init returns - joypad_reset(); + joybus_detect_now(); joypad_read(); - // Update the Joypads on VI interrupt - register_VI_handler(joypad_vi_interrupt_callback); - // Stop rumble on console reset - register_RESET_handler(joypad_reset_interrupt_callback); + // Update the Joypads on vblank interrupt + vi_install_vblank_handler(joypad_vi_interrupt_callback, NULL); } void joypad_close(void) @@ -739,12 +633,20 @@ void joypad_close(void) // Do nothing if there are still dangling references. if (--joypad_init_refcount > 0) { return; } - // Stop updating the Joypads on VI interrupt - unregister_VI_handler(joypad_vi_interrupt_callback); - unregister_RESET_handler(joypad_reset_interrupt_callback); + // Stop updating the Joypads on vblank interrupt + vi_uninstall_vblank_handler(joypad_vi_interrupt_callback, NULL); + const joypad_accessory_library_vtable_t *vtable = __joypad_accessory_vtable; + if (vtable && vtable->close) + { + vtable->close(); + } + vi_close(); + joybus_close(); +} - // Decrement the timer subsystem refcount (possibly closing it) - timer_close(); +bool __joypad_is_initialized(void) +{ + return joypad_init_refcount > 0; } void joypad_poll(void) @@ -755,13 +657,11 @@ void joypad_poll(void) uint8_t output[JOYBUS_BLOCK_SIZE]; joypad_gcn_origin_t origins[JOYPAD_PORT_COUNT]; - joybus_identifier_t identifiers[JOYPAD_PORT_COUNT]; // Take a snapshot of the current "hot" state disable_interrupts(); memcpy(output, (void *)joypad_read_output, sizeof(output)); memcpy(origins, (void *)joypad_origins_hot, sizeof(origins)); - memcpy(identifiers, (void *)joypad_identifiers_hot, sizeof(identifiers)); enable_interrupts(); uint8_t send_len, recv_len, command_id, command_len; @@ -809,8 +709,9 @@ void joypad_poll(void) cmd = (void *)&output[i + JOYBUS_COMMAND_METADATA_SIZE]; i += JOYBUS_COMMAND_METADATA_SIZE + sizeof(*cmd); - // N64 Mouse uses the same read command as a Controller - if (identifiers[port] == JOYBUS_IDENTIFIER_N64_MOUSE) + // N64 Mouse uses the same read command as a Controller. Ask joybus + // what is the last identified device on this port. + if (joybus_get_identifier(port, NULL) == JOYBUS_IDENTIFIER_N64_MOUSE) { device->style = JOYPAD_STYLE_MOUSE; } @@ -841,8 +742,6 @@ void joypad_poll(void) memset(device, 0, sizeof(*device)); i += command_len; } - // Copy the hot identifier to the cold device state - device->identifier = identifiers[port]; } if (check_origins) joypad_gcn_origin_check_async(); @@ -852,18 +751,7 @@ bool joypad_is_connected(joypad_port_t port) { ASSERT_JOYPAD_INITIALIZED(); ASSERT_JOYPAD_PORT_VALID(port); - switch( joypad_devices_cold[port].identifier ) { - case JOYBUS_IDENTIFIER_NONE: - case JOYBUS_IDENTIFIER_UNKNOWN: return false; - default: return true; - } -} - -joybus_identifier_t joypad_get_identifier(joypad_port_t port) -{ - ASSERT_JOYPAD_INITIALIZED(); - ASSERT_JOYPAD_PORT_VALID(port); - return joypad_devices_cold[port].identifier; + return joypad_devices_cold[port].style != JOYPAD_STYLE_NONE; } joypad_style_t joypad_get_style(joypad_port_t port) @@ -880,20 +768,6 @@ joypad_accessory_type_t joypad_get_accessory_type(joypad_port_t port) return joypad_accessories_hot[port].type; } -int joypad_get_accessory_state(joypad_port_t port) -{ - ASSERT_JOYPAD_INITIALIZED(); - ASSERT_JOYPAD_PORT_VALID(port); - return joypad_accessories_hot[port].state; -} - -int joypad_get_accessory_error(joypad_port_t port) -{ - ASSERT_JOYPAD_INITIALIZED(); - ASSERT_JOYPAD_PORT_VALID(port); - return joypad_accessories_hot[port].error; -} - uint8_t joypad_get_transfer_pak_status(joypad_port_t port) { ASSERT_JOYPAD_INITIALIZED(); @@ -915,28 +789,6 @@ bool joypad_get_rumble_active(joypad_port_t port) return joypad_devices_hot[port].rumble_active; } -void joypad_set_rumble_active(joypad_port_t port, bool active) -{ - ASSERT_JOYPAD_INITIALIZED(); - ASSERT_JOYPAD_PORT_VALID(port); - - // Rumble motor operations are disabled during reset - if( exception_reset_time() > 0 ) { return; } - - disable_interrupts(); - volatile joypad_device_hot_t *device = &joypad_devices_hot[port]; - joypad_rumble_method_t rumble_method = device->rumble_method; - if (rumble_method == JOYPAD_RUMBLE_METHOD_N64_RUMBLE_PAK) - { - joypad_rumble_pak_toggle_async(port, active); - } - else if (rumble_method == JOYPAD_RUMBLE_METHOD_GCN_CONTROLLER) - { - joypad_gcn_controller_rumble_toggle(port, active); - } - enable_interrupts(); -} - joypad_inputs_t joypad_get_inputs(joypad_port_t port) { ASSERT_JOYPAD_INITIALIZED(); diff --git a/src/joybus/joypad_accessory.c b/src/joybus/joypad_accessory.c index 592aba439..77ec759fa 100644 --- a/src/joybus/joypad_accessory.c +++ b/src/joybus/joypad_accessory.c @@ -1,6 +1,7 @@ /** * @file joypad_accessory.c * @author Christopher Bonhage + * @author Giovanni Bajo * @brief Joypad accessory helpers * @ingroup joypad */ @@ -9,10 +10,19 @@ #include "debug.h" #include "interrupt.h" +#include "kernel/kernel_internal.h" +#include "kirq.h" +#include "n64sys.h" +#include "timer.h" #include "joypad_internal.h" +#include "../rand_internal.h" +static void joypad_accessory_library_init(void); +static void joypad_accessory_library_close(void); +static void joypad_rumble_pak_reset_interrupt_callback(void); static void joypad_accessory_detect_read_callback(uint64_t *out_dwords, void *ctx); static void joypad_accessory_detect_write_callback(uint64_t *out_dwords, void *ctx); +static void joypad_accessory_detect_state_machine(joypad_port_t port, const joybus_cmd_n64_accessory_read_port_t *cmdr, const joybus_cmd_n64_accessory_write_port_t *cmdw); static void joypad_transfer_pak_enable_read_callback(uint64_t *out_dwords, void *ctx); static void joypad_transfer_pak_enable_write_callback(uint64_t *out_dwords, void *ctx); static void joypad_transfer_pak_load_read_callback(uint64_t *out_dwords, void *ctx); @@ -26,6 +36,30 @@ static void joypad_transfer_pak_wait_timer_callback(int ovfl, void *ctx); * @{ */ +static const joypad_accessory_library_vtable_t joypad_accessory_library_vtable = { + .init = joypad_accessory_library_init, + .close = joypad_accessory_library_close, + .reset = joypad_accessory_reset, + .detect_async = joypad_accessory_detect_async, +}; + +__attribute__((constructor)) +static void joypad_accessory_library_constructor(void) +{ + __joypad_accessory_vtable = &joypad_accessory_library_vtable; +} + +static void joypad_accessory_library_init(void) +{ + // TODO: this is only required for applications using rumble motors. + register_RESET_handler(joypad_rumble_pak_reset_interrupt_callback); +} + +static void joypad_accessory_library_close(void) +{ + unregister_RESET_handler(joypad_rumble_pak_reset_interrupt_callback); +} + /** * @brief Determine whether the accessory read command was successful. Retry if necessary. * @@ -144,6 +178,13 @@ static bool joypad_accessory_check_write_crc_error( } case JOYBUS_ACCESSORY_IO_STATUS_BAD_CRC: { + if (accessory->ignore_write_crc) + { + // Special case: some buggy accessories always return an invalid data CRC. + // Ignore the CRC error and treat the write operation as successful. + accessory->error = JOYPAD_ACCESSORY_ERROR_NONE; + return false; + } size_t retries = accessory->retries; if (retries < JOYPAD_ACCESSORY_RETRY_LIMIT) { @@ -247,6 +288,10 @@ static void joypad_transfer_pak_wait_timer_callback(int ovfl, void *ctx) accessory->type = JOYPAD_ACCESSORY_TYPE_UNKNOWN; accessory->error = JOYPAD_ACCESSORY_ERROR_UNKNOWN; } + else if (joypad_accessory_state_is_detecting(state)) + { + joypad_accessory_detect_state_machine(port, NULL, NULL); + } else if (state == JOYPAD_ACCESSORY_STATE_TRANSFER_ENABLE_PROBE_WAIT) { uint8_t write_data[JOYBUS_ACCESSORY_DATA_SIZE]; @@ -271,26 +316,18 @@ static void joypad_transfer_pak_wait_timer_callback(int ovfl, void *ctx) } } -/** - * @brief Callback for the accessory read commands used by #joypad_accessory_detect_async. - * - * @param out_dwords Joybus output block - * @param ctx Opaque pointer used to pass the Joypad port number - */ -static void joypad_accessory_detect_read_callback(uint64_t *out_dwords, void *ctx) +static void joypad_accessory_detect_state_machine( + joypad_port_t port, + const joybus_cmd_n64_accessory_read_port_t *cmdr, + const joybus_cmd_n64_accessory_write_port_t *cmdw) { - const uint8_t *out_bytes = (void *)out_dwords; - joypad_port_t port = (joypad_port_t)ctx; volatile joypad_device_hot_t *device = &joypad_devices_hot[port]; volatile joypad_accessory_t *accessory = &joypad_accessories_hot[port]; joypad_accessory_state_t state = accessory->state; - if (!joypad_accessory_state_is_detecting(state)) - { - return; // Unexpected accessory state! - } + uint8_t write_data[JOYBUS_ACCESSORY_DATA_SIZE]; // Cancel accessory detection during reset - if( exception_reset_time() > 0 ) + if(exception_reset_time() > 0) { accessory->state = JOYPAD_ACCESSORY_STATE_IDLE; accessory->type = JOYPAD_ACCESSORY_TYPE_UNKNOWN; @@ -298,42 +335,68 @@ static void joypad_accessory_detect_read_callback(uint64_t *out_dwords, void *ct return; } - uint8_t write_data[JOYBUS_ACCESSORY_DATA_SIZE]; - const joybus_cmd_n64_accessory_read_port_t *cmd = - (void *)&out_bytes[port + JOYBUS_COMMAND_METADATA_SIZE]; - joybus_callback_t retry_callback = joypad_accessory_detect_read_callback; - if (joypad_accessory_check_read_crc_error(port, cmd, retry_callback, ctx)) - { - return; // Accessory communication error! - } - else if (state == JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_BACKUP) + switch (state) { - memcpy((void *)accessory->cpak_label_backup, cmd->recv.data, sizeof(cmd->recv.data)); + case JOYPAD_ACCESSORY_STATE_DETECT_INIT: + // Transfer Pak has been turned off; reset Transfer Pak status + accessory->transfer_pak_status.raw = 0x00; + // Step 2A: Set Controller Pak "linear paging bank" to 0 + memset(write_data, 0, sizeof(write_data)); + accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_CPAK_BANK_WRITE0; + accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; + accessory->retries = 0; + joybus_accessory_write_async( + port, JOYPAD_CONTROLLER_PAK_BANK_SWITCH_ADDRESS, write_data, + joypad_accessory_detect_write_callback, (void*)port + ); + break; + + case JOYPAD_ACCESSORY_STATE_DETECT_CPAK_BANK_WRITE0: + // Step 2B: Backup the Controller Pak "label" area + accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_BACKUP; + accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; + accessory->retries = 0; + joybus_accessory_read_async( + port, JOYBUS_ACCESSORY_ADDR_LABEL, + joypad_accessory_detect_read_callback, (void*)port + ); + break; + + case JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_BACKUP: // Step 2C: Overwrite the Controller Pak "label" area - for (size_t i = 0; i < sizeof(write_data); ++i) write_data[i] = i; - accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_WRITE; + memcpy((void *)accessory->cpak_label_backup, cmdr->recv.data, sizeof(cmdr->recv.data)); + accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_WRITE0; accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; accessory->retries = 0; joybus_accessory_write_async( - port, JOYBUS_ACCESSORY_ADDR_LABEL, write_data, - joypad_accessory_detect_write_callback, ctx + port, JOYBUS_ACCESSORY_ADDR_LABEL, (uint8_t *)accessory->cpak_probe_label, + joypad_accessory_detect_write_callback, (void*)port ); - } - else if (state == JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_READ) - { + break; + + case JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_WRITE0: + // Step 2D: Read back the "label" area to detect Controller Pak + accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_READ0; + accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; + accessory->retries = 0; + joybus_accessory_read_async( + port, JOYBUS_ACCESSORY_ADDR_LABEL, + joypad_accessory_detect_read_callback, (void*)port + ); + break; + + case JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_READ0: // Compare the expected label with what was actually read back - for (size_t i = 0; i < sizeof(write_data); ++i) write_data[i] = i; - if (memcmp(cmd->recv.data, write_data, sizeof(write_data)) == 0) + if (memcmp(cmdr->recv.data, (uint8_t *)accessory->cpak_probe_label, sizeof(accessory->cpak_probe_label)) == 0) { - // Step 2E: Restore the Controller Pak "label" area - memcpy(write_data, (void *)accessory->cpak_label_backup, sizeof(write_data)); - accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_RESTORE; + // Step 2E: Set Controller Pak "linear paging bank" to 1 + memset(write_data, 1, sizeof(write_data)); + accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_CPAK_BANK_WRITE1; accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; accessory->retries = 0; joybus_accessory_write_async( - port, JOYBUS_ACCESSORY_ADDR_LABEL, write_data, - joypad_accessory_detect_write_callback, ctx - ); + port, JOYPAD_CONTROLLER_PAK_BANK_SWITCH_ADDRESS, write_data, + joypad_accessory_detect_write_callback, (void*)port); } else { @@ -344,13 +407,74 @@ static void joypad_accessory_detect_read_callback(uint64_t *out_dwords, void *ct accessory->retries = 0; joybus_accessory_write_async( port, JOYBUS_ACCESSORY_ADDR_PROBE, write_data, - joypad_accessory_detect_write_callback, ctx + joypad_accessory_detect_write_callback, (void*)port ); } - } - else if (state == JOYPAD_ACCESSORY_STATE_DETECT_RUMBLE_PROBE_READ) - { - uint8_t probe_value = cmd->recv.data[0]; + break; + + case JOYPAD_ACCESSORY_STATE_DETECT_CPAK_BANK_WRITE1: + // Step 2F: Read back the "label" area of the bank1 + accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_READ1; + accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; + accessory->retries = 0; + joybus_accessory_read_async( + port, JOYBUS_ACCESSORY_ADDR_LABEL, + joypad_accessory_detect_read_callback, (void*)port + ); + break; + + case JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_READ1: + // Step 2G: If the label area was corrupted by bankswitch, the Controller Pak is not multi-bank + // Single-pak Controller Paks will have misinterprted the bank switch command + // as a block #0 write, so we will find here 010101010101010101010. + memset(write_data, 1, sizeof(write_data)); + if (memcmp(cmdr->recv.data, write_data, sizeof(write_data)) != 0) { + accessory->cpak_bankswitching = true; + // Step 2H: switch back to bank0 + memset(write_data, 0, sizeof(write_data)); + accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_CPAK_BANK_WRITE0_AGAIN; + accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; + accessory->retries = 0; + joybus_accessory_write_async( + port, JOYPAD_CONTROLLER_PAK_BANK_SWITCH_ADDRESS, write_data, + joypad_accessory_detect_write_callback, (void*)port + ); + break; + } + accessory->cpak_bankswitching = false; + /* fallthrough: since bankswitching never happened, we're ready to restore the label */ + case JOYPAD_ACCESSORY_STATE_DETECT_CPAK_BANK_WRITE0_AGAIN: + // Step 2H: Restore the Controller Pak "label" area + memcpy(write_data, (void *)accessory->cpak_label_backup, sizeof(write_data)); + accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_RESTORE; + accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; + accessory->retries = 0; + joybus_accessory_write_async( + port, JOYBUS_ACCESSORY_ADDR_LABEL, write_data, + joypad_accessory_detect_write_callback, (void*)port + ); + break; + + case JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_RESTORE: + // Success: Controller Pak detected + accessory->state = JOYPAD_ACCESSORY_STATE_IDLE; + accessory->type = JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK; + accessory->cpak_curbank = 0; // Bank 0 is currently selected + break; + + case JOYPAD_ACCESSORY_STATE_DETECT_RUMBLE_PROBE_WRITE: + // Step 3B: Read probe value to detect Rumble Pak + accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_RUMBLE_PROBE_READ; + accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; + accessory->retries = 0; + joybus_accessory_read_async( + port, JOYBUS_ACCESSORY_ADDR_PROBE, + joypad_accessory_detect_read_callback, (void*)port + ); + break; + + case JOYPAD_ACCESSORY_STATE_DETECT_RUMBLE_PROBE_READ: { + uint8_t probe_value = cmdr->recv.data[0]; if (probe_value == JOYBUS_ACCESSORY_PROBE_RUMBLE_PAK) { // Success: Probe reports that this is a Rumble Pak @@ -373,16 +497,35 @@ static void joypad_accessory_detect_read_callback(uint64_t *out_dwords, void *ct accessory->retries = 0; joybus_accessory_write_async( port, JOYBUS_ACCESSORY_ADDR_PROBE, write_data, - joypad_accessory_detect_write_callback, ctx + joypad_accessory_detect_write_callback, (void*)port ); } - } - else if (state == JOYPAD_ACCESSORY_STATE_DETECT_TRANSFER_PROBE_READ) - { - uint8_t probe_value = cmd->recv.data[0]; + } break; + + case JOYPAD_ACCESSORY_STATE_DETECT_TRANSFER_PROBE_ON: + // Step 4B: Wait for Transfer Pak to power on + accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_TRANSFER_PROBE_WAIT; + accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; + accessory->retries = 0; + restart_timer(accessory->transfer_pak_wait_timer); + break; + + case JOYPAD_ACCESSORY_STATE_DETECT_TRANSFER_PROBE_WAIT: + // Step 4C: Read probe value to detect Transfer Pak + accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_TRANSFER_PROBE_READ; + accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; + accessory->retries = 0; + joybus_accessory_read_async( + port, JOYBUS_ACCESSORY_ADDR_PROBE, + joypad_accessory_detect_read_callback, (void*)port + ); + break; + + case JOYPAD_ACCESSORY_STATE_DETECT_TRANSFER_PROBE_READ: { + uint8_t probe_value = cmdr->recv.data[0]; if (probe_value == JOYBUS_ACCESSORY_PROBE_TRANSFER_PAK_ON) { - // Step 4C: Write probe value to turn off Transfer Pak + // Step 4D: Write probe value to turn off Transfer Pak memset(write_data, JOYBUS_ACCESSORY_PROBE_TRANSFER_PAK_OFF, sizeof(write_data)); accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_TRANSFER_PROBE_OFF; accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; @@ -401,13 +544,31 @@ static void joypad_accessory_detect_read_callback(uint64_t *out_dwords, void *ct accessory->retries = 0; joybus_accessory_write_async( port, JOYBUS_ACCESSORY_ADDR_PROBE, write_data, - joypad_accessory_detect_write_callback, ctx + joypad_accessory_detect_write_callback, (void *)port ); } - } - else if (state == JOYPAD_ACCESSORY_STATE_DETECT_SNAP_PROBE_READ) - { - uint8_t probe_value = cmd->recv.data[0]; + } break; + + case JOYPAD_ACCESSORY_STATE_DETECT_TRANSFER_PROBE_OFF: + // Success: Transfer Pak has been probed and powered off + accessory->state = JOYPAD_ACCESSORY_STATE_IDLE; + accessory->type = JOYPAD_ACCESSORY_TYPE_TRANSFER_PAK; + accessory->transfer_pak_status.power = 0; + break; + + case JOYPAD_ACCESSORY_STATE_DETECT_SNAP_PROBE_WRITE: + // Step 5B: Read probe value to detect Snap Station + accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_SNAP_PROBE_READ; + accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; + accessory->retries = 0; + joybus_accessory_read_async( + port, JOYBUS_ACCESSORY_ADDR_PROBE, + joypad_accessory_detect_read_callback, (void*)port + ); + break; + + case JOYPAD_ACCESSORY_STATE_DETECT_SNAP_PROBE_READ: { + uint8_t probe_value = cmdr->recv.data[0]; if (probe_value == JOYBUS_ACCESSORY_PROBE_SNAP_STATION) { // Success: Probe reports that this is a Snap Station @@ -421,9 +582,36 @@ static void joypad_accessory_detect_read_callback(uint64_t *out_dwords, void *ct accessory->type = JOYPAD_ACCESSORY_TYPE_UNKNOWN; accessory->transfer_pak_status.raw = 0x00; } + } break; + + default: + assertf(false, "Unknown joypad_accessory_state_t value: %d", state); + accessory->state = JOYPAD_ACCESSORY_STATE_IDLE; + accessory->type = JOYPAD_ACCESSORY_TYPE_UNKNOWN; + accessory->error = JOYPAD_ACCESSORY_ERROR_UNKNOWN; + break; } } +/** + * @brief Callback for the accessory read commands used by #joypad_accessory_detect_async. + * + * @param out_dwords Joybus output block + * @param ctx Opaque pointer used to pass the Joypad port number + */ +static void joypad_accessory_detect_read_callback(uint64_t *out_dwords, void *ctx) +{ + const uint8_t *out_bytes = (void *)out_dwords; + joypad_port_t port = (joypad_port_t)ctx; + + const joybus_cmd_n64_accessory_read_port_t *cmdr = + (void *)&out_bytes[port + JOYBUS_COMMAND_METADATA_SIZE]; + joybus_callback_t retry_callback = joypad_accessory_detect_read_callback; + if (joypad_accessory_check_read_crc_error(port, cmdr, retry_callback, ctx)) + return; // Accessory communication error! + joypad_accessory_detect_state_machine(port, cmdr, NULL); +} + /** * @brief Callback for the accessory write commands used by #joypad_accessory_detect_async. * @@ -434,111 +622,21 @@ static void joypad_accessory_detect_write_callback(uint64_t *out_dwords, void *c { const uint8_t *out_bytes = (void *)out_dwords; joypad_port_t port = (joypad_port_t)ctx; - volatile joypad_accessory_t *accessory = &joypad_accessories_hot[port]; - joypad_accessory_state_t state = accessory->state; - if (!joypad_accessory_state_is_detecting(state)) - { - return; // Unexpected accessory state! - } - // Cancel accessory detection during reset - if( exception_reset_time() > 0 ) + const joybus_cmd_n64_accessory_write_port_t *cmdw = + (void *)&out_bytes[port + JOYBUS_COMMAND_METADATA_SIZE]; + + if (cmdw->recv.data_crc == 0x00 && joybus_accessory_calculate_data_crc(cmdw->send.data) != 0x00) { - accessory->state = JOYPAD_ACCESSORY_STATE_IDLE; - accessory->type = JOYPAD_ACCESSORY_TYPE_UNKNOWN; - accessory->error = JOYPAD_ACCESSORY_ERROR_UNKNOWN; - return; + // Special case: some accessories (notably 8BitDo receivers on old firmware) always return an invalid data CRC of 0x00. + // If this buggy behavior is detected, ignore all write CRC errors for this device from now on. + joypad_accessories_hot[port].ignore_write_crc = true; } - const joybus_cmd_n64_accessory_write_port_t *cmd = - (void *)&out_bytes[port + JOYBUS_COMMAND_METADATA_SIZE]; joybus_callback_t retry_callback = joypad_accessory_detect_write_callback; - if (joypad_accessory_check_write_crc_error(port, cmd, retry_callback, ctx)) - { + if (joypad_accessory_check_write_crc_error(port, cmdw, retry_callback, ctx)) return; // Accessory communication error! - } - else if (state == JOYPAD_ACCESSORY_STATE_DETECT_INIT) - { - // Transfer Pak has been turned off; reset Transfer Pak status - accessory->transfer_pak_status.raw = 0x00; - // Step 2A: Set Controller Pak "linear paging bank" to 0 - uint8_t data[JOYBUS_ACCESSORY_DATA_SIZE] = {0}; - accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_CPAK_BANK_WRITE; - accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; - accessory->retries = 0; - joybus_accessory_write_async( - port, JOYPAD_CONTROLLER_PAK_BANK_SWITCH_ADDRESS, data, - joypad_accessory_detect_write_callback, ctx - ); - } - else if (state == JOYPAD_ACCESSORY_STATE_DETECT_CPAK_BANK_WRITE) - { - // Step 2B: Backup the Controller Pak "label" area - accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_BACKUP; - accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; - accessory->retries = 0; - joybus_accessory_read_async( - port, JOYBUS_ACCESSORY_ADDR_LABEL, - joypad_accessory_detect_read_callback, ctx - ); - } - else if (state == JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_WRITE) - { - // Step 2D: Read back the "label" area to detect Controller Pak - accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_READ; - accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; - accessory->retries = 0; - joybus_accessory_read_async( - port, JOYBUS_ACCESSORY_ADDR_LABEL, - joypad_accessory_detect_read_callback, ctx - ); - } - else if (state == JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_RESTORE) - { - // Success: Controller Pak detected - accessory->state = JOYPAD_ACCESSORY_STATE_IDLE; - accessory->type = JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK; - } - else if (state == JOYPAD_ACCESSORY_STATE_DETECT_RUMBLE_PROBE_WRITE) - { - // Step 3B: Read probe value to detect Rumble Pak - accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_RUMBLE_PROBE_READ; - accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; - accessory->retries = 0; - joybus_accessory_read_async( - port, JOYBUS_ACCESSORY_ADDR_PROBE, - joypad_accessory_detect_read_callback, ctx - ); - } - else if (state == JOYPAD_ACCESSORY_STATE_DETECT_TRANSFER_PROBE_ON) - { - // Step 4B: Read probe value to detect Transfer Pak - accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_TRANSFER_PROBE_READ; - accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; - accessory->retries = 0; - joybus_accessory_read_async( - port, JOYBUS_ACCESSORY_ADDR_PROBE, - joypad_accessory_detect_read_callback, ctx - ); - } - else if (state == JOYPAD_ACCESSORY_STATE_DETECT_TRANSFER_PROBE_OFF) - { - // Success: Transfer Pak has been probed and powered off - accessory->state = JOYPAD_ACCESSORY_STATE_IDLE; - accessory->type = JOYPAD_ACCESSORY_TYPE_TRANSFER_PAK; - accessory->transfer_pak_status.power = 0; - } - else if (state == JOYPAD_ACCESSORY_STATE_DETECT_SNAP_PROBE_WRITE) - { - // Step 5B: Read probe value to detect Snap Station - accessory->state = JOYPAD_ACCESSORY_STATE_DETECT_SNAP_PROBE_READ; - accessory->error = JOYPAD_ACCESSORY_ERROR_PENDING; - accessory->retries = 0; - joybus_accessory_read_async( - port, JOYBUS_ACCESSORY_ADDR_PROBE, - joypad_accessory_detect_read_callback, ctx - ); - } + joypad_accessory_detect_state_machine(port, NULL, cmdw); } void joypad_accessory_detect_async(joypad_port_t port) @@ -553,6 +651,12 @@ void joypad_accessory_detect_async(joypad_port_t port) { joypad_transfer_pak_wait_timer_init(port); } + + // Create a random label for the Controller Pak probe + // Avoid generating a probe label with a data CRC of 0x00 + do { __rand((uint8_t *)accessory->cpak_probe_label, sizeof(accessory->cpak_probe_label)); } + while (joybus_accessory_calculate_data_crc((uint8_t *)accessory->cpak_probe_label) == 0x00); + // Don't interrupt other accessory operations if they are still running if (accessory->state == JOYPAD_ACCESSORY_STATE_IDLE) { @@ -569,6 +673,43 @@ void joypad_accessory_detect_async(joypad_port_t port) } } +/** + * @brief Callback for NMI/Reset interrupt to stop rumble motors. + */ +static void joypad_rumble_pak_reset_interrupt_callback(void) +{ + // BBPlayer does not support rumble. + if (sys_bbplayer()) return; + + const joybus_cmd_n64_accessory_write_port_t n64_motor_cmd = { .send = { + .command = JOYBUS_COMMAND_ID_N64_ACCESSORY_WRITE, + .addr_checksum = joybus_accessory_calculate_addr_checksum(JOYBUS_ACCESSORY_ADDR_RUMBLE_MOTOR), + .data = { 0 }, + } }; + const joybus_cmd_gcn_controller_read_port_t gcn_motor_cmd = { .send = { + .command = JOYBUS_COMMAND_ID_GCN_CONTROLLER_READ, + .mode = 3, + .rumble = false, + } }; + + JOYPAD_PORT_FOREACH (port) + { + volatile joypad_device_hot_t *device = &joypad_devices_hot[port]; + if (device->rumble_method == JOYPAD_RUMBLE_METHOD_NONE) continue; + switch (device->style) + { + case JOYPAD_STYLE_N64: + joybus_exec_cmd_struct(port, n64_motor_cmd); + break; + case JOYPAD_STYLE_GCN: + joybus_exec_cmd_struct(port, gcn_motor_cmd); + break; + default: + break; + } + } +} + /** * @brief Callback for the accessory write commands used by #joypad_rumble_pak_toggle_async. * @@ -621,14 +762,35 @@ void joypad_rumble_pak_toggle_async(joypad_port_t port, bool active) ); } +void joypad_set_rumble_active(joypad_port_t port, bool active) +{ + assertf(__joypad_is_initialized(), "joypad_init() was not called"); + ASSERT_JOYPAD_PORT_VALID(port); + + // Rumble motor operations are disabled during reset. + if (exception_reset_time() > 0) { return; } + + disable_interrupts(); + volatile joypad_device_hot_t *device = &joypad_devices_hot[port]; + joypad_rumble_method_t rumble_method = device->rumble_method; + if (rumble_method == JOYPAD_RUMBLE_METHOD_N64_RUMBLE_PAK) + { + joypad_rumble_pak_toggle_async(port, active); + } + else if (rumble_method == JOYPAD_RUMBLE_METHOD_GCN_CONTROLLER) + { + __joypad_gcn_controller_rumble_toggle(port, active); + } + enable_interrupts(); +} + static void joypad_accessory_read_callback(uint64_t *out_dwords, void *ctx) { const uint8_t *out_bytes = (void *)out_dwords; joypad_port_t port = (joypad_port_t)ctx; volatile joypad_accessory_t *accessory = &joypad_accessories_hot[port]; volatile joypad_accessory_io_t *io = &accessory->io; - joypad_accessory_state_t state = accessory->state; - assert(state == JOYPAD_ACCESSORY_STATE_READ); + assert(accessory->state == JOYPAD_ACCESSORY_STATE_READ); const joybus_cmd_n64_accessory_read_port_t *cmd = (void *)&out_bytes[port + JOYBUS_COMMAND_METADATA_SIZE]; @@ -764,7 +926,10 @@ void joypad_accessory_xfer_async( // We can only handle one async transfer at a time. If the accessory is // busy, we will wait for it to become idle before starting the new transfer. - while (accessory->state != JOYPAD_ACCESSORY_STATE_IDLE) {} + kirq_wait_t w = kirq_begin_wait_si(); + while (accessory->state != JOYPAD_ACCESSORY_STATE_IDLE) { + if (__kernel) kirq_wait(&w); + } accessory->io = (joypad_accessory_io_t){ .start = dst, @@ -823,22 +988,40 @@ joypad_accessory_error_t joypad_accessory_xfer( done = true; } + kirq_wait_t w = kirq_begin_wait_si(); joypad_accessory_xfer_async(port, xfer, start_addr, dst, len, callback, NULL); - while (!done) {} + + while (!done) { + if (__kernel) kirq_wait(&w); + } return error; } +bool joypad_controller_pak_supports_bankswitching(joypad_port_t port) +{ + ASSERT_JOYPAD_PORT_VALID(port); + volatile joypad_accessory_t *accessory = &joypad_accessories_hot[port]; + + if (accessory->type != JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK) + return false; // This is not a Controller Pak! + + return accessory->cpak_bankswitching; +} + joypad_accessory_error_t joypad_controller_pak_set_bank(joypad_port_t port, uint8_t bank) { ASSERT_JOYPAD_PORT_VALID(port); volatile joypad_accessory_t *accessory = &joypad_accessories_hot[port]; if (accessory->type != JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK) - { return JOYPAD_ACCESSORY_ERROR_ABSENT; // This is not a Controller Pak! - } - + if (!accessory->cpak_bankswitching) + return JOYPAD_ACCESSORY_ERROR_CONTROLLER_PAK_BANK_SWITCH; + if (accessory->cpak_curbank == bank) + return JOYPAD_ACCESSORY_ERROR_NONE; // Already in the requested bank + + accessory->cpak_curbank = bank; uint8_t data[32]; memset(data, bank, sizeof(data)); return joypad_accessory_xfer(port, JOYPAD_ACCESSORY_XFER_WRITE, diff --git a/src/joybus/joypad_accessory.h b/src/joybus/joypad_accessory_internal.h similarity index 77% rename from src/joybus/joypad_accessory.h rename to src/joybus/joypad_accessory_internal.h index a26125108..17230460c 100644 --- a/src/joybus/joypad_accessory.h +++ b/src/joybus/joypad_accessory_internal.h @@ -1,6 +1,7 @@ /** - * @file joypad_accessory.h + * @file joypad_accessory_internal.h * @author Christopher Bonhage + * @author Giovanni Bajo * @brief Joypad accessory helpers * @ingroup joypad */ @@ -13,7 +14,10 @@ #include "joybus_accessory_internal.h" #include "joypad.h" -#include "timer.h" + +///@cond +typedef struct timer_link timer_link_t; +///@endcond /** * @addtogroup joypad @@ -33,14 +37,18 @@ typedef enum JOYPAD_ACCESSORY_STATE_IDLE = 0, // Accessory detection routine states JOYPAD_ACCESSORY_STATE_DETECT_INIT, - JOYPAD_ACCESSORY_STATE_DETECT_CPAK_BANK_WRITE, + JOYPAD_ACCESSORY_STATE_DETECT_CPAK_BANK_WRITE0, JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_BACKUP, - JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_WRITE, - JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_READ, + JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_WRITE0, + JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_READ0, + JOYPAD_ACCESSORY_STATE_DETECT_CPAK_BANK_WRITE1, + JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_READ1, + JOYPAD_ACCESSORY_STATE_DETECT_CPAK_BANK_WRITE0_AGAIN, JOYPAD_ACCESSORY_STATE_DETECT_CPAK_LABEL_RESTORE, JOYPAD_ACCESSORY_STATE_DETECT_RUMBLE_PROBE_WRITE, JOYPAD_ACCESSORY_STATE_DETECT_RUMBLE_PROBE_READ, JOYPAD_ACCESSORY_STATE_DETECT_TRANSFER_PROBE_ON, + JOYPAD_ACCESSORY_STATE_DETECT_TRANSFER_PROBE_WAIT, JOYPAD_ACCESSORY_STATE_DETECT_TRANSFER_PROBE_READ, JOYPAD_ACCESSORY_STATE_DETECT_TRANSFER_PROBE_OFF, JOYPAD_ACCESSORY_STATE_DETECT_SNAP_PROBE_WRITE, @@ -96,6 +104,7 @@ typedef enum JOYPAD_ACCESSORY_ERROR_NONE = 0, ///< No error JOYPAD_ACCESSORY_ERROR_ABSENT, ///< Accessory is absent JOYPAD_ACCESSORY_ERROR_CHECKSUM, ///< Checksum error + JOYPAD_ACCESSORY_ERROR_CONTROLLER_PAK_BANK_SWITCH, ///< Controller Pak does not support bank switching JOYPAD_ACCESSORY_ERROR_TRANSFER_PAK_STATUS_CHANGE, ///< Transfer Pak status changed JOYPAD_ACCESSORY_ERROR_UNKNOWN, ///< Unknown error } joypad_accessory_error_t; @@ -145,7 +154,11 @@ typedef struct joypad_accessory_s joypad_accessory_state_t state; joypad_accessory_error_t error; unsigned retries; + bool ignore_write_crc; ///< Should write CRC errors be ignored? uint8_t cpak_label_backup[JOYBUS_ACCESSORY_DATA_SIZE]; + uint8_t cpak_probe_label[JOYBUS_ACCESSORY_DATA_SIZE]; + bool cpak_bankswitching; ///< Does Controller Pak support bankswitching? + int cpak_curbank; ///< Current Controller Pak bank joypad_accessory_io_t io; timer_link_t *transfer_pak_wait_timer; joybus_transfer_pak_status_t transfer_pak_status; @@ -167,12 +180,21 @@ void joypad_accessory_reset(joypad_port_t port); * * Step 2B: Backup the Controller Pak "label" area * * Step 2C: Overwrite the Controller Pak "label" area * * Step 2D: Read back the "label" area to detect Controller Pak - * * Step 2E: Restore the Controller Pak "label" area + * If matches, it is a Controller Pak. Continue to Step 2E, + * to check for multi-bank Controller Paks. + * If not matches, it is not a Controller Pak. Continue to Step 3A. + * * Step 2E: Set Controller Pak "linear paging bank" to 1 + * * Step 2F: Read back the "label" area + * * Step 2G: If the label area was corrupted by bankswitch, the Controller Pak + * does not support bankswitching. + * Otherwise switch back to "linear paging bank" 0. + * * Step 2H: Restore the original label area. Done: Controller Pak detected. * * Step 3A: Write probe value to detect Rumble Pak * * Step 3B: Read probe value to detect Rumble Pak * * Step 4A: Write probe value to detect Transfer Pak - * * Step 4B: Read probe value to detect Transfer Pak - * * Step 4C: Write probe value to turn off Transfer Pak + * * Step 4B: Wait for Transfer Pak to power on + * * Step 4C: Read probe value to detect Transfer Pak + * * Step 4D: Write probe value to turn off Transfer Pak * * Step 5A: Write probe value to detect Snap Station * * Step 5B: Read probe value to detect Snap Station * @@ -242,7 +264,7 @@ void joypad_accessory_xfer_async( * There is no alignment requirement for this address. * @param dst Destination buffer to read accessory data into. * @param len Number of bytes to read. Any number of bytes can be read. - * @return joypad_accessory_error_t Error code indicating the result of the read operation. + * @return Error code indicating the result of the read operation. */ joypad_accessory_error_t joypad_accessory_xfer( joypad_port_t port, @@ -253,27 +275,47 @@ joypad_accessory_error_t joypad_accessory_xfer( ); /** - * @brief Select the active bank for a Controller Pak. + * @brief Return true if the Controller Pak supports bankswitching. + * + * Most controller paks (including all first-party ones) support a bankswitching + * protocol, even if they only have only one 32 KiB bank installed. However, some + * third-party controller paks did not bother implement the bankswitching + * protocol, and in fact trying to change bank can cause data corruption. * - * Most controller paks (including all first-party ones) have a single bank - * of 32 KiB of storage. However, some third-party controller paks have - * multiple banks, and require an explicit bank switch operation to access - * data beyond the first 32 KiB. + * During the initial accessory detection, the Controller Pak is probed to verify + * if it supports bankswitching or not. This function returns the result of that probe. + * + * Notice that the joypad accessory detection routine does not perform any + * write tests to determine the number of banks in a Controller Pak. It can + * only determine if the Controller Pak supports bankswitching or not, but not how + * many banks it has. + * + * @param port Joypad port number (#joypad_port_t) + * @return true The Controller Pak supports bankswitching + * @return false The Controller Pak does not support bankswitching + */ +bool joypad_controller_pak_supports_bankswitching(joypad_port_t port); + +/** + * @brief Select the active bank for a Controller Pak. * * Generic transfer functions for accessories like #joypad_accessory_xfer_async * will only access the active bank. * - * There is no way to probe the number of banks in a Controller Pak at the - * hardware level. In situation where probing is necessary (eg: formatting - * functions), write tests can be performed to determine the number of banks. + * This function will return #JOYPAD_ACCESSORY_ERROR_CONTROLLER_PAK_BANK_SWITCH + * if the Controller Pak is not multi-bank, as issuing a bank switch command to + * a single-bank Controller Pak would result in data corruption. + * + * Notice that since the actual number of banks in a Controller Pak is not + * known, this function cannot check if the bank number is valid. Setting an + * invalid bank number will result in undefined behavior. * * @param port Joypad port number (#joypad_port_t) * @param bank Bank number to switch to. - * @return joypad_accessory_error_t Error code for the transfer operation. + * @return Error code for the transfer operation. */ joypad_accessory_error_t joypad_controller_pak_set_bank(joypad_port_t port, uint8_t bank); - /** * @brief Turn the Rumble Pak motor on or off for a Joypad port. * diff --git a/src/joybus/joypad_internal.h b/src/joybus/joypad_internal.h index 457fdcc1e..4905e9ae7 100644 --- a/src/joybus/joypad_internal.h +++ b/src/joybus/joypad_internal.h @@ -1,6 +1,7 @@ /** * @file joypad_internal.h * @author Christopher Bonhage + * @author Giovanni Bajo * @brief Joypad internal * @ingroup joypad */ @@ -12,7 +13,7 @@ #include #include -#include "joypad_accessory.h" +#include "joypad_accessory_internal.h" #include "utils.h" /** @@ -84,8 +85,6 @@ typedef struct joypad_gcn_origin_s /** @brief "Cold" (non-volatile) Joypad device structure. */ typedef struct joypad_device_cold_s { - /** @brief Joybus device type identifier. */ - joybus_identifier_t identifier; /** @brief Joypad style. */ joypad_style_t style; /** @brief Joypad inputs for current frame. */ @@ -105,7 +104,6 @@ typedef struct joypad_device_hot_s bool rumble_active; } joypad_device_hot_t; -extern volatile joybus_identifier_t joypad_identifiers_hot[JOYPAD_PORT_COUNT]; extern volatile joypad_device_hot_t joypad_devices_hot[JOYPAD_PORT_COUNT]; extern volatile joypad_gcn_origin_t joypad_origins_hot[JOYPAD_PORT_COUNT]; extern volatile joypad_accessory_t joypad_accessories_hot[JOYPAD_PORT_COUNT]; @@ -125,22 +123,38 @@ extern volatile joypad_accessory_t joypad_accessories_hot[JOYPAD_PORT_COUNT]; */ joypad_inputs_t joypad_read_n64_inputs(joypad_port_t port); +/** + * @brief Joypad accessory library vtable. + * + * This structure is used to allow avoid linking the Joypad accessory library + * into the application if it is not needed. WHen the library is linked, the + * __joypad_accessory_vtable will be set to a valid vtable by a constructor. + */ +typedef struct { + /** @brief Initialize the accessory library. */ + void (*init)(void); + + /** @brief Close the accessory library. */ + void (*close)(void); + + /** @brief Reset accessory state for a given port. */ + void (*reset)(joypad_port_t port); + + /** @brief Detect the accessory on a given port, asynchronously. */ + void (*detect_async)(joypad_port_t port); +} joypad_accessory_library_vtable_t; + +extern const joypad_accessory_library_vtable_t *__joypad_accessory_vtable; + /** - * @brief Get the Joypad accessory state for a Joypad port. - * - * @param port Joypad port number (#joypad_port_t) - * - * @return Joypad accessory state enumeration value + * @brief Returns true if joypad_init has been called. */ -int joypad_get_accessory_state(joypad_port_t port); +bool __joypad_is_initialized(void); /** - * @brief Get the Joypad accessory error for a Joypad port. - * - * @param port Joypad port number (#joypad_port_t) - * @return Joypad accessory error enumeration value + * @brief Toggle GameCube rumble and keep cached read command in sync. */ -int joypad_get_accessory_error(joypad_port_t port); +void __joypad_gcn_controller_rumble_toggle(joypad_port_t port, bool active); #ifdef __cplusplus } diff --git a/src/joybus/libdragon.mk b/src/joybus/libdragon.mk index 2fe92f806..8b324114e 100644 --- a/src/joybus/libdragon.mk +++ b/src/joybus/libdragon.mk @@ -1,10 +1,17 @@ LIBDRAGON_OBJS += \ + $(BUILD_DIR)/joybus/bio_sensor.o \ $(BUILD_DIR)/joybus/controller.o \ + $(BUILD_DIR)/joybus/cpak.o \ + $(BUILD_DIR)/joybus/cpakfs.o \ + $(BUILD_DIR)/joybus/cpakfs_path.o \ + $(BUILD_DIR)/joybus/cpakfs_fsck.o \ $(BUILD_DIR)/joybus/eeprom.o \ $(BUILD_DIR)/joybus/eepromfs.o \ $(BUILD_DIR)/joybus/joybus_accessory.o \ + $(BUILD_DIR)/joybus/joybus_rtc.o \ $(BUILD_DIR)/joybus/joybus.o \ + $(BUILD_DIR)/joybus/joyframe.o \ $(BUILD_DIR)/joybus/joypad_accessory.o \ $(BUILD_DIR)/joybus/joypad.o \ $(BUILD_DIR)/joybus/mempak.o \ diff --git a/src/joybus/mempak.c b/src/joybus/mempak.c index a1f0b5b51..1368d9455 100644 --- a/src/joybus/mempak.c +++ b/src/joybus/mempak.c @@ -5,10 +5,22 @@ * @ingroup controllerpak */ #include -#include "regsinternal.h" +#include #include -#include "controller.h" +#include "kernel/kernel_internal.h" +#include "kirq.h" +#include "regsinternal.h" +#include "joybus.h" +#include "joybus_accessory.h" +#include "joypad_accessory_internal.h" #include "mempak.h" +#include "cpak.h" +#include "cpakfs.h" + +// Disable deprecation warnings for this file +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + /** * @name Inode values @@ -30,14 +42,22 @@ int read_mempak_sector( int controller, int sector, uint8_t *sector_data ) if( sector < 0 || sector >= 128 ) { return -1; } if( sector_data == 0 ) { return -1; } - /* Sectors are 256 bytes, a Controller Pak reads 32 bytes at a time */ - for( int i = 0; i < 8; i++ ) - { - if( read_mempak_address( controller, (sector * MEMPAK_BLOCK_SIZE) + (i * 32), sector_data + (i * 32) ) ) - { - /* Failed to read a block */ - return -2; - } + // NOTE: don't use cpak_read here, because this function has historically + // never supported bankswitching, so it would work on the "current bank". + // We can't know if somebody implemented bankswitching manually around it, + // so we keep the old behavior of accessing the current pak. To do so, + // we use the internal API directly (as the public API doesn't support + // accessing implicitly the current bank, for good reasons) + joypad_accessory_error_t result = joypad_accessory_xfer( + controller, + JOYPAD_ACCESSORY_XFER_READ, + sector * MEMPAK_BLOCK_SIZE, + sector_data, + MEMPAK_BLOCK_SIZE + ); + + if (result != JOYPAD_ACCESSORY_ERROR_NONE) { + return -2; } return 0; @@ -48,14 +68,22 @@ int write_mempak_sector( int controller, int sector, uint8_t *sector_data ) if( sector < 0 || sector >= 128 ) { return -1; } if( sector_data == 0 ) { return -1; } - /* Sectors are 256 bytes, a Controller Pak writes 32 bytes at a time */ - for( int i = 0; i < 8; i++ ) - { - if( write_mempak_address( controller, (sector * MEMPAK_BLOCK_SIZE) + (i * 32), sector_data + (i * 32) ) ) - { - /* Failed to read a block */ - return -2; - } + // NOTE: don't use cpak_write here, because this function has historically + // never supported bankswitching, so it would work on the "current bank". + // We can't know if somebody implemented bankswitching manually around it, + // so we keep the old behavior of accessing the current pak. To do so, + // we use the internal API directly (as the public API doesn't support + // accessing implicitly the current bank, for good reasons) + joypad_accessory_error_t result = joypad_accessory_xfer( + controller, + JOYPAD_ACCESSORY_XFER_WRITE, + sector * MEMPAK_BLOCK_SIZE, + sector_data, + MEMPAK_BLOCK_SIZE + ); + + if (result != JOYPAD_ACCESSORY_ERROR_NONE) { + return -2; } return 0; @@ -165,66 +193,86 @@ static int __validate_toc( uint8_t *sector ) } /** - * @brief Convert a Controller Pak character to ASCII + * @brief Convert a Controller Pak character to UTF-8 + * + * The codepage used by the controller pak contains a subset of ASCII and + * some Katakana. * * @param[in] c * A character read from a Controller Pak entry title + * @param[out] out + * Output buffer to write the bytes to (at least 3 bytes). * - * @return ASCII equivalent of character read + * @return The number of bytes written to the output buffer */ -static char __n64_to_ascii( char c ) +static int __n64_to_utf8( uint8_t c, char *out ) { /* Miscelaneous chart */ switch( c ) { case 0x00: - return 0; + *out++ = 0; return 1; case 0x0F: - return ' '; + *out++ = ' '; return 1; case 0x34: - return '!'; + *out++ = '!'; return 1; case 0x35: - return '\"'; + *out++ = '\"'; return 1; case 0x36: - return '#'; + *out++ = '#'; return 1; case 0x37: - return '`'; + *out++ = '`'; return 1; case 0x38: - return '*'; + *out++ = '*'; return 1; case 0x39: - return '+'; + *out++ = '+'; return 1; case 0x3A: - return ','; + *out++ = ','; return 1; case 0x3B: - return '-'; + *out++ = '-'; return 1; case 0x3C: - return '.'; + *out++ = '.'; return 1; case 0x3D: - return '/'; + *out++ = '/'; return 1; case 0x3E: - return ':'; + *out++ = ':'; return 1; case 0x3F: - return '='; + *out++ = '='; return 1; case 0x40: - return '?'; + *out++ = '?'; return 1; case 0x41: - return '@'; + *out++ = '@'; return 1; } /* Numbers */ if( c >= 0x10 && c <= 0x19 ) { - return '0' + (c - 0x10); + *out++ = '0' + (c - 0x10); + return 1; } /* Uppercase ASCII */ if( c >= 0x1A && c <= 0x33 ) { - return 'A' + (c - 0x1A); + *out++ = 'A' + (c - 0x1A); + return 1; + } + + /* Katakana and CJK symbols */ + if( c >= 0x42 && c <= 0x94 ) + { + const int cjk_base = 0x3000; + static uint8_t cjk_map[83] = { 2, 155, 156, 161, 163, 165, 167, 169, 195, 227, 229, 231, 242, 243, 162, 164, 166, 168, 170, 171, 173, 175, 177, 179, 181, 183, 185, 187, 189, 191, 193, 196, 198, 200, 202, 203, 204, 205, 206, 207, 210, 213, 216, 219, 222, 223, 224, 225, 226, 228, 230, 232, 233, 234, 235, 236, 237, 239, 172, 174, 176, 178, 180, 182, 184, 186, 188, 190, 192, 194, 197, 199, 201, 208, 211, 214, 217, 220, 209, 212, 215, 218, 221 }; + uint16_t codepoint = cjk_base + cjk_map[c - 0x42]; + *out++ = 0xE0 | ((codepoint >> 12) & 0x0F); + *out++ = 0x80 | ((codepoint >> 6) & 0x3F); + *out++ = 0x80 | (codepoint & 0x3F); + return 3; } /* Default to space for unprintables */ - return ' '; + *out++ = ' '; + return 1; } /** @@ -363,23 +411,25 @@ static int __read_note( uint8_t *tnote, entry_structure_t *note ) /* Translate n64 to ascii */ memset( note->name, 0, sizeof( note->name ) ); + int nidx = 0; for( int i = 0; i < 16; i++ ) { - note->name[i] = __n64_to_ascii( tnote[0x10 + i] ); + if ( tnote[0x10 + i] == 0 ) break; + nidx += __n64_to_utf8( tnote[0x10 + i], ¬e->name[nidx] ); } - /* Find the last position */ - for( int i = 0; i < 17; i++ ) + /* Separator between name and extension */ + note->name[nidx++] = '.'; + + for( int i = 0; i < 4; i++ ) { - if( note->name[i] == 0 ) - { - /* Here it is! */ - note->name[i] = '.'; - note->name[i+1] = __n64_to_ascii( tnote[0xC] ); - break; - } + if ( tnote[0xC + i] == 0 ) break; + nidx += __n64_to_utf8( tnote[0xC + i], ¬e->name[nidx] ); } + /* String terminator */ + note->name[nidx++] = 0; + /* Validate entries */ if( note->inode < BLOCK_VALID_FIRST || note->inode > BLOCK_VALID_LAST ) { @@ -413,7 +463,7 @@ static int __read_note( uint8_t *tnote, entry_structure_t *note ) */ static int __write_note( entry_structure_t *note, uint8_t *out_note ) { - char tname[19]; + char tname[sizeof(note->name)]; if( !out_note || !note ) { return -1; } /* Start with baseline */ @@ -653,18 +703,12 @@ static int __get_valid_toc( int controller ) int validate_mempak( int controller ) { - int toc = __get_valid_toc( controller ); - - if( toc == 1 || toc == 2 ) - { - /* Found a valid TOC */ - return 0; - } - else - { - /* Pass on return code */ - return toc; - } + int ret = cpakfs_fsck( controller, false, NULL, NULL ); + if (ret > 0) + return -3; /* Filesystem has issues */ + if (ret < 0) + return -2; /* Controller Pak is not inserted or I/O error in general */ + return 0; /* Valid filesystem */ } int get_mempak_entry( int controller, int entry, entry_structure_t *entry_data ) @@ -684,7 +728,7 @@ int get_mempak_entry( int controller, int entry, entry_structure_t *entry_data ) /* Entries are spread across two sectors, but we can luckly grab just one with a single Controller Pak read */ - if( read_mempak_address( controller, (3 * MEMPAK_BLOCK_SIZE) + (entry * 32), data ) ) + if( joybus_accessory_read( controller, (3 * MEMPAK_BLOCK_SIZE) + (entry * 32), data ) ) { /* Couldn't read note database */ return -2; @@ -723,96 +767,27 @@ int get_mempak_entry( int controller, int entry, entry_structure_t *entry_data ) int get_mempak_free_space( int controller ) { - uint8_t data[MEMPAK_BLOCK_SIZE]; - int toc; + cpakfs_stats_t stats; + if (cpakfs_get_stats( controller, &stats ) < 0) { + if (errno != ENODEV) + return -2; /* Controller Pak is not inserted or I/O error in general */ - /* Make sure Controller Pak is valid */ - if( (toc = __get_valid_toc( controller )) <= 0 ) - { - /* Bad Controller Pak or was removed, return */ - return -2; - } - - /* Grab the valid TOC to get free space */ - if( read_mempak_sector( controller, toc, data ) ) - { - /* Couldn't read TOC */ - return -2; + // Controller Pak not mounted. Try mounting it + if (cpakfs_mount(controller, "mempak_shim") < 0) + return -2; + int err = cpakfs_get_stats( controller, &stats ); + cpakfs_unmount(controller); + if (err < 0) + return -2; } - return __get_free_space( data ); + return stats.pages.total - stats.pages.used; } int format_mempak( int controller ) { - /* Many mempak dumps exist online for users of emulated games to get - saves that have all unlocks. Every beginning sector on all these - was the same, so this is the data I use to initialize the first - sector */ - uint8_t sector[MEMPAK_BLOCK_SIZE] = { 0x81,0x01,0x02,0x03,0x04,0x05,0x06,0x07, - 0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f, - 0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17, - 0x18,0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f, - 0xff,0xff,0xff,0xff,0x05,0x1a,0x5f,0x13, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, - 0xff,0xff,0x01,0xff,0x66,0x25,0x99,0xcd, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0xff,0xff,0xff,0xff,0x05,0x1a,0x5f,0x13, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, - 0xff,0xff,0x01,0xff,0x66,0x25,0x99,0xcd, - 0xff,0xff,0xff,0xff,0x05,0x1a,0x5f,0x13, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, - 0xff,0xff,0x01,0xff,0x66,0x25,0x99,0xcd, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0xff,0xff,0xff,0xff,0x05,0x1a,0x5f,0x13, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, - 0xff,0xff,0x01,0xff,0x66,0x25,0x99,0xcd, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 }; - - if( write_mempak_sector( controller, 0, sector ) ) - { - /* Couldn't write initial sector */ - return -2; - } - - /* Write out entry sectors, which can safely be zero */ - memset( sector, 0x0, MEMPAK_BLOCK_SIZE ); - if( write_mempak_sector( controller, 3, sector ) || - write_mempak_sector( controller, 4, sector ) ) - { - /* Couldn't write entry sectors */ - return -2; - } - - /* Go through, insert 'empty sector' marking on all entries */ - for( int i = 0; i < 128; i++ ) - { - sector[(i << 1) + 1] = BLOCK_EMPTY; - } - - /* Fix the checksum */ - sector[1] = __get_toc_checksum( sector ); - - /* Write out */ - if( write_mempak_sector( controller, 1, sector ) || - write_mempak_sector( controller, 2, sector ) ) - { - /* Couldn't write TOC sectors */ - return -2; - } + if (cpakfs_format( controller, false ) < 0) + return -2; /* Controller Pak is not inserted or I/O error in general */ return 0; } @@ -858,28 +833,6 @@ int read_mempak_entry_data( int controller, entry_structure_t *entry, uint8_t *d return 0; } -/** - * @brief Write associated data to a Controller Pak entry - * - * Given a Controller Pak entry structure with a valid region, name and block count, writes the - * entry and associated data to the Controller Pak. This function will not overwrite any existing - * user data. To update an existing entry, use #delete_mempak_entry followed by - * #write_mempak_entry_data with the same entry structure. - * - * @param[in] controller - * The controller (0-3) to write the entry and data to - * @param[in] entry - * The entry structure containing a region, name and block count - * @param[in] data - * The associated data to write to to the created entry - * - * @retval 0 if the entry was created and written successfully - * @retval -1 if the parameters were invalid or the note has no length - * @retval -2 if the Controller Pak wasn't present or was bad - * @retval -3 if there was an error writing to the Controller Pak - * @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 - */ int write_mempak_entry_data( int controller, entry_structure_t *entry, uint8_t *data ) { uint8_t sector[MEMPAK_BLOCK_SIZE]; @@ -968,7 +921,7 @@ int write_mempak_entry_data( int controller, entry_structure_t *entry, uint8_t * { entry_structure_t tmp_entry; - if( read_mempak_address( controller, (3 * MEMPAK_BLOCK_SIZE) + (i * 32), tmp_data ) ) + if( joybus_accessory_read( controller, (3 * MEMPAK_BLOCK_SIZE) + (i * 32), tmp_data ) ) { /* Couldn't read note database */ return -2; @@ -1011,7 +964,7 @@ int write_mempak_entry_data( int controller, entry_structure_t *entry, uint8_t * __write_note( entry, tmp_data ); /* Store entry to empty slot on Controller Pak */ - if( write_mempak_address( controller, (3 * MEMPAK_BLOCK_SIZE) + (entry->entry_id * 32), tmp_data ) ) + if( joybus_accessory_write( controller, (3 * MEMPAK_BLOCK_SIZE) + (entry->entry_id * 32), tmp_data ) ) { /* Couldn't update note database */ return -2; @@ -1033,7 +986,7 @@ int delete_mempak_entry( int controller, entry_structure_t *entry ) if( entry->inode < BLOCK_VALID_FIRST || entry->inode > BLOCK_VALID_LAST ) { return -1; } /* Ensure that the entry passed in matches what's on the Controller Pak */ - if( read_mempak_address( controller, (3 * MEMPAK_BLOCK_SIZE) + (entry->entry_id * 32), data ) ) + if( joybus_accessory_read( controller, (3 * MEMPAK_BLOCK_SIZE) + (entry->entry_id * 32), data ) ) { /* Couldn't read note database */ return -2; @@ -1053,7 +1006,7 @@ int delete_mempak_entry( int controller, entry_structure_t *entry ) /* The entry matches, so blank it */ memset( data, 0, 32 ); - if( write_mempak_address( controller, (3 * MEMPAK_BLOCK_SIZE) + (entry->entry_id * 32), data ) ) + if( joybus_accessory_write( controller, (3 * MEMPAK_BLOCK_SIZE) + (entry->entry_id * 32), data ) ) { /* Couldn't update note database */ return -2; diff --git a/src/joybus/tpak.c b/src/joybus/tpak.c index 182ab7426..73f4c1f1b 100755 --- a/src/joybus/tpak.c +++ b/src/joybus/tpak.c @@ -47,25 +47,25 @@ int tpak_set_value(int controller, uint16_t address, uint8_t value) { uint8_t block[TPAK_BLOCK_SIZE]; memset(block, value, TPAK_BLOCK_SIZE); - return write_mempak_address(controller, address, block); + return joybus_accessory_write(controller, address, block); } int tpak_init(int controller) { int result = 0; - int accessory = identify_accessory(controller); - if (accessory != ACCESSORY_TRANSFERPAK) return TPAK_ERROR_NO_TPAK; + int accessory = joypad_get_accessory_type(controller); + if (accessory != JOYPAD_ACCESSORY_TYPE_TRANSFER_PAK) { return TPAK_ERROR_NO_TPAK; } result = tpak_set_power(controller, true); - if (result) return result; + if (result) { return result; } result = tpak_set_access(controller, true); - if (result) return result; + if (result) { return result; } uint8_t status = tpak_get_status(controller); - if (status & TPAK_STATUS_REMOVED) return TPAK_ERROR_NO_CARTRIDGE; - if (!(status & TPAK_STATUS_READY)) return TPAK_ERROR_UNKNOWN_BEHAVIOUR; + if (status & TPAK_STATUS_REMOVED) { return TPAK_ERROR_NO_CARTRIDGE; } + if (!(status & TPAK_STATUS_READY)) { return TPAK_ERROR_UNKNOWN_BEHAVIOUR; } return 0; } @@ -90,7 +90,7 @@ int tpak_set_bank(int controller, int bank) uint8_t tpak_get_status(int controller) { uint8_t block[TPAK_BLOCK_SIZE]; - read_mempak_address(controller, TPAK_ADDRESS_STATUS, block); + joybus_accessory_read(controller, TPAK_ADDRESS_STATUS, block); return block[0]; } @@ -133,7 +133,7 @@ int tpak_write(int controller, uint16_t address, uint8_t* data, uint16_t size) adjusted_address = TPAK_ADDRESS_DATA; } - write_mempak_address(controller, adjusted_address, cursor); + joybus_accessory_write(controller, adjusted_address, cursor); address += TPAK_BLOCK_SIZE; cursor += TPAK_BLOCK_SIZE; adjusted_address += TPAK_BLOCK_SIZE; @@ -170,7 +170,7 @@ int tpak_read(int controller, uint16_t address, uint8_t* buffer, uint16_t size) adjusted_address = TPAK_ADDRESS_DATA; } - read_mempak_address(controller, adjusted_address, cursor); + joybus_accessory_read(controller, adjusted_address, cursor); address += TPAK_BLOCK_SIZE; cursor += TPAK_BLOCK_SIZE; adjusted_address += TPAK_BLOCK_SIZE; diff --git a/src/kernel/cthreads.c b/src/kernel/cthreads.c new file mode 100644 index 000000000..359f384a9 --- /dev/null +++ b/src/kernel/cthreads.c @@ -0,0 +1,67 @@ +/** + * @file cthreads.c + * @author Giovanni Bajo + * @brief C11 thread and synchronization primitive implementations. + */ +#include "threads.h" +#include "kernel.h" +#include "kernel_internal.h" +#include "interrupt.h" +#include + +int thrd_create_ex(thrd_t *thr, const char *name, int stack_size, int pri, thrd_start_t func, void *arg) +{ + kthread_t *th = __kthread_new_internal(name, stack_size, pri, 0, func, arg); + if (th == NULL) + return thrd_nomem; + *thr = th; + return thrd_success; +} + +int thrd_sleep(const struct timespec* duration, struct timespec* remaining) +{ + uint64_t ticks = TICKS_FROM_US((uint64_t)duration->tv_sec * 1000000 + (uint64_t)duration->tv_nsec / 1000); + kthread_sleep(ticks); + if (remaining) memset(remaining, 0, sizeof(struct timespec)); + return 0; +} + +int thrd_join(thrd_t thr, int *res) +{ + int result = kthread_join(thr); + if (res) *res = result; + return thrd_success; +} + +int mtx_init(mtx_t *mtx, int type) +{ + kmutex_init(mtx, type & mtx_recursive ? KMUTEX_RECURSIVE : KMUTEX_STANDARD); + return thrd_success; +} + +static uint64_t ticks_until(const struct timespec *time_point) +{ + struct timeval now; + gettimeofday(&now, NULL); + + // calculate the remaining time + struct timespec remaining; + remaining.tv_sec = time_point->tv_sec - now.tv_sec; + remaining.tv_nsec = time_point->tv_nsec - now.tv_usec * 1000; + + // convert to ticks + return TICKS_FROM_US((uint64_t)remaining.tv_sec * 1000000 + (uint64_t)remaining.tv_nsec / 1000); +} + +int mtx_timedlock(mtx_t *restrict mutex, const struct timespec *restrict time_point) { + return kmutex_try_lock(mutex, ticks_until(time_point)) ? thrd_success : thrd_busy; +} + +int cnd_timedwait(cnd_t *cond, mtx_t *mutex, const struct timespec *time_point) { + return kcond_wait_timeout(cond, mutex, ticks_until(time_point)) ? thrd_success : thrd_busy; +} + +extern inline int thrd_create(thrd_t *thr, thrd_start_t func, void *arg); +extern inline thrd_t thrd_current(void); +extern inline inline void thrd_yield(void); +extern inline int thrd_equal(thrd_t lhs, thrd_t rhs); diff --git a/src/kernel/kernel.c b/src/kernel/kernel.c new file mode 100644 index 000000000..04e4a689a --- /dev/null +++ b/src/kernel/kernel.c @@ -0,0 +1,1024 @@ +/** + * @file kernel.c + * @author Giovanni Bajo + */ +#include "kernel.h" +#include "kernel_internal.h" +#include "ktls_internal.h" +#include "backtrace_internal.h" +#include "timer.h" +#include "debug.h" +#include "exception.h" +#include "interrupt.h" +#include "backtrace.h" +#include "backtrace_internal.h" +#include +#include +#include + +#define DEBUG_KERNEL 0 ///< Enable kernel debugging +#define KERNEL_CHECKS 1 ///< Enable stack-smashing checks of threads + +#define STACK_COOKIE 0xDEADBEEFBAADC0DE ///< Stack cookie value for overflow detection +#define STACK_GUARD 64 ///< Stack guard size in bytes + + +/** @brief Read the current value of the gp register */ +#define REG_GP() ({ uint64_t gp; __asm("move %0, $28": "=r" (gp)); gp; }) + +/** + * @brief Execute a context switch. + * + * This forces a context switch to happen, by invoking the syscall 0x0. The + * syscall will be intercepted by the generic interrupt handler (inthandler.S) + * that will call #__kthread_syscall_schedule. + * + * This macro is very low level and is called as part of higher-level primitives + * that force a context switch like #kthread_yield. + * + * Do not call this under interrupt; use #KTHREAD_SWITCH_ISR instead. + */ +#define KTHREAD_SWITCH() __asm("syscall 0x0") + +/** + * @brief Executed a context switch, during an ongoing interrupt service routine. + * + * This forces a context switch to happen just like #KTHREAD_SWITCH, but it is + * the right version to call if the code is currently servicing an interrupt. + */ +#define KTHREAD_SWITCH_ISR() ({ __isr_force_schedule = true; }) + +/** @brief Main thread */ +kthread_t th_main; +/** @brief Pointer to the current thread */ +kthread_t *th_cur; +/** @brief Pointer to the idle thread */ +kthread_t *th_idle; +/** @brief List of ready threads */ +static kthread_t *th_ready; +/** @brief Number of live thread */ +static int th_count; + +/** @brief True if the multithreading kernel has been initialized and is running. */ +bool __kernel = false; +/** @brief True if a context switch must be done at the end of current interrupt. */ +bool __isr_force_schedule = false; +/* Global current interrupt depth (defined in interrupt.c) */ +extern int __interrupt_depth; +extern int __interrupt_sr; +/** @brief Thread TLS data copy (linker symbol) */ +extern char __th_tdata_copy[]; +/** @brief TLS data alignment (linker symbol) */ +extern __attribute__((section(".data"))) size_t __tdata_align; + +#ifndef NDEBUG +kthread_t *__kernel_all_threads; +#endif + +/** @brief Initializes TLS support for the kernel */ +__attribute__((constructor(102))) void __kernel_tls_init(void) +{ + memcpy(__th_tdata_copy, __tls_base, TDATA_SIZE); +} + +/** + * @brief Boot function for a kthread. + * + * It invokes the user entry function, and then kills the thread + * if the function ever returns. + */ +__attribute__((noreturn)) +void __kthread_boot(void) +{ + // Initialize GP register. This is the same as done in the entrypoint + // and is also used by the backtrace code as marker to stop walking. + asm volatile ("la $gp, _gp"); + int res = th_cur->user_entry(th_cur->user_data); + if (DEBUG_KERNEL) debugf("thread end: %s[%p] res=%d\n", th_cur->name, th_cur, res); + kthread_exit(res); +} + +/** @brief Checks for stack overflow in a thread */ +void __kthread_check_overflow(kthread_t *th) +{ + // If the current stack pointer is beyond the end of the stack, + // this is a stack overflow even without checking the integrity of the + // guard. + assertf((uintptr_t)th->stack_state >= (uintptr_t)th->stack + STACK_GUARD, + "stack overflow in thread: %s[%p]\nSP:%p | Stack top: %p | Overflow: %d bytes", + th->name, th, + th->stack_state, th->stack+STACK_GUARD, + th->stack+STACK_GUARD-(void*)th->stack_state); + + // Check if the stack guard has been corrupted. This indicates that + // has a stack overflow has happened, even if right now the stack has + // rolled back. + if (KERNEL_CHECKS) + { + uint64_t *cookie = (uint64_t*)th->stack; + for (int i=0;iname, th); + } +} + +/** @brief Frees a thread and its resources */ +static void kthread_free(kthread_t *th) +{ + if (DEBUG_KERNEL) debugf("[kernel] freeing %s[%p]\n", th->name, th); + // Get the start of the heap-allocated block (the stack) + void *mem = th->stack; + #ifndef NDEBUG + // Clear memory to avoid dangling pointers + memset(th, 0, sizeof(kthread_t)); + // Remove the thread from the all-threads list + kthread_t **p = &__kernel_all_threads; + while (*p && *p != th) + p = &((*p)->all_next); + if (*p) *p = th->all_next; + #endif + // Free the thread memory + free(mem); +} + +/** @brief Add a thread to a linked list */ +void __thlist_add(kthread_t **list, kthread_t *th) +{ + assert(!(th->flags & TH_FLAG_INLIST)); + th->flags |= TH_FLAG_INLIST; + + th->next = *list; + *list = th; +} + +/** @brief Add a thread to a linked list sorted by priority. */ +void __thlist_add_pri(kthread_t **list, kthread_t *th) +{ + while (*list && (*list)->pri >= th->pri) + list = &((*list)->next); + __thlist_add(list, th); +} + +/** @brief Add a thread to a physical address list sorted by priority */ +#define __phys_thlist_add_pri(list, th) ({ \ + kthread_t *__list = (list) ? VirtualCachedAddr(list) : NULL; \ + __thlist_add_pri(&__list, (th)); \ + (list) = PhysicalAddr(__list); \ +}) + +/** @brief Peek the head of a thread list (without popping it) */ +kthread_t* __thlist_head(kthread_t **list) +{ + if (!list) return NULL; + return *list; +} + +/** @brief Pop the head of a thread list (highest priority one, if the list is sorted) */ +kthread_t* __thlist_pop(kthread_t **list) +{ + kthread_t *th = *list; + if (th) + { + assertf(th->flags & TH_FLAG_INLIST, "thread %s[%p] not in list", th->name, th); + th->flags &= ~TH_FLAG_INLIST; + + *list = th->next; + th->next = NULL; + } + return th; +} + +/** @brief Remove an element from a thread list */ +bool __thlist_remove(kthread_t **list, kthread_t *th) +{ + while (*list && *list != th) + list = &((*list)->next); + if (*list) + { + *list = th->next; + th->next = NULL; + th->flags &= ~TH_FLAG_INLIST; + return true; + } + return false; +} + +/** @brief Remove an element from a physical address thread list */ +#define __phys_thlist_remove(list, th) ({ \ + kthread_t *__list = (list) ? VirtualCachedAddr(list) : NULL; \ + bool __ret = __thlist_remove(&__list, (th)); \ + (list) = PhysicalAddr(__list); \ + __ret; \ +}) + +/** + * @brief Move all threads from list src to list dst, respecting priority. + * + * NOTE: both lists must be sorted by priority, otherwise result is not + * guaranteed to be sorted. + * + * @return true if at least a thread of priority equal or higher than the + * the current one has been moved. + **/ +bool __thlist_splice_pri(kthread_t **dst, kthread_t **src) +{ + bool highpri = false; + kthread_t *th; + while ((th = __thlist_pop(src))) + { + highpri = highpri || (th->pri >= th_cur->pri); + __thlist_add_pri(dst, th); + dst = &th->next; + } + return highpri; +} + +/** @brief Splice threads from physical address list respecting priority */ +#define __phys_thlist_splice_pri(dst, src) ({ \ + kthread_t *__src = (src) ? VirtualCachedAddr(src) : NULL; \ + bool __ret = __thlist_splice_pri(dst, &__src); \ + (src) = PhysicalAddr(__src); \ + __ret; \ +}) + +/* Symbolize the function interrupted by the exception */ +static char* __symbolize_caller(reg_block_t *state) +{ + void *buffer[2]; + __backtrace_from(buffer, 2, (uint32_t*)state->epc, (uint32_t*)(uint32_t)state->sp, (uint32_t*)(uint32_t)state->fp, NULL); + static char buf[64]; + return __symbolize(buffer[1], buf, sizeof(buf)); +} + + +/** + * @brief Kernel scheduler: park the current thread and schedule the next thread + * + * This is the main scheduling function. It is invoked by the interrupt handler + * if explicitly requested via a syscall exception (#KTHREAD_SWITCH) or if + * an interrupt handler is preempting the current thread (#KTHREAD_SWITCH_ISR). + * + * @param stack_state Stack pointer containing the register block for the + * the current thread. + * + * @return The stack pointer containing the register block of the thread to + * be activated. + **/ +reg_block_t* __kthread_syscall_schedule(reg_block_t *stack_state) +{ + if (th_cur) + { + // Save the stack state for the current thread. + th_cur->stack_state = stack_state; + + // For debugging purposes: check if the current thread has overflown + // its allocated stack. + __kthread_check_overflow(th_cur); + + if (th_cur->flags & TH_FLAG_ZOMBIE) + { + // If the current thread is marked as zombie, it means that it must + // be freed. + if (DEBUG_KERNEL) debugf("[kernel] killing zombie: %s(%p) PC=%lx(%s)\n", th_cur->name, th_cur, stack_state->epc, __symbolize_caller(stack_state)); + assert(!(th_cur->flags & TH_FLAG_INLIST)); + assert(th_cur->flags & TH_FLAG_DETACHED); + kthread_free(th_cur); + } + else if (th_cur->flags & TH_FLAG_WAITFORJOIN) + { + // Nothing to do. This is a non-detached thread that just exited without a joiner. + // We can just forget about it, it wil get freed when someone calls kthread_join(). + assert(!(th_cur->flags & TH_FLAG_INLIST)); + assertf(!(th_cur->flags & TH_FLAG_DETACHED), "thread %s[%p] is waiting for a joiner, but is detached", th_cur->name, th_cur); + } + else + { + // Save the current thread state. + if (DEBUG_KERNEL) debugf("[kernel] parking %s(%p) PC=%lx(%s) SP=%llx\n", th_cur->name, th_cur, stack_state->epc, __symbolize_caller(stack_state), stack_state->sp); + + // Check how we got here. There are two possibilities: explicit + // syscall forcing a thread switch (THREAD_SWITCH), or an interrupt + // that awakes a higher priority thread (THREAD_SWITCH_ISR). + if (C0_GET_CAUSE_EXC_CODE(stack_state->cr) == EXCEPTION_CODE_SYS_CALL) + { + // Skip syscall instruction when this thread will be scheduled + // again. Notice that, given that this is an explicit syscall, + // the thread must already have been added to a waiting list, + // otherwise it wouldn't ever be scheduled again. + assert(th_cur->flags & TH_FLAG_INLIST); + assertf(*(uint32_t*)th_cur->stack_state->epc == 0x0000000C, "invalid opcode found by __kthread_syscall_schedule:\nexpected 0x0000000C (SYSCALL 0x0), found: %08lx", *(uint32_t*)th_cur->stack_state->epc); + th_cur->stack_state->epc += 4; + } + else + { + // If this is a preemptive switch forced by an interrupt, + // the current thread was running and it's still ready, so + // add it to the ready list again. + assertf(!(th_cur->flags & TH_FLAG_INLIST), "thread %s[%p] in list? flags=%x", th_cur->name, th_cur, th_cur->flags); + __thlist_add_pri(&th_ready, th_cur); + } + // Save the current interrupt depth. Interrupt depth is actually + // per-thread, so we just save/restore it every time a thread is + // scheduled. + th_cur->tls.interrupt_depth = __interrupt_depth; + th_cur->tls.interrupt_sr = __interrupt_sr; + } + } + + // Schedule the highest-priority thread that is ready, and is not waiting for + // a join. This would be the first thread in the ready list. There is always + // at least a thread here that matches this condition: the idle thread. + // Wait-for-join threads are purposedly skipped and we lose the reference to + // them: it's required for someone to call kthread_join() to free them. + kthread_t *th_next = NULL; + do { + th_next = __thlist_pop(&th_ready); + assert(th_next != NULL); + } while (th_next->flags & (TH_FLAG_WAITFORJOIN | TH_FLAG_SUSPENDED)); + + if (DEBUG_KERNEL) debugf("[kernel] switching to %s(%p) SP=%lx PC=%lx(%s) flags=%x\n", th_next->name, th_next, (uint32_t)th_next->stack_state->sp, th_next->stack_state->epc, __symbolize_caller(th_next->stack_state), th_next->flags); + assert(!(th_next->flags & TH_FLAG_INLIST)); + + // Set the current interrupt depth to that of the current thread. + th_cur = th_next; + + __interrupt_depth = th_cur->tls.interrupt_depth; + __interrupt_sr = th_cur->tls.interrupt_sr; + + __th_cur_tp = th_cur->tp_value; + + #ifdef __NEWLIB__ + _REENT = th_cur->tls.reent_ptr; + #endif + + return th_cur->stack_state; +} + +/** + * @brief Idle thread function + * + * Since there must always be something run by the CPU, the kernel creates + * a "idle" thread: a lowest-priority thread that is run only when all + * user-created threads are blocked. + * + * This thread does absolutely nothing. It just waits for any other thread + * to wake up so that it will be preempted. + */ +__attribute__((noreturn)) +static int __kthread_idle(void *arg) +{ + // When this thread is scheduled, there are no + // ready threads and all threads are probably waiting for some + // hardware event to happen. Just spin-wait here. + while(1) {} +} + +kthread_t* kernel_init(void) +{ + assertf(__tdata_align <= 8, "Unsupported TLS alignment of %d (Maximum 8)", __tdata_align); + assert(!__kernel); + #ifdef __NEWLIB__ + // Check if __malloc_lock is a nop. This happens with old toolchains where + // newlib was compiled with --disable-threads. + extern void __malloc_lock(void); + assertf(*(uint32_t*)__malloc_lock != 0x03e00008, + "Toolchain is outdated and does not support multithreading -- upgrade toolchain to the latest version"); + // Protect from link errors when building with old toolchains without retargetable locks. + // Those should fall into the previous check anyway. + #ifdef _RETARGETABLE_LOCKING + // Check that retargetable locks are correctly implemented. Since a simple change + // in linker command can cause them to disappear, better have an assert here. + extern void __retarget_lock_acquire_recursive(_LOCK_T lock); + assertf(*(uint32_t*)__retarget_lock_acquire_recursive != 0x03e00008, + "Build error: retargetable locks not defined -- check that the linker invokation is correct"); + #endif + #endif + th_ready = NULL; // empty ready list + th_count = 1; // start with the main thread + + // Configure the main thread + memset(&th_main, 0, sizeof(th_main)); + th_main.pri = 0; + th_main.name = "main"; + th_main.stack_size = 0x10000; // see STACK_SIZE in system.c + th_main.flags = TH_FLAG_DETACHED; // main thread cannot be joined + th_main.tp_value = __tls_base+TP_OFFSET; + + #ifdef __NEWLIB__ + th_main.tls.reent_ptr = _REENT; + #endif + + // NOTE: keep this in sync with system.c + const int STACK_SIZE = 0x10000; + th_main.stack = (char*)0x80000000 + __boot_memsize - STACK_SIZE; + + uint64_t *s = (uint64_t*)th_main.stack; + for (int i=0;istack = thmem; + + th->name = name; + th->user_entry = user_entry; + th->user_data = user_data; + th->pri = pri; + th->stack_size = stack_size; + + // Initialize the stack guard + uint64_t *s = (uint64_t*)th->stack; + for (int i=0;istack_state = (reg_block_t*)(top_stack - sizeof(reg_block_t)); + assert(((uint32_t)th->stack_state & 7) == 0); + memset(th->stack_state, 0, sizeof(reg_block_t)); + + // EPC: __thread_boot function (which will then invoke the user entry point) + th->stack_state->epc = (uint32_t)&__kthread_boot; + + // SR: start in exception mode (we will be within an exception during scheduling), + // and with interrupts enabled. + th->stack_state->sr = C0_STATUS() | C0_STATUS_EXL | C0_STATUS_IE; + + // GP: this is needed by the compiler to point to the global memory pool, + // so let's initialize to the same value it has in this thread. + th->stack_state->gp = REG_GP(); + + // SP: top of stack + th->stack_state->sp = (int64_t)(int32_t)top_stack; + + disable_interrupts(); + th_count++; + __thlist_add_pri(&th_ready, th); + #ifndef NDEBUG + // Add the thread to the all-threads list for debugging purposes + th->all_next = __kernel_all_threads; + __kernel_all_threads = th; + #endif + enable_interrupts(); + + //Initialize TLS Data + memcpy(extra, __th_tdata_copy, TDATA_SIZE); + memset(extra+TDATA_SIZE, 0, TLS_SIZE-TDATA_SIZE); + th->tp_value = extra+TP_OFFSET; + extra += TLS_SIZE; + // TLS initial configuration + #ifdef __NEWLIB__ + th->tls.reent_ptr = extra; + _REENT_INIT_PTR(th->tls.reent_ptr); + #endif + + // If the new thread has a priority higher or equal to the current one, + // yield immediately so that it will get started. This preserves the basic + // invariant that a higher priority thread is immediately scheduled whenever + // it becomes ready. + if (th->pri >= th_cur->pri) + kthread_yield(); + + return th; +} + +kthread_t* kthread_new(const char *name, int stack_size, int8_t pri, int (*user_entry)(void*), void *user_data) +{ + return __kthread_new_internal(name, stack_size, pri, 0, user_entry, user_data); +} + +kthread_t* kthread_current(void) +{ + return th_cur; +} + +void kthread_kill(kthread_t *th, int res) +{ + if (th == NULL) th = th_cur; + + if (DEBUG_KERNEL) debugf("killing: %s[%p] (flags:%x)\n", th->name, th, th->flags); + + disable_interrupts(); + th_count--; + + if (th->flags & TH_FLAG_DETACHED) { + // In general, we cannot just free the memory. If the thread is enqueued + // in some list (eg: waiting on a mailbox), we would need to remove it + // from that list before freeing it, but we don't have a way to do it + // (a thread doesn't have a reference to the list it's in, just a next + // pointer). + // So just mark the thread as zombie. This will cause the scheduler to + // free it as soon as the thread is first scheduled. + th->flags |= TH_FLAG_ZOMBIE; + } else { + if (th->joiner) { + th->joiner->joined_result = res; + __thlist_add_pri(&th_ready, th->joiner); + } else { + th->flags |= TH_FLAG_WAITFORJOIN; + th->joined_result = res; + } + } + + // If we're trying to kill ourselves, it's sufficient to force a context + // switch now. This thread will be killed immediately by the scheduler. + if (th == th_cur) + KTHREAD_SWITCH(); + + enable_interrupts(); +} + +void kthread_exit(int res) +{ + kthread_kill(NULL, res); + __builtin_unreachable(); +} + +void kthread_yield(void) +{ + // Check if there's a ready thread that has priority + // higher than or equal to the current thread, otherwise it's + // useless to force a context switch: the current thread would + // be rescheduled again. + assertf(!exception_is_running(), "cannot yield while an exception is running"); + kthread_t *th = __thlist_head(&th_ready); + if (th && th->pri >= th_cur->pri) + { + if (DEBUG_KERNEL) debugf("yielding: %s[%p] (flags:%x, status:%lx)\n", th_cur->name, th_cur, th_cur->flags, C0_STATUS()); + disable_interrupts(); + + __thlist_add_pri(&th_ready, th_cur); + KTHREAD_SWITCH(); + + enable_interrupts(); + } +} + +void kthread_suspend(kthread_t *th) +{ + disable_interrupts(); + th->flags |= TH_FLAG_SUSPENDED; + enable_interrupts(); +} + +void kthread_resume(kthread_t *th) +{ + disable_interrupts(); + th->flags &= ~TH_FLAG_SUSPENDED; + enable_interrupts(); +} + +void kthread_detach(kthread_t *th) +{ + if (th == NULL) th = th_cur; + disable_interrupts(); + assertf((th->flags & TH_FLAG_WAITFORJOIN) == 0, "cannot detach thread %s[%p] which is already exited", th->name, th); + th->flags |= TH_FLAG_DETACHED; + enable_interrupts(); +} + +int kthread_join(kthread_t *th) +{ + disable_interrupts(); + assertf(!(th->flags & TH_FLAG_DETACHED), "cannot join a detached thread %s[%p]", th->name, th); + + if (th->flags & TH_FLAG_WAITFORJOIN) + { + th_cur->joined_result = th->joined_result; + kthread_free(th); + } + else + { + // Add the current thread to the joiner list, and then force a context switch. + assertf(th->joiner == NULL, "thread %s[%p] already joined by %s[%p]", th->name, th, th->joiner->name, th->joiner); + th->joiner = th_cur; + KTHREAD_SWITCH(); + } + + enable_interrupts(); + return th_cur->joined_result; +} + +bool kthread_try_join(kthread_t *th, int *res) +{ + bool joined = false; + + disable_interrupts(); + assertf((th->flags & TH_FLAG_DETACHED), "cannot join a detached thread %s[%p]", th->name, th); + + if (th->flags & TH_FLAG_WAITFORJOIN) + { + th_cur->joined_result = th->joined_result; + if (res) *res = th->joined_result; + kthread_free(th); + joined = true; + } + + enable_interrupts(); + return joined; +} + +void kthread_set_pri(kthread_t *th, int8_t pri) +{ + assertf(pri >= 0, "thread priority cannot be negative"); + if (th == NULL) th = th_cur; + th->pri = pri; + + // Yield in case the priority change has immediate effect + kthread_yield(); +} + +void kthread_sleep(int64_t ticks) +{ + kthread_t *th = th_cur; + assertf(ticks > 0, "ticks must be positive"); + + // Timer callback. This will be invoked when the timer elapses after the + // requested delay. + void cb(int ovlf) + { + if (DEBUG_KERNEL) debugf("[kernel] sleep finished %s[%p]\n", th->name, th); + + // Put the thread again in the ready list, and forces a context switch + th->flags &= ~TH_FLAG_INLIST; + __thlist_add_pri(&th_ready, th); + KTHREAD_SWITCH_ISR(); + } + + if (DEBUG_KERNEL) debugf("[kernel] sleeping %lld %s[%p]\n", ticks, th->name, th); + disable_interrupts(); + + // Start a timer for the specified delay. + timer_link_t timer; + start_timer(&timer, ticks, TF_ONE_SHOT, cb); + + // Mark the thread as being in a list to avoid the scheduler to panic. + // This is not "technically true", but it's "semantically true": when + // we say "a thread is in a list" it means that it's registered somewhere + // so that there's a way to wake it up in the future. In this case, + // the timer callback will do it, so we can reassure the kernel scheduler + // that everything is fine. + th->flags |= TH_FLAG_INLIST; + + // Context switch + KTHREAD_SWITCH(); + + enable_interrupts(); +} + +int kthread_backtrace(kthread_t *th, void *buffer, int size) +{ + if (th == NULL) th = th_cur; + + if (th == th_cur) + return backtrace(buffer, size); + + return __backtrace_from(buffer, size, + (void*)(uint32_t)th->stack_state->epc, + (void*)(uint32_t)th->stack_state->sp, + (void*)(uint32_t)th->stack_state->fp, + (void*)(uint32_t)th->stack_state->ra); +} + +const char *kthread_name(kthread_t *th) +{ + if (th == NULL) th = th_cur; + return th->name; +} + +void kmutex_init(kmutex_t *mutex, uint8_t flags) +{ + assertf(mutex, "out of free memory"); + ((uint32_t*)mutex)[0] = 0; + ((uint32_t*)mutex)[1] = 0; + mutex->flags = flags; +} + +void kmutex_destroy(kmutex_t *mutex) +{ + kthread_t *owner = (void*)(mutex->owner | 0x80000000); + assertf(!mutex->owner, "kmutex_destroy() called, but mutex is locked by %s[%p]", owner->name, owner); + assertf(!mutex->waiting, "kmutex_destroy() called, but threads are waiting"); +} + +void kmutex_lock(kmutex_t *mutex) +{ + kthread_t *th = th_cur; + + disable_interrupts(); + assertf(!exception_is_running(), "cannot lock a mutex from an exception handler"); + + if (mutex->owner == PhysicalAddr(th)) + { + assertf(mutex->flags & KMUTEX_RECURSIVE, "a non-recursive mutex cannot be locked twice"); + mutex->counter++; + } + else + { + while (mutex->owner) + { + // Make owner thread inherit a higher priority, so that + // we avoid priority inversion. + kthread_t *owner = (void*)(mutex->owner | 0x80000000); + if (th->pri > owner->pri) + owner->pri = th->pri; + + __phys_thlist_add_pri(mutex->waiting, th); + KTHREAD_SWITCH(); + } + mutex->owner = PhysicalAddr(th); + mutex->counter = 1; + mutex->original_pri = th->pri; + } + enable_interrupts(); +} + +bool kmutex_try_lock(kmutex_t *mutex, uint32_t ticks) +{ + kthread_t *th = th_cur; + bool locked = false; + + disable_interrupts(); + if (mutex->owner == PhysicalAddr(th)) + { + assertf(mutex->flags & KMUTEX_RECURSIVE, "a non-recursive mutex cannot be locked twice"); + mutex->counter++; + locked = true; + } + else if (!mutex->owner) + { + mutex->owner = PhysicalAddr(th); + mutex->counter = 1; + mutex->original_pri = th->pri; + locked = true; + } + else if (ticks > 0) + { + bool timeout = false; + + // Timer callback. This will be invoked when the timer elapses after the + // requested delay. + void cb(int ovlf) + { + if (DEBUG_KERNEL) debugf("[kernel] mutex timeout %s[%p]\n", th->name, th); + + // Put the thread again in the ready list, and forces a context switch. + // Avoid race conditions here: if the thread is not anymore in the wait list, + // don't add it to the ready list. + timeout = true; + if (__phys_thlist_remove(mutex->waiting, th)) + __thlist_add_pri(&th_ready, th); + KTHREAD_SWITCH_ISR(); + } + + // Start a timer for the specified delay. + timer_link_t timer; + start_timer(&timer, ticks, TF_ONE_SHOT, cb); + + while (mutex->owner && !timeout) + { + // Make owner thread inherit a higher priority, so that + // we avoid priority inversion. + kthread_t *owner = (void*)(mutex->owner | 0x80000000); + if (th->pri > owner->pri) + owner->pri = th->pri; + __phys_thlist_add_pri(mutex->waiting, th); + KTHREAD_SWITCH(); + } + if (!timeout) + { + stop_timer(&timer); + mutex->owner = PhysicalAddr(th); + mutex->counter = 1; + mutex->original_pri = th->pri; + locked = true; + } + } + + enable_interrupts(); + return locked; +} + +__attribute__((noinline)) +static bool kmutex_unlock_internal(kmutex_t *mutex) +{ + bool highpri = false; + + mutex->counter--; + if (mutex->counter == 0) + { + mutex->owner = 0; + th_cur->pri = mutex->original_pri; + highpri = __phys_thlist_splice_pri(&th_ready, mutex->waiting); + } + return highpri; +} + +void kmutex_unlock(kmutex_t *mutex) +{ + kthread_t *th = th_cur; + + disable_interrupts(); + assertf(mutex->owner == PhysicalAddr(th), "mutex_unlock() called, but mutex is not locked by %s[%p]", th->name, th); + assertf(mutex->counter > 0, "mutex_unlock() called, but mutex is not locked"); + + // Unlock the mutex, and yield in case a higher priority thread is awaken + if (kmutex_unlock_internal(mutex)) + kthread_yield(); + + enable_interrupts(); +} + +void kcond_init(kcond_t *cond) +{ + memset(cond, 0, sizeof(kcond_t)); +} + +void kcond_destroy(kcond_t *cond) +{ + assertf(cond->waiting == NULL, "kcond_free() called, but some threads were waiting"); +} + +void kcond_signal(kcond_t *cond) +{ + disable_interrupts(); + // Wake up the highest-priority thread in the waiting list + kthread_t *th = __thlist_pop(&cond->waiting); + if (th) { + __thlist_add_pri(&th_ready, th); + if (th_cur->pri < th->pri) + kthread_yield(); + } + enable_interrupts(); +} + +void __kcond_signal_isr(kcond_t *cond) +{ + kthread_t *th = __thlist_pop(&cond->waiting); + if (th) { + __thlist_add_pri(&th_ready, th); + if (th_cur->pri < th->pri) + KTHREAD_SWITCH_ISR(); + } +} + +void kcond_broadcast(kcond_t *cond) +{ + disable_interrupts(); + // Wake up all threads in the waiting list + if (__thlist_splice_pri(&th_ready, &cond->waiting)) + kthread_yield(); + enable_interrupts(); +} + +void __kcond_broadcast_isr(kcond_t* cond) +{ + // This is a special version of kcond_broadcast that can be called + // from an interrupt handler. + if (__thlist_splice_pri(&th_ready, &cond->waiting)) + KTHREAD_SWITCH_ISR(); +} + +void kcond_wait(kcond_t *cond, kmutex_t *mutex) +{ + kthread_t *th = th_cur; + + disable_interrupts(); + assertf(!exception_is_running(), "cannot wait on a condition variable from an exception handler"); + if (mutex) { + assertf(mutex->owner == PhysicalAddr(th), "kcond_wait() called, but mutex is not locked by %s[%p]", th->name, th); + assertf(mutex->counter == 1, "kcond_wait() called, but mutex is locked multiple times"); + + // Unlock the mutex, and put the thread in the cond waiting list + kmutex_unlock_internal(mutex); + } + __thlist_add_pri(&cond->waiting, th); + + // Context switch + KTHREAD_SWITCH(); + + // Try to re-lock the mutex + if (mutex) kmutex_lock(mutex); + enable_interrupts(); +} + +bool kcond_wait_timeout(kcond_t *cond, kmutex_t *mutex, uint32_t ticks) +{ + kthread_t *th = th_cur; + bool timeout = false; + + disable_interrupts(); + if (mutex) { + assertf(mutex->owner == PhysicalAddr(th), "kcond_wait_timeout() called, but mutex is not locked by %s[%p]", th->name, th); + assertf(mutex->counter == 1, "kcond_wait_timeout() called, but mutex is locked multiple times"); + + // Unlock the mutex, and put the thread in the cond waiting list + kmutex_unlock_internal(mutex); + } + __thlist_add_pri(&cond->waiting, th); + + // Timer callback. This will be invoked when the timer elapses after the + // requested delay. + void cb(int ovlf) + { + if (DEBUG_KERNEL) debugf("[kernel] cond timeout %s[%p]\n", th->name, th); + + // Put the thread again in the ready list, and forces a context switch. + // Avoid race conditions here: if the thread is not anymore in the wait list, + // don't add it to the ready list. + timeout = true; + if (__thlist_remove(&cond->waiting, th)) + __thlist_add_pri(&th_ready, th); + KTHREAD_SWITCH_ISR(); + } + + // Start a timer for the specified delay. + timer_link_t timer; + start_timer(&timer, ticks, TF_ONE_SHOT, cb); + + // Context switch + KTHREAD_SWITCH(); + + if (!timeout) stop_timer(&timer); + + if (mutex) kmutex_lock(mutex); + enable_interrupts(); + return !timeout; +} diff --git a/src/kernel/kernel_internal.h b/src/kernel/kernel_internal.h new file mode 100644 index 000000000..7289c9995 --- /dev/null +++ b/src/kernel/kernel_internal.h @@ -0,0 +1,130 @@ +/** + * @file kernel_internal.h + * @author Giovanni Bajo + * @brief Internal Kernel Definitions + * @ingroup kernel + */ +#ifndef __LIBDRAGON_KERNEL_INTERNAL_H +#define __LIBDRAGON_KERNEL_INTERNAL_H + +#include "kernel.h" +#ifdef __NEWLIB__ +#include +#endif + +/** + * @addtogroup kernel + * @{ + */ + +#define TH_FLAG_ZOMBIE (1<<0) ///< The thread is a zombie (dead, waiting for cleanup) +#define TH_FLAG_INLIST (1<<1) ///< The thread is in a list (ready or waiting) +#define TH_FLAG_DETACHED (1<<2) ///< The thread is detached (no one will join it) +#define TH_FLAG_WAITFORJOIN (1<<3) ///< The non-detached thread is finished and is waiting for a join +#define TH_FLAG_SUSPENDED (1<<4) ///< The thread is suspended (will not be scheduled) +#define TH_FLAG_INSPECTOR1 (1<<7) ///< Flag reserved for usage in the inspector + +/** + * @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 __attribute__((aligned (8))) kthread_s +{ + /** Pointer to the top of the stack, which contains the thread register state */ + reg_block_t* stack_state; + /** TLS Pointer + TP_OFFSET */ + void *tp_value; + struct { + /** Mirror of __interrupt_depth */ + int interrupt_depth; + /** Mirror of __interrupt_sr */ + int interrupt_sr; + #ifdef __NEWLIB__ + /** Newlib reentrancy */ + struct _reent *reent_ptr; + #endif + } tls; ///< Thread-local storage + /** Size of the stack in bytes */ + int stack_size; + /** Name of thread (for debugging purposes) */ + const char *name; + /** Internal flags */ + uint8_t flags; + /** Priority of the thread (0=lowest, use positive numbers only) */ + int8_t pri; + /** Thread that is waiting for this one to finish */ + struct kthread_s *joiner; + /** Result code for the thread that was joined */ + int joined_result; + /** Intrusive link to next thread in a waiting list */ + struct kthread_s *next; + #ifndef NDEBUG + /** Intrusive link to next thread in the all list */ + struct kthread_s *all_next; + #endif + /** Entry point function for the thread */ + int (*user_entry)(void*); + /** Custom argument to be passed to the entry point */ + void *user_data; + /** Pointer to the stack buffer */ + void *stack; +} kthread_t; + +/** Kernel initialization flag. */ +extern bool __kernel; + +extern kcond_t __kirq_cond_sp; ///< Condition variable for SP interrupt +extern kcond_t __kirq_cond_dp; ///< Condition variable for DP interrupt +extern kcond_t __kirq_cond_si; ///< Condition variable for SI interrupt +extern kcond_t __kirq_cond_ai; ///< Condition variable for AI interrupt +extern kcond_t __kirq_cond_vi; ///< Condition variable for VI interrupt +extern kcond_t __kirq_cond_pi; ///< Condition variable for PI interrupt + +/** + * @brief Broadcast a condition under interrupt. + * + * This is useful for the special condition variables used to wait for interrupts. + */ +void __kcond_broadcast_isr(kcond_t* cond); + +/** @brief Signal a condition within interrupt. */ +void __kcond_signal_isr(kcond_t* cond); + +/** @brief Initialize kirq condition variables */ +void __kirq_init(void); + +/** @brief Syscall handler to run the scheduler and optionally switch current thread */ +reg_block_t* __kthread_syscall_schedule(reg_block_t *stack_state); + +/** @brief Internal thread creation function with also flags */ +kthread_t* __kthread_new_internal(const char *name, int stack_size, int8_t pri, uint8_t flag, int (*user_entry)(void*), void *user_data); + +/** @brief Register a hook to be called on every thread switch */ +void __kernel_register_switch_hook(void (*hook)(kthread_t*, kthread_t*)); + +/** @brief Unregister a previously registered thread switch hook */ +void __kernel_unregister_switch_hook(void (*hook)(kthread_t*, kthread_t*)); + +#ifndef NDEBUG +/** @brief List of all threads, used for debugging purposes (uses the #kthread_t all_next pointer) */ +extern kthread_t *__kernel_all_threads; +#endif + + +/** @} */ /* kernel */ + +#endif diff --git a/src/kernel/kirq.c b/src/kernel/kirq.c new file mode 100644 index 000000000..2b837113b --- /dev/null +++ b/src/kernel/kirq.c @@ -0,0 +1,87 @@ +/** + * @file kirq.c + * @author Giovanni Bajo + */ +#include "kirq.h" +#include "kernel.h" +#include "kernel_internal.h" +#include "interrupt.h" + +extern volatile int64_t __interrupt_counter; + +kcond_t __kirq_cond_sp; +kcond_t __kirq_cond_dp; +kcond_t __kirq_cond_si; +kcond_t __kirq_cond_ai; +kcond_t __kirq_cond_vi; +kcond_t __kirq_cond_pi; + +void __kirq_init(void) +{ + kcond_init(&__kirq_cond_sp); + kcond_init(&__kirq_cond_dp); + kcond_init(&__kirq_cond_si); + kcond_init(&__kirq_cond_ai); + kcond_init(&__kirq_cond_vi); + kcond_init(&__kirq_cond_pi); +} + + +kirq_wait_t kirq_begin_wait_sp(void) +{ + return (kirq_wait_t){ + .counter = __interrupt_counter, + .cond = &__kirq_cond_sp, + }; +} + +kirq_wait_t kirq_begin_wait_dp(void) +{ + return (kirq_wait_t){ + .counter = __interrupt_counter, + .cond = &__kirq_cond_dp, + }; +} + +kirq_wait_t kirq_begin_wait_si(void) +{ + return (kirq_wait_t){ + .counter = __interrupt_counter, + .cond = &__kirq_cond_si, + }; +} + +kirq_wait_t kirq_begin_wait_ai(void) +{ + return (kirq_wait_t){ + .counter = __interrupt_counter, + .cond = &__kirq_cond_ai, + }; +} + +kirq_wait_t kirq_begin_wait_vi(void) +{ + return (kirq_wait_t){ + .counter = __interrupt_counter, + .cond = &__kirq_cond_vi, + }; +} + +kirq_wait_t kirq_begin_wait_pi(void) +{ + return (kirq_wait_t){ + .counter = __interrupt_counter, + .cond = &__kirq_cond_pi, + }; +} + +void kirq_wait(kirq_wait_t* wait) +{ + if (!__kernel) return; + + disable_interrupts(); + if (wait->counter == __interrupt_counter) + kcond_wait(wait->cond, NULL); + wait->counter = __interrupt_counter; + enable_interrupts(); +} diff --git a/src/kernel/kqueue.c b/src/kernel/kqueue.c new file mode 100644 index 000000000..86ae56d40 --- /dev/null +++ b/src/kernel/kqueue.c @@ -0,0 +1,153 @@ +/** + * @file kqueue.c + * @author Giovanni Bajo + * @author Jeff Kent + * @brief Thread-safe FIFO queue implementation for kernel use. + */ +#include "kernel.h" +#include "kernel_internal.h" +#include "kqueue.h" +#include "debug.h" +#include +#include +#include + +/** @brief A thread-safe FIFO queue */ +typedef struct kqueue_s { + /** @brief The size of the buffer */ + int16_t size; + /** @brief The head of the queue */ + int16_t head; + /** @brief The tail of the queue */ + int16_t tail; + /** @brief The number of elements in the queue */ + int16_t count; + /** @brief The mutex to protect the queue */ + kmutex_t mutex; + /** @brief The condition variable to signal when the queue is not empty */ + kcond_t not_empty; + /** @brief The condition variable to signal when the queue is not full */ + kcond_t not_full; + /** @brief Buffer of the queue */ + void *buffer[]; +} kqueue_t; + +kqueue_t *kqueue_new(int size) +{ + kqueue_t *queue = calloc(1, sizeof(kqueue_t) + size * sizeof(void*)); + assertf(queue, "Out of memory"); + queue->size = size; + kmutex_init(&queue->mutex, KMUTEX_STANDARD); + kcond_init(&queue->not_empty); + kcond_init(&queue->not_full); + return queue; +} + +void kqueue_destroy(kqueue_t *queue) +{ + kmutex_destroy(&queue->mutex); + kcond_destroy(&queue->not_empty); + kcond_destroy(&queue->not_full); + free(queue); +} + +void kqueue_put(kqueue_t *queue, void *element) +{ + kmutex_lock(&queue->mutex); + while (queue->count == queue->size) + kcond_wait(&queue->not_full, &queue->mutex); + queue->buffer[queue->tail] = element; + queue->tail = (queue->tail + 1) % queue->size; + queue->count++; + kcond_signal(&queue->not_empty); + kmutex_unlock(&queue->mutex); +} + +bool kqueue_try_put_isr(kqueue_t *queue, void *element) +{ + assert(exception_is_running()); + + if (queue->mutex.counter == 0 && queue->count < queue->size) { + queue->buffer[queue->tail] = element; + queue->tail = (queue->tail + 1) % queue->size; + queue->count++; + __kcond_signal_isr(&queue->not_empty); + return true; + } + return false; +} + +bool kqueue_try_put(kqueue_t *queue, void *element, uint32_t ticks) +{ + kmutex_lock(&queue->mutex); + while (queue->count == queue->size) { + if (!kcond_wait_timeout(&queue->not_full, &queue->mutex, ticks)) { + kmutex_unlock(&queue->mutex); + return false; + } + } + queue->buffer[queue->tail] = element; + queue->tail = (queue->tail + 1) % queue->size; + queue->count++; + kcond_signal(&queue->not_empty); + kmutex_unlock(&queue->mutex); + return true; +} + +void *kqueue_get(kqueue_t *queue) +{ + void *element = NULL; + kmutex_lock(&queue->mutex); + while (queue->count == 0) + kcond_wait(&queue->not_empty, &queue->mutex); + element = queue->buffer[queue->head]; + queue->head = (queue->head + 1) % queue->size; + queue->count--; + kcond_signal(&queue->not_full); + kmutex_unlock(&queue->mutex); + return element; +} + +bool kqueue_try_get(kqueue_t *queue, void **element, uint32_t ticks) +{ + kmutex_lock(&queue->mutex); + while (queue->count == 0) { + if (!kcond_wait_timeout(&queue->not_empty, &queue->mutex, ticks)) { + kmutex_unlock(&queue->mutex); + return false; + } + } + if (element) { + *element = queue->buffer[queue->head]; + } + queue->head = (queue->head + 1) % queue->size; + queue->count--; + kcond_signal(&queue->not_full); + kmutex_unlock(&queue->mutex); + return true; +} + +int kqueue_count(kqueue_t *queue) +{ + return queue->count; +} + +int kqueue_size(kqueue_t *queue) +{ + return queue->size; +} + +bool kqueue_empty(kqueue_t *queue) +{ + return queue->count == 0; +} + +bool kqueue_full(kqueue_t *queue) +{ + return queue->count == queue->size; +} + +void *kqueue_peek(kqueue_t *queue) +{ + return queue->count ? queue->buffer[queue->head] : NULL; +} diff --git a/src/kernel/ksemaphore.c b/src/kernel/ksemaphore.c new file mode 100644 index 000000000..d4dbac6cd --- /dev/null +++ b/src/kernel/ksemaphore.c @@ -0,0 +1,66 @@ +/** + * @file ksemaphore.c + * @author Giovanni Bajo + * @brief Kernel Semaphore prmitive + * @ingroup kernel + * + */ +#include "ksemaphore.h" +#include "kernel_internal.h" + +void ksemaphore_init(ksemaphore_t *sem, int value) +{ + kmutex_init(&sem->mutex, KMUTEX_STANDARD); + kcond_init(&sem->cond); + sem->count = value; +} + +void ksemaphore_destroy(ksemaphore_t *sem) +{ + kcond_destroy(&sem->cond); + kmutex_destroy(&sem->mutex); +} + +void ksemaphore_wait(ksemaphore_t *sem) +{ + kmutex_lock(&sem->mutex); + while (sem->count <= 0) + kcond_wait(&sem->cond, &sem->mutex); + sem->count--; + kmutex_unlock(&sem->mutex); +} + +bool ksemaphore_try_wait(ksemaphore_t *sem, uint32_t ticks_) +{ + bool ret = false; + int64_t ticks = ticks_; + kmutex_lock(&sem->mutex); + while (sem->count <= 0 && ticks > 0) { + uint64_t now = get_ticks(); + if (!kcond_wait_timeout(&sem->cond, &sem->mutex, ticks)) + break; + ticks -= get_ticks() - now; + } + if (sem->count > 0) { + sem->count--; + ret = true; + } + kmutex_unlock(&sem->mutex); + return ret; +} + +void ksemaphore_post(ksemaphore_t *sem) +{ + kmutex_lock(&sem->mutex); + sem->count++; + kcond_signal(&sem->cond); + kmutex_unlock(&sem->mutex); +} + +void ksemaphore_post_isr(ksemaphore_t *sem) +{ + assert(exception_is_running()); + + sem->count++; + __kcond_signal_isr(&sem->cond); +} diff --git a/src/kernel/ktls.c b/src/kernel/ktls.c new file mode 100644 index 000000000..a788233ff --- /dev/null +++ b/src/kernel/ktls.c @@ -0,0 +1,27 @@ +/** + * @file ktls.c + * @author Giovanni Bajo + */ + +#include "ktls_internal.h" + +/** @brief Current thread TLS pointer */ +void *__th_cur_tp; + +void __ktls_init(void) +{ + // Set the initial thread pointer to the TLS area in the data section, + // which will be used for the main thread + __th_cur_tp = __tls_base + TP_OFFSET; +} + +void __ktls_close(void) +{ + __th_cur_tp = KERNEL_TP_INVALID; +} + +/** @brief TLS constructor */ +__attribute__((constructor(101))) void __ktls_constructor(void) +{ + __ktls_init(); +} diff --git a/src/kernel/ktls_internal.h b/src/kernel/ktls_internal.h new file mode 100644 index 000000000..ac269f08a --- /dev/null +++ b/src/kernel/ktls_internal.h @@ -0,0 +1,49 @@ +/** + * @file ktls_internal.h + * @author Giovanni Bajo + */ +#ifndef LIBDRAGON_KERNEL_TLS_INTERNAL_H +#define LIBDRAGON_KERNEL_TLS_INTERNAL_H + +/* TLS Linker symbols */ +/** @brief TLS Base Linker Symbol */ +extern char __tls_base[]; +/** @brief TLS End Linker Symbol */ +extern char __tls_end[]; +/** @brief TLS data start (linker symbol) */ +extern char __tdata_start[]; +/** @brief TLS data end (linker symbol) */ +extern char __tdata_end[]; +/** @brief TLS BSS start (linker symbol) */ +extern char __tbss_start[]; +/** @brief TLS BSS end (linker symbol) */ +extern char __tbss_end[]; + +/** @brief Size of .tdata section */ +#define TDATA_SIZE ((uint32_t)(__tdata_end) - (uint32_t)(__tdata_start)) + +/** @brief Size of .tdata and .tbss sections combined */ +#define TLS_SIZE ((uint32_t)(__tls_end) - (uint32_t)(__tls_base)) + +/** @brief Offset of Pointer for TLS accesses */ +#define TP_OFFSET 0x7000 + +/** + * @brief Invalid thread pointer value + * + * This is used to indicate that the current thread pointer is invalid, which + * happens during exception processing. If the code happens to access TLS, + * it will dereference this, and the exception code in __get_exception_name will + * be able to detect this and print an useful error message. + */ +#define KERNEL_TP_INVALID ((void *)0x5FFF8001) + +extern void *__th_cur_tp; + +/** @brief Initialize TLS support */ +void __ktls_init(void); + +/** @brief Close TLS support */ +void __ktls_close(void); + +#endif diff --git a/src/kernel/libdragon.mk b/src/kernel/libdragon.mk new file mode 100644 index 000000000..33288f159 --- /dev/null +++ b/src/kernel/libdragon.mk @@ -0,0 +1,7 @@ +LIBDRAGON_OBJS += \ + $(BUILD_DIR)/kernel/kernel.o \ + $(BUILD_DIR)/kernel/cthreads.o \ + $(BUILD_DIR)/kernel/ktls.o \ + $(BUILD_DIR)/kernel/kqueue.o \ + $(BUILD_DIR)/kernel/ksemaphore.o \ + $(BUILD_DIR)/kernel/kirq.o diff --git a/src/lspr1.c b/src/lspr1.c new file mode 100644 index 000000000..43fce619d --- /dev/null +++ b/src/lspr1.c @@ -0,0 +1,281 @@ +/** + * @file lspr1.c + * @author Christopher Bonhage + * @author Giovanni Bajo + * @brief Lossy-sprite Level 1: BC1Q (BC1/DXT1) decoder + */ + +#include "lspr1.h" +#include "sprite.h" +#include "sprite_internal.h" +#include "surface.h" +#include "n64sys.h" +#include "utils.h" +#include "debug.h" +#include "rspq.h" +#include "rsp.h" +#include "asset.h" + +#include +#include +#include +#include + +/** @brief Max BC1 blocks per RSP strip (mirrors N_STRIP_BLOCKS_MAX in rsp_lspr1.S). */ +#define BC1Q_STRIP_BLOCKS_MAX 32 + +// rspq command IDs (must mirror RSPQ_DefineCommand order in rsp_lspr1.S). +#define BC1Q_CMD_SET_BUFFERS 0x0 ///< rspq command: record source/dest buffers +#define BC1Q_CMD_DECODE_STRIP 0x1 ///< rspq command: decode one strip of blocks + +/** @brief The lspr1 (BC1Q) RSP ucode overlay */ +DEFINE_RSP_UCODE(rsp_lspr1); +static uint32_t lspr1_ovl_id = 0; + +/** @brief BC1Q version number. */ +#define BC1Q_VERSION 2 + +/** @brief Required alignment of the decoded sprite buffer. */ +#define BC1Q_BUF_ALIGN 64 + +/** @brief BC1 block payload size in bytes. */ +#define BC1Q_BLOCK_SIZE 8 + +/** + * @brief Header structure for BC1Q-encoded files. + * + * Must mirror the layout produced by tools/mksprite/mksprite_bc1q.cpp. + * All multi-byte fields are big-endian on disk; the runtime decoder runs + * on the N64 (also big-endian) so no byte-swap is needed on read. + */ +typedef struct lspr1_header_s { + uint8_t magic[BC1Q_FILE_MAGIC_SIZE]; + uint16_t version; + uint16_t flags; // bit 0: has_alpha (any block uses 3-color mode) + uint16_t width; + uint16_t height; + uint8_t block_size; // = BC1Q_BLOCK_SIZE + uint8_t reserved[7]; // zero in v1; pads header to 24 bytes so payload[] + // is 8-byte aligned (RSP DMA requirement). + uint8_t payload[]; // single BC1 block grid: ceil(w/4) * ceil(h/4) * 8 bytes +} lspr1_header_t; + +_Static_assert(sizeof(lspr1_header_t) == 24, "lspr1_header_t must be 24 bytes"); +_Static_assert(offsetof(lspr1_header_t, payload) % 8 == 0, + "BC1Q payload must be 8-byte aligned for RSP DMA"); + +static bool lspr1_is_encoded(const void *buf, int sz) { + if (!buf || sz < (int)sizeof(lspr1_header_t)) return false; + const lspr1_header_t *hdr = (const lspr1_header_t *)buf; + return memcmp(hdr->magic, BC1Q_FILE_MAGIC, BC1Q_FILE_MAGIC_SIZE) == 0; +} + +static size_t lspr1_decoded_size_buf(const void *encoded_buf, int encoded_sz) { + if (!lspr1_is_encoded(encoded_buf, encoded_sz)) return 0; + const lspr1_header_t *hdr = (const lspr1_header_t *)encoded_buf; + assertf(hdr->version == BC1Q_VERSION, "Invalid lossy sprite version (BC1Q version %d, expected %d)\nPlease regenerate your asset files", hdr->version, BC1Q_VERSION); + size_t pixel_bytes = (size_t)hdr->width * hdr->height * 2; + size_t header_bytes = sizeof(sprite_t) + sizeof(sprite_ext_t); + return ROUND_UP(header_bytes, 64) + ROUND_UP(pixel_bytes, 16); +} + +/** + * @brief Decode one BC1 block into RGBA5551 pixels. + * + * @param block Pointer to the 8-byte BC1 block (BE on disk, read raw on BE host). + * @param dst Destination row pointer for pixel (block_x*4, block_y*4). + * @param dst_stride Destination row stride in 16-bit pixels (= image width). + * @param max_dx Number of in-bounds columns in this block (1..4). + * @param max_dy Number of in-bounds rows in this block (1..4). + */ +static void lspr1_decode_block( + const uint8_t *block, + uint16_t *dst, int dst_stride, + int max_dx, int max_dy +) { + uint16_t c0 = ((uint16_t)block[0] << 8) | block[1]; + uint16_t c1 = ((uint16_t)block[2] << 8) | block[3]; + uint32_t indices = + ((uint32_t)block[4] << 24) | + ((uint32_t)block[5] << 16) | + ((uint32_t)block[6] << 8) | + ((uint32_t)block[7]); + + // Endpoints are stored on-disk as RGBA5551 (alpha bit always 1); unpack + // the 5-bit R/G/B channels (the alpha bit in position 0 is ignored here). + int r0 = (c0 >> 11) & 0x1F; + int g0 = (c0 >> 6) & 0x1F; + int b0 = (c0 >> 1) & 0x1F; + int r1 = (c1 >> 11) & 0x1F; + int g1 = (c1 >> 6) & 0x1F; + int b1 = (c1 >> 1) & 0x1F; + + // Build the 4-entry RGBA5551 palette. DXT1a uses endpoint ordering + // as the per-block alpha flag: c0 > c1 selects opaque 4-color mode; + // c0 <= c1 selects 3-color + transparent. + uint16_t pal[4]; + // Endpoint 0 and 1 are already RGBA5551; use them directly (force alpha=1). + pal[0] = c0 | 1; + pal[1] = c1 | 1; + if (c0 > c1) { + int r2 = (2*r0 + r1) / 3, g2 = (2*g0 + g1) / 3, b2 = (2*b0 + b1) / 3; + int r3 = (r0 + 2*r1) / 3, g3 = (g0 + 2*g1) / 3, b3 = (b0 + 2*b1) / 3; + pal[2] = (r2 << 11) | (g2 << 6) | (b2 << 1) | 1; + pal[3] = (r3 << 11) | (g3 << 6) | (b3 << 1) | 1; + } else { + int r2 = (r0 + r1) / 2, g2 = (g0 + g1) / 2, b2 = (b0 + b1) / 2; + pal[2] = (r2 << 11) | (g2 << 6) | (b2 << 1) | 1; + pal[3] = 0x0000; // fully transparent (a=0) + } + + for (int dy = 0; dy < max_dy; dy++) { + uint16_t *row = dst + dy * dst_stride; + for (int dx = 0; dx < max_dx; dx++) { + int idx = (indices >> (2 * (dy * 4 + dx))) & 0x3; + row[dx] = pal[idx]; + } + } +} + +static sprite_t *lspr1_load_buf(const void *encoded_buf, int encoded_sz) { + assertf(lspr1_is_encoded(encoded_buf, encoded_sz), "Invalid BC1Q buffer"); + const lspr1_header_t *hdr = (const lspr1_header_t *)encoded_buf; + assertf(hdr->version == BC1Q_VERSION, + "Unsupported BC1Q version %u (this build supports %u)", + hdr->version, BC1Q_VERSION); + assertf(hdr->block_size == BC1Q_BLOCK_SIZE, + "Unsupported BC1Q block_size %u (this build supports %u)", + hdr->block_size, BC1Q_BLOCK_SIZE); + // Reserved byte at offset 13 was set aside for a future mip-count field. + // A non-zero value indicates the file was produced by a future encoder + // that this build does not understand; refuse to decode. + assertf(hdr->reserved[0] == 0, + "BC1Q file requires a newer lspr1 decoder (reserved=%u)", + hdr->reserved[0]); + + int width = hdr->width; + int height = hdr->height; + assertf(width > 0 && height > 0, "Invalid BC1Q dimensions %dx%d", width, height); + + int bw = (width + 3) / 4; + int bh = (height + 3) / 4; + size_t payload_size = (size_t)bw * bh * BC1Q_BLOCK_SIZE; + assertf((size_t)encoded_sz >= sizeof(lspr1_header_t) + payload_size, + "BC1Q buffer truncated (sz=%d, expected at least %zu)", + encoded_sz, sizeof(lspr1_header_t) + payload_size); + + size_t decoded_sz = lspr1_decoded_size_buf(encoded_buf, encoded_sz); + sprite_t *sprite = (sprite_t *)memalign(BC1Q_BUF_ALIGN, decoded_sz); + assertf(sprite, "Out of memory decoding BC1Q sprite (%zu bytes)", decoded_sz); + + size_t header_bytes = ROUND_UP(sizeof(sprite_t) + sizeof(sprite_ext_t), 64); + memset(sprite, 0, header_bytes); + sprite->width = width; + sprite->height = height; + sprite->flags = SPRITE_FLAGS_OWNEDBUFFER | SPRITE_FLAGS_NODATA | SPRITE_FLAGS_EXT | FMT_RGBA16; + sprite->hslices = 1; + sprite->vslices = 1; + + sprite_ext_t *sx = (sprite_ext_t *)sprite->data; + sx->size = sizeof(sprite_ext_t); + sx->version = SPRITE_EXT_VERSION; + sx->data_ptr = (uint32_t)header_bytes; + + uint16_t *dst = (uint16_t *)((uint8_t *)sprite + header_bytes); + const uint8_t *src = hdr->payload; + + int bw_full = width / 4; // fully in-bounds block columns + int bh_full = height / 4; // fully in-bounds block rows + int has_right_edge = (width & 3) != 0; + int has_bottom_edge = (height & 3) != 0; + + size_t pixel_bytes = (size_t)width * height * 2; + size_t pixel_bytes_aligned = ROUND_UP(pixel_bytes, 16); + + // Hand the source buffer and destination region to the RSP. The source + // payload was just CPU-read from the asset; flush it so RSP DMA sees the + // final bytes. The destination region is RSP-written; invalidate it so any + // dirty CPU lines covering it don't overwrite RSP's stores on later evict. + data_cache_hit_writeback((void *)src, (size_t)bw * bh * BC1Q_BLOCK_SIZE); + data_cache_hit_writeback_invalidate(dst, pixel_bytes_aligned); + + rspq_write(lspr1_ovl_id, BC1Q_CMD_SET_BUFFERS, + PhysicalAddr(src), PhysicalAddr(dst), (uint32_t)(width * 2)); + + // Walk the fully-aligned interior block grid as horizontal strips. + for (int by = 0; by < bh_full; by++) { + int strip_x = 0; + while (strip_x < bw_full) { + int n_blocks = bw_full - strip_x; + if (n_blocks > BC1Q_STRIP_BLOCKS_MAX) n_blocks = BC1Q_STRIP_BLOCKS_MAX; + uint32_t in_off = (uint32_t)(by * bw + strip_x) * BC1Q_BLOCK_SIZE; + uint32_t out_off = (uint32_t)((by * 4) * width + strip_x * 4) * 2; + rspq_write(lspr1_ovl_id, BC1Q_CMD_DECODE_STRIP, + in_off, out_off, (uint32_t)n_blocks); + strip_x += n_blocks; + } + } + + // CPU-decode the right-edge partial column (if any) while the RSP drains + // the interior. These touch < 1/bw of the image so the cost is negligible. + if (has_right_edge) { + int bx = bw_full; + int max_dx = width - bx * 4; + for (int by = 0; by < bh_full; by++) { + const uint8_t *blk = src + (by * bw + bx) * BC1Q_BLOCK_SIZE; + uint16_t *block_dst = dst + (by * 4) * width + (bx * 4); + lspr1_decode_block(blk, block_dst, width, max_dx, 4); + } + } + + // CPU-decode the bottom-edge partial row (if any) across the full width, + // which naturally covers the bottom-right corner block as well. + if (has_bottom_edge) { + int by = bh_full; + int max_dy = height - by * 4; + for (int bx = 0; bx < bw; bx++) { + int max_dx = width - bx * 4; + if (max_dx > 4) max_dx = 4; + const uint8_t *blk = src + (by * bw + bx) * BC1Q_BLOCK_SIZE; + uint16_t *block_dst = dst + (by * 4) * width + (bx * 4); + lspr1_decode_block(blk, block_dst, width, max_dx, max_dy); + } + } + + // Wait for the RSP to finish before returning the sprite. + rspq_wait(); + + // CPU edge fixups wrote through the cache, so flush those lines too. The + // interior was written by RSP DMA so it's already coherent with RDRAM. + if (has_right_edge || has_bottom_edge) { + data_cache_hit_writeback(dst, pixel_bytes_aligned); + } + + return sprite; +} + +static int lspr1_init_refcount = 0; +static sprite_decoder_t *lspr1_decoder = NULL; + +void lspr1_init(void) +{ + if (lspr1_init_refcount++ > 0) return; + + assertf(lspr1_decoder == NULL, "lspr1 is already initialized"); + rspq_init(); + asset_init_compression(2); + lspr1_ovl_id = rspq_overlay_register(&rsp_lspr1); + assertf(lspr1_ovl_id != 0, "lspr1: failed to register rsp_lspr1 overlay"); + lspr1_decoder = sprite_decoder_register(lspr1_is_encoded, lspr1_load_buf); +} + +void lspr1_close(void) +{ + if (--lspr1_init_refcount > 0) return; + + assertf(lspr1_decoder != NULL, "lspr1 is not initialized"); + sprite_decoder_unregister(lspr1_decoder); + lspr1_decoder = NULL; + rspq_overlay_unregister(lspr1_ovl_id); + lspr1_ovl_id = 0; +} diff --git a/src/lspr3.c b/src/lspr3.c new file mode 100644 index 000000000..54d552528 --- /dev/null +++ b/src/lspr3.c @@ -0,0 +1,393 @@ +/** + * @file lspr3.c + * @author Christopher Bonhage + * @author Giovanni Bajo + * @brief Lossy-sprite Level 3: H264I decoder + */ + +#include "lspr3.h" + +// h264_decoder.h defines OPTIMIZE_NO_DECODED_FLAG before pulling in the +// h264bsd headers, so mbStorage_t here matches the layout used inside the +// h264_decoder TU. +#include "video/h264_decoder.h" +#include "video/h264_decoder/h264bsd_macroblock_layer.h" +#include "video/h264_decoder/h264bsd_neighbour.h" +#include "video/h264_decoder/h264bsd_pic_param_set.h" +#include "video/h264_decoder/h264bsd_seq_param_set.h" +#include "video/h264_decoder/h264bsd_slice_header.h" +#include "video/h264_decoder/h264bsd_util.h" +#include "video/rsph264_internal.h" +#include "graphics.h" +#include "rdpq.h" +#include "rdpq_attach.h" +#include "rdpq_mode.h" +#include "rspq.h" +#include "sprite.h" +#include "sprite_internal.h" +#include "n64sys.h" +#include "surface.h" +#include "utils.h" +#include "yuv.h" +#include "debug.h" + +#include +#include +#include +#include +#include + +/** @brief H264I version number. */ +#define H264I_VERSION 5 + +/** @brief H264I header flag: file carries a 1-bit alpha bitmap after the H.264 payload. */ +#define LSPR3_FLAG_ALPHA1 0x01 + +/** @brief H264I decode macroblock ring size */ +#define H264I_MB_RING 2 + +/** @brief Required alignment of the decoded sprite buffer. */ +#define H264I_BUF_ALIGN 64 + +/** + * @brief Header structure for H264I-encoded files. + * + * Must mirror the layout used in tools/mksprite/mksprite_h264i.cpp + */ +typedef struct lspr3_header_s { + uint8_t magic[H264I_FILE_MAGIC_SIZE]; + uint16_t version; + uint16_t width; + uint16_t height; + uint16_t orig_width; + uint16_t orig_height; + // x264-emitted PPS values that the runtime decoder needs to match the + // dequant scale used at encode time. Not derivable from the bitstream + // because mksprite strips the SPS/PPS NALs (only the IDR slice ships). + uint8_t pic_init_qp; // 0..51 + int8_t chroma_qp_index_offset; // -12..12 + uint8_t flags; // bit 0 (LSPR3_FLAG_ALPHA1): 1-bit alpha bitmap present + uint8_t reserved[3]; // zero in v5; also pads alpha_size to 4-byte alignment + uint32_t alpha_size; // byte size of the 1bpp alpha bitmap (0 if none) + uint8_t payload[]; // H.264 RBSP, followed by the alpha bitmap if present +} lspr3_header_t; + +// The runtime reads fields via a raw pointer cast over the file bytes, and uses +// both `hdr->payload` and `sizeof(lspr3_header_t)` to locate/measure the H.264 +// payload. Keep the struct free of trailing padding so the two stay consistent +// and match the byte layout emitted by tools/mksprite/mksprite_h264i.cpp. +_Static_assert(sizeof(lspr3_header_t) == 28, "lspr3_header_t must be 28 bytes"); +_Static_assert(offsetof(lspr3_header_t, payload) == 28, + "lspr3_header_t payload must start at offset 28 (no trailing padding)"); + +static bool lspr3_is_encoded(const void *buf, int sz) { + if (!buf || sz < (int)sizeof(lspr3_header_t)) return false; + const lspr3_header_t *hdr = (const lspr3_header_t *)buf; + return memcmp(hdr->magic, H264I_FILE_MAGIC, H264I_FILE_MAGIC_SIZE) == 0; +} + +static size_t lspr3_decoded_size_buf(const void *encoded_buf, int encoded_sz) { + if (!lspr3_is_encoded(encoded_buf, encoded_sz)) return 0; + const lspr3_header_t *hdr = (const lspr3_header_t *)encoded_buf; + assertf(hdr->version == H264I_VERSION, "Invalid lossy sprite version (H264I version %d, expected %d)\nPlease regenerate your asset files", hdr->version, H264I_VERSION); + size_t pixel_bytes = hdr->orig_width * hdr->orig_height * 2; + size_t header_bytes = sizeof(sprite_t) + sizeof(sprite_ext_t); + return ROUND_UP(header_bytes, 64) + ROUND_UP(pixel_bytes, 16); +} + +static void lspr3_decode_intra_slice( + const uint8_t *payload, + size_t payload_size, + uint16_t width, + uint16_t height, + uint8_t pic_init_qp, + int8_t chroma_qp_index_offset, + uint8_t **out_yuv, + size_t *out_yuv_size +) { + assertf(payload_size >= 2, "H264I: payload too small"); + + uint8_t nal_hdr = payload[0]; + assertf((nal_hdr & 0x80) == 0, "H264I: forbidden_zero_bit set"); + nalUnit_t nal = { + .nalUnitType = (nalUnitType_e)(nal_hdr & 0x1F), + .nalRefIdc = (nal_hdr >> 5) & 3, + }; + assertf(nal.nalUnitType == NAL_CODED_SLICE_IDR, "H264I: non-IDR NAL"); + + int mb_w = (width + 15) / 16; + int mb_h = (height + 15) / 16; + u32 pic_size_in_mbs = (u32)(mb_w * mb_h); + size_t yuv_size = (size_t)pic_size_in_mbs * 256 + (size_t)pic_size_in_mbs * 64 * 2; + uint8_t *yuv = (uint8_t*)malloc_uncached(yuv_size); + assertf(yuv, "H264I: out of memory"); + image_t image = { .data = yuv, .width = (u32)mb_w, .height = (u32)mb_h }; + + seqParamSet_t sps = { + .profileIdc = 66, + .levelIdc = 22, + .seqParameterSetId = 0, + .maxFrameNum = 1 << 4, + .picOrderCntType = 2, + .numRefFrames = 1, + .picWidthInMbs = (u32)mb_w, + .picHeightInMbs = (u32)mb_h, + }; + + picParamSet_t pps = { + .picParameterSetId = 0, + .seqParameterSetId = 0, + .picOrderPresentFlag = 0, + .numSliceGroups = 1, + .sliceGroupMapType = 0, + .picSizeInMapUnits = (u32)pic_size_in_mbs, + .numRefIdxL0Active = 1, + .picInitQp = pic_init_qp, + .chromaQpIndexOffset = chroma_qp_index_offset, + .deblockingFilterControlPresentFlag = 1, + .constrainedIntraPredFlag = 0, + .redundantPicCntPresentFlag = 0, + }; + + uint8_t *rbsp = (uint8_t *)(payload + 1); + size_t rbsp_size = payload_size - 1; + strmData_t strm = {0}; + strm.pStart = rbsp; + strm.pCurr = ((u64)(uintptr_t)rbsp) << 3; + strm.pEnd = ((u64)(uintptr_t)(rbsp + rbsp_size)) << 3; + + // Reset the RSP decoder's per-frame state (notably last_packed_delta_buf) + // before queuing any macroblock work for this slice. + rsph264_begin_frame(); + + sliceHeader_t slice = {0}; + u32 slice_status = h264bsdDecodeSliceHeader(&strm, &slice, &sps, &pps, &nal); + assertf(slice_status == HANTRO_OK, "H264I: slice header decode failed"); + assertf(IS_I_SLICE(slice.sliceType), "H264I: non-intra slice"); + assertf(slice.firstMbInSlice == 0, "H264I: first_mb_in_slice != 0"); + assertf(slice.picParameterSetId == 0, "H264I: unexpected PPS id"); + + mbStorage_t *mb = (mbStorage_t*)calloc(pic_size_in_mbs, sizeof(mbStorage_t)); + assertf(mb, "H264I: out of memory"); + h264bsdInitMbNeighbours(mb, (u32)mb_w, pic_size_in_mbs); + + i32 qpY = (i32)pps.picInitQp + slice.sliceQpDelta; + u32 currMbAddr = 0; + + // Each MB writes its packed delta residual records into + // mbLayer.residual.posCoefBuf, which the RSP later DMAs asynchronously. + // Reusing a slot before the RSP has consumed it would race MB N+1's CPU + // writes against MB N's pending DMA. A 2-slot ring paced with rspq + // syncpoints makes this deterministic: before reusing a slot we wait on + // the syncpoint recorded right after that slot's RSP work was queued, so + // the RSP is guaranteed to have drained the buffer. Two slots fit on the + // stack, so no allocation is needed. + macroblockLayer_t mbLayers[H264I_MB_RING] = {0}; + rspq_syncpoint_t slot_sync[H264I_MB_RING] = {0}; // 0 = none pending (ids start at 1) + + while (currMbAddr < pic_size_in_mbs) { + u32 slot = currMbAddr % H264I_MB_RING; + // Wait for the RSP to finish consuming this slot before overwriting it. + if (slot_sync[slot]) rspq_syncpoint_wait(slot_sync[slot]); + macroblockLayer_t *mbLayer = &mbLayers[slot]; + + mbStorage_t *pMb = mb + currMbAddr; + pMb->sliceId = 1; + pMb->disableDeblockingFilterIdc = slice.disableDeblockingFilterIdc; + pMb->filterOffsetA = slice.sliceAlphaC0Offset; + pMb->filterOffsetB = slice.sliceBetaOffset; + pMb->chromaQpIndexOffset = pps.chromaQpIndexOffset; + + u32 mb_layer_status = h264bsdDecodeMacroblockLayer(&strm, mbLayer, mb + currMbAddr, + slice.sliceType, slice.numRefIdxL0Active); + assertf(mb_layer_status == HANTRO_OK, + "H264I: macroblock layer decode failed at mb=%lu", + (unsigned long)currMbAddr); + assertf(IS_INTRA_MB(*mbLayer), + "H264I: inter MB not supported (mb=%lu type=%d)", + (unsigned long)currMbAddr, + (int)mbLayer->mbType); + u32 mb_status = h264bsdDecodeMacroblock(mb + currMbAddr, mbLayer, &image, NULL, + &qpY, currMbAddr, pps.constrainedIntraPredFlag, + &slice); + assertf(mb_status == HANTRO_OK, + "H264I: macroblock decode failed at mb=%lu", + (unsigned long)currMbAddr); + // Record the queue position after this MB's RSP work; the slot is safe + // to reuse once the RSP reaches this point. + slot_sync[slot] = rspq_syncpoint_new(); + currMbAddr++; + if (!h264bsdMoreRbspData(&strm)) break; + } + + // Drain the RSP queue before freeing `mb`: the RSP asynchronously DMAs + // neighbour data and residual records out of it, so it must be idle + // before the memory is released. + rsph264_sync(); + assertf(currMbAddr == pic_size_in_mbs, "H264I: incomplete slice"); + free(mb); + + *out_yuv = yuv; + *out_yuv_size = yuv_size; +} + +// Build the FMT_RGBA16 sprite from the I420 reconstruction. The RDP YUV +// combiner does the YUV→RGB conversion on the fly (BT.709 full range, hard +// coded to match the encoder's rgba_to_i420), with bilinear chroma upsample +// as a side benefit. +static void lspr3_build_rgba16_sprite( + sprite_t *sprite, const uint8_t *pic, + uint16_t orig_w, uint16_t orig_h, + int stride, int luma_h +) { + uint8_t preserved_flags = sprite->flags & SPRITE_FLAGS_OWNEDBUFFER; + size_t header_bytes = ROUND_UP(sizeof(sprite_t) + sizeof(sprite_ext_t), 64); + sys_hw_memset(sprite, 0, header_bytes); + sprite->width = orig_w; + sprite->height = orig_h; + sprite->flags = preserved_flags | SPRITE_FLAGS_NODATA | SPRITE_FLAGS_EXT | FMT_RGBA16; + sprite->hslices = 1; + sprite->vslices = 1; + + sprite_ext_t *sx = (sprite_ext_t*)sprite->data; + sx->size = sizeof(sprite_ext_t); + sx->version = SPRITE_EXT_VERSION; + sx->data_ptr = (uint32_t)header_bytes; + + int uv_stride = stride / 2; + size_t y_bytes = (size_t)stride * luma_h; + size_t uv_bytes = (size_t)uv_stride * (luma_h / 2); + const uint8_t *y_plane = pic; + const uint8_t *u_plane = pic + y_bytes; + const uint8_t *v_plane = u_plane + uv_bytes; + uint8_t *dst = (uint8_t*)sprite + header_bytes; + + yuv_frame_t frame = { + .y = surface_make_linear((void*)y_plane, FMT_I8, stride, luma_h), + .u = surface_make_linear((void*)u_plane, FMT_I8, uv_stride, luma_h / 2), + .v = surface_make_linear((void*)v_plane, FMT_I8, uv_stride, luma_h / 2), + }; + + // The RDP writes through to RAM bypassing the CPU cache. Discard + // any (possibly speculative) cached lines for the destination so a + // future read of `dst` doesn't return stale data. + size_t pixel_bytes = (size_t)orig_w * orig_h * 2; + size_t pixel_bytes_aligned = ROUND_UP(pixel_bytes, 16); + data_cache_hit_writeback_invalidate(dst, pixel_bytes_aligned); + surface_t target_surf = surface_make_linear(dst, FMT_RGBA16, orig_w, orig_h); + + yuv_init(); + { + rdpq_attach(&target_surf, NULL); + { + // yuv_tex_blit puts the RDP in YUV mode. Without push/pop the YUV + // combiner state leaks back to the caller's render mode, which makes + // the next non-YUV draw on the screen surface emit magenta/purple + // pixels until the caller next sets a render mode. + rdpq_mode_push(); + { + yuv_tex_blit(&frame, 0, 0, NULL, &YUV_BT709_FULL); + } + rdpq_mode_pop(); + } + rdpq_detach_wait(); + } + yuv_close(); +} + +// Apply the 1-bit alpha bitmap to the decoded RGBA5551 sprite. Each set bit +// marks a transparent pixel (see build_alpha1_bitmap() in the encoder): the +// whole pixel is cleared to 0x0000 (RGB and alpha both zero, like BC1Q's +// pal[3]) so bilinear filtering at draw time cannot bleed color out of +// transparent texels. The bitmap is row-major, ceil(w/8) bytes per row, +// MSB-first, at the sprite's (cropped) width/height. +static void lspr3_apply_alpha1(uint16_t *dst, uint16_t w, uint16_t h, + const uint8_t *bitmap) +{ + int row_bytes = (w + 7) / 8; + for (int y = 0; y < h; y++) { + const uint8_t *brow = bitmap + (size_t)y * row_bytes; + uint16_t *drow = dst + (size_t)y * w; + for (int x = 0; x < w; x++) { + if (brow[x >> 3] & (0x80 >> (x & 7))) + drow[x] = 0x0000; + } + } + // The RGBA5551 buffer was written by the RDP straight to RDRAM (its cache + // lines were invalidated before), so our CPU edits sit in cache: flush them + // back so the sprite is coherent when the RDP samples it at draw time. + data_cache_hit_writeback(dst, ROUND_UP((size_t)w * h * 2, 16)); +} + +static sprite_t *lspr3_load_buf(const void *encoded_buf, int encoded_sz) { + // Allocate the buffer for the decoded sprite. + size_t decoded_sz = lspr3_decoded_size_buf(encoded_buf, encoded_sz); + assertf(decoded_sz > 0, "Invalid H264I buffer"); + sprite_t *sprite = (sprite_t *)memalign(H264I_BUF_ALIGN, decoded_sz); + assertf(sprite, "Out of memory"); + sprite->flags = SPRITE_FLAGS_OWNEDBUFFER; + + // Decode the H264I bitstream into YUV planes. When a 1-bit alpha bitmap is + // present it is appended after the H.264 payload, so the payload itself is + // shorter than the whole buffer by alpha_size bytes. + const lspr3_header_t *hdr = (const lspr3_header_t *)encoded_buf; + size_t alpha_size = (hdr->flags & LSPR3_FLAG_ALPHA1) ? hdr->alpha_size : 0; + assertf((size_t)encoded_sz >= sizeof(lspr3_header_t) + alpha_size, + "H264I buffer truncated (sz=%d, alpha_size=%zu)", encoded_sz, alpha_size); + size_t payload_size = (size_t)encoded_sz - sizeof(lspr3_header_t) - alpha_size; + uint8_t *pic = NULL; + size_t pic_size = 0; + lspr3_decode_intra_slice( + hdr->payload, payload_size, + hdr->width, hdr->height, + hdr->pic_init_qp, hdr->chroma_qp_index_offset, + &pic, &pic_size + ); + + // Convert the YUV planes into the final RGBA16 sprite + int mb_w = (hdr->width + 15) / 16; + int mb_h = (hdr->height + 15) / 16; + int stride = mb_w * 16; + int luma_h = mb_h * 16; + lspr3_build_rgba16_sprite( + sprite, pic, + hdr->orig_width, hdr->orig_height, + stride, luma_h + ); + free_uncached(pic); + + // Re-apply the lossless 1-bit alpha mask (if any) on top of the RGBA5551 + // pixels the RDP just produced (the YUV combiner writes alpha=1 everywhere). + if (alpha_size) { + size_t header_bytes = ROUND_UP(sizeof(sprite_t) + sizeof(sprite_ext_t), 64); + uint16_t *dst = (uint16_t *)((uint8_t *)sprite + header_bytes); + const uint8_t *bitmap = hdr->payload + payload_size; + lspr3_apply_alpha1(dst, hdr->orig_width, hdr->orig_height, bitmap); + } + + return sprite; +} + +static int lspr3_init_refcount = 0; +static sprite_decoder_t *lspr3_decoder = NULL; + +void lspr3_init(void) +{ + // Just increment the refcount if already initialized. + if (lspr3_init_refcount++ > 0) return; + + assertf(lspr3_decoder == NULL, "lspr3 is already initialized"); + lspr3_decoder = sprite_decoder_register(lspr3_is_encoded, lspr3_load_buf); + // Register and upload the H.264 RSP overlays used by the slice decoder. + rsph264_init(); +} + +void lspr3_close(void) +{ + // Just decrement the refcount if there are still dangling references. + if (--lspr3_init_refcount > 0) return; + + assertf(lspr3_decoder != NULL, "lspr3 is not initialized"); + sprite_decoder_unregister(lspr3_decoder); + lspr3_decoder = NULL; +} diff --git a/src/magma/.gitignore b/src/magma/.gitignore new file mode 100644 index 000000000..3e937e73a --- /dev/null +++ b/src/magma/.gitignore @@ -0,0 +1,2 @@ +# Automatically generated during build +rsp_magma.h diff --git a/src/magma/libdragon.mk b/src/magma/libdragon.mk new file mode 100644 index 000000000..ba6c2739c --- /dev/null +++ b/src/magma/libdragon.mk @@ -0,0 +1,8 @@ +LIBDRAGON_OBJS += \ + $(BUILD_DIR)/magma/magma_math.o \ + $(BUILD_DIR)/magma/magma.o \ + $(BUILD_DIR)/magma/mgfx.o \ + $(BUILD_DIR)/magma/rsp_magma_clipping.o \ + $(BUILD_DIR)/magma/rsp_magma.o \ + $(BUILD_DIR)/magma/rsp_mgfx_env.o \ + $(BUILD_DIR)/magma/rsp_mgfx.o \ No newline at end of file diff --git a/src/magma/magma.c b/src/magma/magma.c new file mode 100644 index 000000000..47e56d082 --- /dev/null +++ b/src/magma/magma.c @@ -0,0 +1,1160 @@ +/** + * @file magma.c + * @author Dennis Heinze + * @brief Magma core implementation + * @ingroup magma + * + * # Magma: implementation details + * + * For an explanation of magma's concepts at the API-level, see @ref magma + * + * At the high level, the most important concepts of magma's implementation are the following: + * - Vertex pipeline + * - Vertex cache + * - Drawing commands + * + * Drawing geometry roughly happens in two steps: + * 1. The vertex pipeline produces vertex data and stores it in the vertex cache. This step is highly customisable and by far the most complex. + * 2. Drawing commands then read the stored vertex data, assemble RDP triangle commands and submit them into the RDP queue. This step is mostly fixed function. + * + * ## Vertex pipeline + * The vertex pipeline (or just "pipeline" for short) is invoked by enqueueing the command `MG_CMD_LOAD_VERTICES`. + * However, the core magma ucode doesn't actually contain the pipeline at all. + * Instead, that command jumps to a small bootstrapping segment which will load the actual pipeline into IMEM first. + * More precisely, it will load the shader code (see below) of the currently bound pipeline. + * It is loaded at the same address as the bootstrapper, overwriting it. + * This means, that as long as rspq doesn't switch overlays, the pipeline remains there and is directly jumped to whenever `MG_CMD_LOAD_VERTICES` is called. + * When the overlay does get switched, the bootstrapper will be run again the next time the pipeline is invoked. + * + * The bootstrapper (and therefore by extension the code of the pipeline) is located at the symbol `_MG_VTX_SHADER`. + * + * Each pipeline itself is responsible for actually reading the input vertices from the vertex buffer and writing output correctly to the vertex cache. + * + * ### Pipeline construction + * + * A pipeline is simply a copy of a vertex shader, which has been patched to work with a certain vertex layout. + * A vertex shader in turn is just a regular #rsp_ucode_t that complies with magma's vertex shader requirements. + * + * A compliant vertex shader must: + * - Have a text segment that + * - is bitwise equal to the core magma overlay up to `_MG_VTX_SHADER` + * - and contains the actual shader code at `_MG_VTX_SHADER`. + * - Have a meta segment (see @ref rsp_ucode_t.meta) whose contents are compatible with #mg_meta_header_t. + * + * These requirements are automatically met by following the instructions in `rsp_magma.inc`. + * + * Upon construction of a pipeline, magma creates a copy in RDRAM of the text segment starting at `_MG_VTX_SHADER` up to the end of the text segment + * (or in other words, it copies the shader code). + * Then it uses the shader description within the meta segment and the specified vertex layout to patch the shader code. + * + * #### Shader description + * + * The shader description contains the following: + * - A list of attribute descriptions + * - A list of uniform descriptions + * + * An attribute description, in turn, contains: + * - The input number assigned to the attribute + * - Whether the attribute is optional + * - A list of loaders + * - A list of patches + * + * A uniform description consists of: + * - The uniform's binding number + * - The uniform's start address in uniform memory + * - The uniform's end adddress + * + * The pipeline construction process primarily makes use of the attribute descriptions (see next section). + * The uniform descriptions are simply stored within the pipeline so they can be queried by the user later via #mg_pipeline_get_uniform. + * + * #### Loaders and patches + * + * To adapt the shader code to a specific vertex layout, magma needs to patch every instruction that accesses vertex attributes, + * so that the data is loaded from the correct offset. + * Shaders can contain arbitrary code and have an arbitrary set of attributes (note that magma itself doesn't dictate the *format* of attributes). + * That means every vertex shader needs to advertise the locations of all instructions that load attribute data. + * These instructions are called "loaders". + * Both scalar and vector loading instructions are supported. + * + * Shaders may also mark any attribute as optional. + * If such an attribute is omitted from the vertex layout, the corresponding loaders are simply left untouched. + * For cases in which a pipeline needs to behave differently whenever an attribute is omitted, shaders can also specify sets of "patches". + * A patch is always associated with exactly one attribute. + * It consists of the following: + * - The location of an (arbitrary) instruction that will be modified in case the attribute is omitted. + * - An alternative instruction that will replace the original instruction. + * + * NOTE: Since patches are only applied when their attribute is omitted, and loaders will not be modified in that case, + * it is in fact possible for an instruction to be both a loader, and the target of a patch. + * + * ### Uniforms + * + * Uniforms are located in the saved data region, so that their contents are preserved across overlay switches. + * More specifically, they are located at `_MG_VTX_SHADER_UNIFORMS`, after all other data in DMEM. + * Because uniforms can be loaded via DMA individually, they must be aligned to 8 bytes. + * This is automatically ensured by the macros in `rsp_magma.inc`. + * + * Uniforms don't have any format requirements and may contain any arbitrary data as required by the vertex shader code. + * This means that uniforms in and of themselves are just a region in DMEM. + * Loading uniforms is an accordingly simple process. + * A uniform loading command will just DMA some data from an arbitrary RDRAM location to the uniform's DMEM region. + * + * However, there are two main modes of uniform loading: + * - Loading from uniform buffer (see #mg_uniform_load) + * - Loading from inline data (see #mg_uniform_load_inline) + * + * The term "uniform buffer" here just means any arbitrary location in RDRAM that is managed by the user. + * Most notably, the user is responsible for keeping this data valid for as long as required by DMAs. + * + * Loading uniforms "inline" does something different. Instead of just taking the pointer to the data, + * it will actually copy the data into the rspq command stream itself. + * Magma's ucode defines a series of commands of exponentially increasing size to make this possible without wasting too much space. + * The size of the data is obviously limited by the maximum size of rspq commands. + * Of course this means that loading uniforms inline may be wasteful if the uniform doesn't exactly fit one of these commands. + * The major advantage is that the user doesn't need to manage the data themselves. + * + * ## Drawing commands + * + * From the ucode's perspective, there is only a single drawing command: `MG_CMD_DRAW_INDICES`. + * This command will draw a single triangle from three vertices that are inside the vertex cache. + * The command can be invoked directly by using #mg_draw_triangle. + * + * All other drawing functions will internally just emit a series of `MG_CMD_LOAD_VERTICES` and `MG_CMD_DRAW_INDICES`. + * In general they will draw triangles in "batches". + * In a single batch, they will try to fill the vertex cache in such a way, that as many triangles as possible can be drawn in that batch, + * using multiple invocations of `MG_CMD_LOAD_VERTICES`. + * Then, those triangles are drawn using `MG_CMD_DRAW_INDICES`. + * The next batch will fill the cache from scratch again. + * + * #mg_draw does this in a straightforward manner, since it just draws a long list of triangles with unique vertices. + * A batch will simply load as many vertices as can fit into the cache. + * + * #mg_draw_indexed on the other hand needs to do multiple loads per batch, since triangles may use vertices that are not contiguously arranged in the vertex buffer. + * It will try to optimize the loading sequence to some extent to avoid loading unnecessary vertices. + * It does not reorder triangles, however. + * + * ### Clipping + * + * Some triangles need to be clipped to avoid overflows while assembling the RDP command. + * To reduce the number of clipped triangles, magma uses a so called "guard band". + * See #mg_set_clip_factor for details. + * + * The clipping check is performed automatically by the triangle assembling routine in `rsp_rdpq_tri.inc`. + * Magma registers as the clipping callback another bootstrapping function that will load the actual clipping routine into IMEM. + * This is done to reserve more IMEM for vertex shaders, at the cost of some performance when clipping occurs. + * This tradeoff is considered appropriate under the assumption that most triangles will not be clipped in typical game scenes. + * + * Due to how the guard band works, large triangles are most affected by clipping. + * This can especially happen when they are distorted by perspective projection. + * Preventing this can therefore be a potential method of improving performance. + * + * The clipping routine will overwrite any currently loaded vertex shader when loaded in. + * This will also restore the vertex shader bootstrapper (see above). + * + * ### Skinning + * + * Magma offers a CPU-side solution for skinning which is agnostic of any specific vertex shader. + * The user simply needs to provide a list of matrix indices (bone indices) alongside the vertices, + * a list of matrices, and the uniform which the matrices should be loaded into. + * The drawing functions will automatically perform the uniform loads inbetween vertex batches. + * For details, see #mg_input_assembly_parms_t. + */ + +#include "magma_internal.h" +#include "magma_constants.h" +#include "rspq.h" +#include "rdpq.h" +#include "utils.h" +#include "rsp_magma.h" +#include "../rdpq/rdpq_internal.h" +#include "rsp_asm.h" +#include + +/** @brief Metadata about a uniform defined by a shader. */ +typedef struct +{ + uint32_t binding; ///< Binding number of the uniform. + uint32_t start; ///< Start address of the uniform in DMEM, relative to the start of uniform memory. + uint32_t end; ///< End address of the uniform in DMEM, relative to the start of uniform memory. +} mg_meta_uniform_t; + +/** @brief Metadata about a patch of a vertex attribute defined by a shader. */ +typedef struct +{ + uint32_t address; ///< Address in IMEM of the instruction to be patched, relative to the overlay address. + uint32_t replacement; ///< New instruction to be substituted, in case this patch is invoked. +} mg_meta_attribute_patch_t; + +/** @brief Metadata about a vertex attribute defined by a shader. */ +typedef struct +{ + uint32_t input; ///< Input number of this attribute. + uint32_t is_optional; ///< Non-zero if this attribute is optional. + uint32_t loaders_offset; ///< Offset of the array of loaders in bytes, relative to the list of attributes. + uint32_t patches_offset; ///< Offset of the array of patches in bytes, relative to the list of attributes. + uint32_t loader_count; ///< Number of loaders. + uint32_t patches_count; ///< Number of patches. +} mg_meta_attribute_t; + + +/** @brief Metadata header defined by a shader. */ +typedef struct +{ + uint32_t uniform_count; ///< Number of uniforms. + uint32_t uniforms_offset; ///< Offset of the list of uniforms in bytes, relative to the metadata header. + uint32_t attribute_count; ///< Number of attributes. + uint32_t attributes_offset; ///< Offset of the list of attributes in bytes, relative to the metadata header. +} mg_meta_header_t; + +/** @brief The main magma ucode overlay */ +DEFINE_RSP_UCODE(rsp_magma); + +/** @brief The magma clipping ucode overlay */ +DEFINE_RSP_UCODE(rsp_magma_clipping); + +static bool is_initialized = false; + +/** @brief Overlay id of the magma ucode */ +uint32_t mg_overlay_id; + +void mg_uniform_load_raw(uint32_t offset, uint32_t size, const void *data) +{ + assertf(size > 0, "size must be greater than 0"); + mg_cmd_write(MG_CMD_LOAD_UNIFORM, PhysicalAddr(data), ((size-1) << 16) | offset); +} + +void mg_draw_begin(void) +{ + __rdpq_autosync_use(AUTOSYNC_PIPE | AUTOSYNC_TILES | AUTOSYNC_TMEMS); +} + +void mg_draw_end(void) +{ + mg_cmd_write(MG_CMD_DRAW_END); +} + +void mg_load_vertices(uint32_t buffer_index, uint8_t cache_index, uint32_t count) +{ + assertf(count > 0, "count must be greater than 0"); + assertf(count <= MG_VERTEX_CACHE_COUNT, "too many vertices: %lu", count); + assertf(cache_index + count <= MG_VERTEX_CACHE_COUNT, "cache_index out of range: %u, count: %lu", cache_index, count); + mg_cmd_write(MG_CMD_LOAD_VERTICES, buffer_index, (cache_index<<16) | count); +} + +void mg_draw_triangle(uint8_t index0, uint8_t index1, uint8_t index2) +{ + assertf(index0 <= MG_VERTEX_CACHE_COUNT, "index0 is out of range: %u", index0); + assertf(index1 <= MG_VERTEX_CACHE_COUNT, "index1 is out of range: %u", index1); + assertf(index2 <= MG_VERTEX_CACHE_COUNT, "index2 is out of range: %u", index2); + + uint16_t i0 = index0 * MG_VTX_SIZE + RSP_MAGMA_MG_VERTEX_CACHE; + uint16_t i1 = index1 * MG_VTX_SIZE + RSP_MAGMA_MG_VERTEX_CACHE; + uint16_t i2 = index2 * MG_VTX_SIZE + RSP_MAGMA_MG_VERTEX_CACHE; + + mg_rdpq_write(MG_CMD_DRAW_INDICES, i0, (i1 << 16) | i2); +} + +static void get_overlay_span(rsp_ucode_t *ucode, void **code, uint32_t *code_size) +{ + uint32_t overlay_offset = RSP_MAGMA__MG_OVERLAY - RSP_MAGMA__start; + uint32_t ucode_size = (uint8_t*)ucode->code_end - ucode->code; + + *code = (uint8_t*)ucode->code + overlay_offset; + *code_size = ucode_size - overlay_offset; +} + +mg_rsp_state_t *mg_get_rsp_state() +{ + return (mg_rsp_state_t*)(((uint8_t*)rspq_overlay_get_state(&rsp_magma)) + RSP_MAGMA_MG_STATE - RSP_MAGMA__RSPQ_SAVED_STATE_START); +} + +void mg_init(void) +{ + if (is_initialized) return; + + // Write location and size of the clipping overlay to magma's overlay data before registering it in rspq + void* clipping_code; + uint32_t clipping_code_size; + get_overlay_span(&rsp_magma_clipping, &clipping_code, &clipping_code_size); + + phys_addr_t clipping_code_phys = PhysicalAddr(clipping_code); + clipping_code_size = ROUND_UP(clipping_code_size, 8) - 1; + + uint8_t *overlay_data = UncachedAddr(rsp_magma.data); + memcpy(overlay_data + RSP_MAGMA_MG_CLIPPING_TEXT, &clipping_code_phys, sizeof(clipping_code_phys)); + memcpy(overlay_data + RSP_MAGMA_MG_CLIPPING_TEXT_SIZE, &clipping_code_size, sizeof(clipping_code_size)); + + rspq_init(); + mg_overlay_id = rspq_overlay_register(&rsp_magma); + + is_initialized = true; +} + +void mg_close(void) +{ + if (!is_initialized) return; + + rspq_overlay_unregister(mg_overlay_id); + is_initialized = false; +} + +static void check_shader_binary_compatibility(rsp_ucode_t *shader_ucode) +{ + // FIXME: This currently doesn't work because .bss addresses can vary in shaders. + // The core problem is that in shaders, uniforms rarely take up exactly the maximum allowed amount of memory, + // so the .bss addresses that follow them will shift towards 0. +#if 0 + assertf(memcmp(shader_ucode->code, rsp_magma.code, RSP_MAGMA__MG_OVERLAY - RSP_MAGMA__start) == 0, "Common code of shader %s does not match!", shader_ucode->name); + assertf(memcmp(shader_ucode->data, rsp_magma.data, RSP_MAGMA__MG_VTX_SHADER_UNIFORMS - RSP_MAGMA__data_start) == 0, "Common data of shader %s does not match!", shader_ucode->name); +#endif +} + +static const mg_meta_attribute_t *find_meta_attribute_by_input(const mg_meta_attribute_t *attributes, uint32_t attribute_count, uint32_t input) +{ + for (size_t i = 0; i < attribute_count; i++) + { + if (attributes[i].input == input) { + return &attributes[i]; + } + } + + return NULL; +} + +static const mg_vertex_attribute_t *find_vertex_attribute_by_input(const mg_vertex_layout_t *input_parms, uint32_t input) +{ + for (size_t i = 0; i < input_parms->attribute_count; i++) + { + if (input_parms->attributes[i].input == input) { + return &input_parms->attributes[i]; + } + } + + return NULL; +} + +static uint32_t get_vector_load_offset_shift(uint32_t opcode) +{ + switch (opcode) { + case VLOAD_BYTE: + return 0; + case VLOAD_HALF: + return 1; + case VLOAD_LONG: + return 2; + case VLOAD_DOUBLE: + case VLOAD_PACK: + case VLOAD_UPACK: + return 3; + case VLOAD_QUAD: + case VLOAD_REST: + case VLOAD_HPACK: + case VLOAD_FPACK: + case VLOAD_TRANSPOSE: + return 4; + default: + assertf(0, "Invalid vector loader opcode!"); + return 0; + } +} + +static void patch_vertex_attribute_loader(void *shader_code, uint32_t loader_offset, const mg_vertex_attribute_t *vertex_attribute) +{ + uint32_t *loader_ptr = (uint32_t*)((uint8_t*)shader_code + loader_offset); + uint32_t loader_op = *loader_ptr; + + uint32_t opcode = loader_op >> 26; + + switch (opcode) { + case LB: + case LH: + case LW: + case LBU: + case LHU: + case LWU: + *loader_ptr = (loader_op & 0xFFFF0000) | (vertex_attribute->offset & 0xFFFF); + break; + case LWC2: + uint32_t vl_opcode = (loader_op >> 11) & 0x1F; + uint32_t offset_shift = get_vector_load_offset_shift(vl_opcode); + assertf((vertex_attribute->offset & ((1<input, 1<offset >> offset_shift; + *loader_ptr = (loader_op & 0xFFFFFF80) | (offset & 0x7F); + break; + default: + assertf(0, "Invalid loader opcode!"); + } +} + +static void patch_shader_with_vertex_layout(void *shader_code, const mg_meta_header_t *meta_header, const mg_vertex_layout_t *parms) +{ + // Check that all attributes in the configuration are valid + const mg_meta_attribute_t *attributes = (const mg_meta_attribute_t*)((uint8_t*)meta_header + meta_header->attributes_offset); + for (size_t i = 0; i < parms->attribute_count; i++) + { + const mg_meta_attribute_t *meta_attribute = find_meta_attribute_by_input(attributes, meta_header->attribute_count, parms->attributes[i].input); + assertf(meta_attribute != NULL, "Vertex attribute with input number %ld could not be found!", parms->attributes[i].input); + } + + for (size_t i = 0; i < meta_header->attribute_count; i++) + { + const mg_vertex_attribute_t *vertex_attribute = find_vertex_attribute_by_input(parms, attributes[i].input); + if (vertex_attribute != NULL) { + // If the vertex attribute is enabled, patch all loaders with the correct offset + uint32_t *loaders = (uint32_t*)((uint8_t*)attributes + attributes[i].loaders_offset); + for (size_t j = 0; j < attributes[i].loader_count; j++) + { + patch_vertex_attribute_loader(shader_code, loaders[j], vertex_attribute); + } + } else { + assertf(attributes[i].is_optional, "The vertex attribute with input number %ld was omitted, but it is not optional!", attributes[i].input); + // Otherwise, if the vertex attribute is disabled, apply all patches + mg_meta_attribute_patch_t *patches = (mg_meta_attribute_patch_t*)((uint8_t*)attributes + attributes[i].patches_offset); + for (size_t j = 0; j < attributes[i].patches_count; j++) + { + *(uint32_t*)((uint8_t*)shader_code + patches[j].address) = patches[j].replacement; + } + } + } +} + +static void extract_pipeline_uniforms(mg_pipeline_t *pipeline, mg_meta_header_t *meta_header) +{ + const mg_meta_uniform_t *uniforms = (const mg_meta_uniform_t*)((uint8_t*)meta_header + meta_header->uniforms_offset); + + pipeline->uniform_count = meta_header->uniform_count; + + if (pipeline->uniform_count > 0) { + pipeline->uniforms = calloc(pipeline->uniform_count, sizeof(mg_uniform_t)); + + for (size_t i = 0; i < pipeline->uniform_count; i++) + { + pipeline->uniforms[i].binding = uniforms[i].binding; + pipeline->uniforms[i].offset = uniforms[i].start; + pipeline->uniforms[i].size = uniforms[i].end - uniforms[i].start; + } + } +} + +mg_pipeline_t *mg_pipeline_create(const mg_pipeline_parms_t *parms) +{ + check_shader_binary_compatibility(parms->vertex_shader_ucode); + + mg_pipeline_t *pipeline = malloc(sizeof(mg_pipeline_t)); + + pipeline->vertex_stride = parms->vertex_layout.stride; + + get_overlay_span(parms->vertex_shader_ucode, &pipeline->shader_code, &pipeline->shader_code_size); + + // Copy the shader ucode to a new buffer where it will be patched + // This is cached memory so copying and patching are faster + void *code_copy = memalign(16, ROUND_UP(pipeline->shader_code_size, 16)); + memcpy(code_copy, pipeline->shader_code, pipeline->shader_code_size); + + mg_meta_header_t *meta_header = (mg_meta_header_t*)parms->vertex_shader_ucode->meta; + + // Patch shader ucode according to configured vertex layout + patch_shader_with_vertex_layout(code_copy, meta_header, &parms->vertex_layout); + data_cache_hit_writeback(code_copy, pipeline->shader_code_size); + pipeline->shader_code = code_copy; + + // Extract uniform definitions from ucode metadata + extract_pipeline_uniforms(pipeline, meta_header); + + return pipeline; +} + +void mg_pipeline_free(mg_pipeline_t *pipeline) +{ + if (pipeline->uniforms) { + free(pipeline->uniforms); + } + free(pipeline->shader_code); + free(pipeline); +} + +static const mg_uniform_t *mg_pipeline_try_get_uniform(mg_pipeline_t *pipeline, uint32_t binding) +{ + for (uint32_t i = 0; i < pipeline->uniform_count; i++) + { + if (pipeline->uniforms[i].binding == binding) { + return &pipeline->uniforms[i]; + } + } + + return NULL; +} + +const mg_uniform_t *mg_pipeline_get_uniform(mg_pipeline_t *pipeline, uint32_t binding) +{ + const mg_uniform_t *uniform = mg_pipeline_try_get_uniform(pipeline, binding); + assertf(uniform != NULL, "Uniform with binding number %ld was not found", binding); + return uniform; +} + +bool mg_pipeline_is_uniform_compatible(mg_pipeline_t *pipeline, const mg_uniform_t *uniform) +{ + const mg_uniform_t *matching_uniform = mg_pipeline_try_get_uniform(pipeline, uniform->binding); + return matching_uniform != NULL && matching_uniform->offset == uniform->offset && matching_uniform->size == uniform->size; +} + +void mg_set_vertex_stride(uint32_t vertex_stride) +{ + int16_t v0 = vertex_stride; + int16_t v1 = MG_VTX_SIZE; + int16_t v2 = vertex_stride; + mg_cmd_set_word(offsetof(mg_rsp_state_t, vertex_size), (v0 << 16) | v1); + mg_cmd_set_word(offsetof(mg_rsp_state_t, vertex_size) + sizeof(int16_t)*2, (v2 << 16)); +} + +void mg_pipeline_bind(mg_pipeline_t *pipeline) +{ + uint32_t code = PhysicalAddr(pipeline->shader_code); + uint32_t code_size = pipeline->shader_code_size; + mg_cmd_write(MG_CMD_SET_SHADER, code, ROUND_UP(code_size, 8) - 1); + mg_set_vertex_stride(pipeline->vertex_stride); +} + +static mg_rsp_viewport_t viewport_to_rsp_state(const mg_viewport_t *viewport) +{ + float half_width = viewport->width / 2; + float half_height = viewport->height / 2; + float depth_diff = viewport->maxDepth - viewport->minDepth; + float half_depth = depth_diff / 2; + float z_planes_sum = viewport->z_near + viewport->z_far; + float w_norm_factor = z_planes_sum > 0.0f ? 2.0f / z_planes_sum : 1.0f; + return (mg_rsp_viewport_t) { + .scale = { + half_width * 8, + half_height * 8, + half_depth * 0x7FFF * 2, + (uint16_t)(w_norm_factor * 0xFFFF) + }, + .offset = { + (viewport->x + half_width) * 4, + (viewport->y + half_height) * 4, + (viewport->minDepth + half_depth) * 0x7FFF, + 0 + } + }; +} + + +void mg_set_viewport(const mg_viewport_t *viewport) +{ + mg_rsp_viewport_t rsp_viewport = viewport_to_rsp_state(viewport); + uint32_t value0 = (rsp_viewport.scale[0] << 16) | rsp_viewport.scale[1]; + uint32_t value1 = (rsp_viewport.scale[2] << 16) | rsp_viewport.scale[3]; + uint32_t value2 = (rsp_viewport.offset[0] << 16) | rsp_viewport.offset[1]; + uint32_t value3 = (rsp_viewport.offset[2] << 16) | rsp_viewport.offset[3]; + mg_cmd_set_quad(offsetof(mg_rsp_state_t, viewport), value0, value1, value2, value3); +} + +void mg_uniform_load_inline_raw(uint32_t offset, uint32_t size, const void *data) +{ + assertf((offset&7) == 0, "offset must be a multiple of 8"); + assertf((size&3) == 0, "size must be a multiple of 4"); + assertf(size <= MG_MAX_UNIFORM_PAYLOAD_SIZE, "size must not be greater than %d", MG_MAX_UNIFORM_PAYLOAD_SIZE); + + uint32_t aligned_size = ROUND_UP(size, 8); + + uint32_t command_id = MG_CMD_INLINE_UNIFORM_MAX; + uint32_t command_size = MG_MAX_UNIFORM_PAYLOAD_SIZE; + + if (aligned_size <= 8) { + command_id = MG_CMD_INLINE_UNIFORM_8; + command_size = 8; + } else if (aligned_size <= 16) { + command_id = MG_CMD_INLINE_UNIFORM_16; + command_size = 16; + } else if (aligned_size <= 32) { + command_id = MG_CMD_INLINE_UNIFORM_32; + command_size = 32; + } else if (aligned_size <= 64) { + command_id = MG_CMD_INLINE_UNIFORM_64; + command_size = 64; + } else if (aligned_size <= 128) { + command_id = MG_CMD_INLINE_UNIFORM_128; + command_size = 128; + } + + rspq_write_t w = rspq_write_begin(mg_overlay_id, command_id, (MG_INLINE_UNIFORM_HEADER + command_size)/4); + + // We want to place the payload at an 8-byte aligned address after the end of the command itself + // w.pointer is already +1 from the actual start of the command + // the command itself is 2 words, so advance it by one more word + uint32_t pointer = PhysicalAddr(w.pointer + 1); + bool aligned = (pointer & 0x7) == 0; + + // If the address right after the command is not aligned, advance by another word + rspq_write_arg(&w, aligned ? pointer : pointer + 4); + rspq_write_arg(&w, ((aligned_size-1) << 16) | offset); + + // Write padding for alignment + if (!aligned) { + rspq_write_arg(&w, 0); + } + + const uint32_t *words = data; + + uint32_t word_count = size / sizeof(uint32_t); + for (uint32_t i = 0; i < word_count; i++) + { + rspq_write_arg(&w, words[i]); + } + + rspq_write_end(&w); +} + +static void load_batch_with_mtx_indices(const mg_input_assembly_parms_t *input_assembly_parms, uint32_t first, uint32_t count, uint32_t cache_offset, uint8_t *prev_mtx_index) +{ + if (input_assembly_parms->mtx_indices == NULL) { + mg_load_vertices(first, cache_offset, count); + return; + } + + const uint8_t *mtx_indices = (const uint8_t*)input_assembly_parms->mtx_indices; + uint32_t batch_offset = 0; + + for (size_t i = 0; i < count; i++) + { + uint8_t mtx_index = mtx_indices[(first + i) * input_assembly_parms->mtx_indices_stride]; + if (mtx_index != *prev_mtx_index) { + if (i != 0) { + mg_load_vertices(first + batch_offset, cache_offset + batch_offset, i - batch_offset); + } + mg_uniform_load(&input_assembly_parms->matrix_uniform, ((const uint8_t*)input_assembly_parms->matrices) + mtx_index * input_assembly_parms->matrices_stride); + batch_offset = i; + *prev_mtx_index = mtx_index; + } + } + + if (batch_offset < count) { + mg_load_vertices(first + batch_offset, cache_offset + batch_offset, count - batch_offset); + } +} + +/** @brief The maximum number of separate triangles that can fit into the vertex cache. */ +#define TRI_LIST_ADVANCE_COUNT ROUND_DOWN(MG_VERTEX_CACHE_COUNT, 3) + +void mg_draw(const mg_input_assembly_parms_t *input_assembly_parms, uint32_t vertex_count, uint32_t first_vertex) +{ + assertf(input_assembly_parms != NULL, "input_assembly_parms must not be NULL!"); + + uint32_t cache_offset = 0; + uint32_t next_cache_offset = 0; + uint32_t advance_count = 0; + uint32_t batch_size = 0; + switch (input_assembly_parms->primitive_topology) { + case MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST: + advance_count = TRI_LIST_ADVANCE_COUNT; + batch_size = TRI_LIST_ADVANCE_COUNT; + break; + case MG_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP: + advance_count = MG_VERTEX_CACHE_COUNT - 2; + batch_size = MG_VERTEX_CACHE_COUNT; + break; + case MG_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN: + advance_count = MG_VERTEX_CACHE_COUNT - 1; + batch_size = MG_VERTEX_CACHE_COUNT; + break; + } + + uint8_t mtx_index = UINT8_MAX; + + for (uint32_t current_vertex = 0; current_vertex < vertex_count; current_vertex += advance_count - cache_offset) + { + cache_offset = next_cache_offset; + uint32_t load_count = MIN(batch_size - cache_offset, vertex_count - current_vertex); + + load_batch_with_mtx_indices(input_assembly_parms, current_vertex + first_vertex, load_count, cache_offset, &mtx_index); + + switch (input_assembly_parms->primitive_topology) { + case MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST: + { + size_t prim_count = load_count / 3; + for (size_t i = 0; i < prim_count; i++) mg_draw_triangle(3*i, 3*i+1, 3*i+2); + break; + } + case MG_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP: + { + size_t prim_count = MAX(0, load_count - 2); + for (size_t i = 0; i < prim_count; i++) mg_draw_triangle(i, i + 1 + i%2, i + 2 - i%2); + break; + } + case MG_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN: + { + size_t prim_count = MAX(0, load_count - 2 + cache_offset); + for (size_t i = 0; i < prim_count; i++) mg_draw_triangle(i+1, i+2, 0); + next_cache_offset = 1; + break; + } + } + } +} + +/** @brief An index value that triggers a primitive to be restarted. */ +#define SPECIAL_INDEX UINT16_MAX + +/// @cond +typedef struct vertex_cache_block_s vertex_cache_block; +/// @endcond + +/** @brief Represents a block of consecutive vertices loaded into the vertex cache at some offset. */ +typedef struct vertex_cache_block_s +{ + uint16_t start; ///< The index (in the vertex buffer) of the first vertex in this block. + uint16_t count; ///< The number of vertices in this block. + vertex_cache_block *next; ///< Pointer to the next block to form a linked list that is sorted by "start". +} vertex_cache_block; + +/** @brief Represents a simulated state of the vertex cache. */ +typedef struct +{ + vertex_cache_block blocks[MG_VERTEX_CACHE_COUNT]; ///< Array of blocks. + vertex_cache_block *head; ///< Linked list of currently loaded blocks. + vertex_cache_block *unused; ///< Linked list of unused blocks. + uint32_t total_count; ///< Total count of vertices in the cache. + +} vertex_cache; + +static void vertex_cache_clear(vertex_cache *cache) +{ + cache->total_count = 0; + for (size_t i = 0; i < MG_VERTEX_CACHE_COUNT-1; i++) + { + cache->blocks[i].next = &cache->blocks[i+1]; + } + cache->blocks[MG_VERTEX_CACHE_COUNT-1].next = NULL; + cache->unused = &cache->blocks[0]; + cache->head = NULL; +} + +static vertex_cache_block* vertex_cache_insert_after(vertex_cache *cache, vertex_cache_block *block) +{ + // Make sure there are still unused blocks remaining! + assertf(cache->unused, "Maximum number of blocks reached! This is a bug within magma."); + + // Remove an entry from the list of unused blocks and use it as the new one + vertex_cache_block *new_block = cache->unused; + cache->unused = new_block->next; + + if (block) { + // If block is set, insert after it + new_block->next = block->next; + block->next = new_block; + } else { + // Otherwise insert at the start of the list + new_block->next = cache->head; + cache->head = new_block; + } + + return new_block; +} + +static void vertex_cache_merge_with_next(vertex_cache *cache, vertex_cache_block *block) +{ + vertex_cache_block *next = block->next; + if (!next) { + // Nothing to do if there is no next block + return; + } + + // Check if the two blocks are exactly bordering each other. + // This is suffient since blocks can normally only grow by one at a time. + uint16_t block_end = block->start + block->count; + + if (block_end == next->start) { + // Grow this block to encompass the next one + block->count += next->count; + + // Remove the next block from the list + block->next = next->next; + + // Return it to the list of unused blocks + next->next = cache->unused; + cache->unused = next; + } else { + // Check invariant + assertf(block_end < next->start, "Blocks are overlapping! This is a bug within magma."); + } +} + +static bool vertex_cache_try_insert(vertex_cache *cache, uint16_t index) +{ + if (cache->total_count >= MG_VERTEX_CACHE_COUNT) { + return false; + } + + // Find existing block to insert into + vertex_cache_block *block = cache->head; + vertex_cache_block *prev = NULL; + while (block) + { + if (block->count > 0) { + uint16_t block_end = block->start + block->count; + if (index >= block->start && index < block_end) { + // Index is already contained in this block + return true; + } + + if (index == block_end) { + // Index is the next one after this block. Grow it by one. + block->count++; + cache->total_count++; + + // Try to merge with the next block in case they are now bordering + vertex_cache_merge_with_next(cache, block); + + return true; + } + + if (block->start - index == 1) { + // Index is the previous one before this block. Grow it backwards by one. + block->count++; + block->start--; + cache->total_count++; + + // Try to merge with the previous block in case they are now bordering + if (prev) vertex_cache_merge_with_next(cache, prev); + + return true; + } + + if (block->start > index) { + // Blocks are always sorted. That means if the current block's start + // is after the index, we already know that none of the following blocks + // will contain the index. The new block we need to create has to be inserted + // before the current block. + break; + } + } + + prev = block; + block = block->next; + } + + // If no existing block contained the index, insert new block after "prev". + // If prev is null, there are no blocks yet and this will create the new block at the start of the list. + vertex_cache_block *new_block = vertex_cache_insert_after(cache, prev); + new_block->start = index; + new_block->count = 1; + cache->total_count++; + return true; +} + +static bool vertex_cache_find(const vertex_cache *cache, uint16_t index, uint8_t *cache_index) +{ + uint16_t cur_offset = 0; + vertex_cache_block *block = cache->head; + while (block) + { + uint16_t diff = index - block->start; + if (diff < block->count) { + *cache_index = cur_offset + diff; + return true; + } + cur_offset += block->count; + block = block->next; + } + + return false; +} + +static uint8_t vertex_cache_get(const vertex_cache *cache, uint16_t index) +{ + uint8_t cache_index = 0; + bool found = vertex_cache_find(cache, index, &cache_index); + assertf(found, "Index %d not found in vertex batch! This is a bug within magma.", index); + return cache_index; +} + +static void vertex_cache_load(const vertex_cache *cache, int32_t offset, uint32_t cache_offset, const mg_input_assembly_parms_t *assembly_parms, uint8_t *mtx_index) +{ + vertex_cache_block *block = cache->head; + while (block) + { + if (block->count > 0) { + load_batch_with_mtx_indices(assembly_parms, block->start + offset, block->count, cache_offset, mtx_index); + cache_offset += block->count; + } + + block = block->next; + } +} + +#ifdef MG_DEBUG_VERTEX_CACHE +static void vertex_cache_dump(const vertex_cache *cache) +{ + debugf("vertex cache dump:\n"); + vertex_cache_block *block = cache->head; + while (block) + { + debugf("%d %d\n", block->start, block->count); + block = block->next; + } +} +#endif + +static uint32_t prepare_batch(const mg_input_assembly_parms_t *assembly_parms, const uint16_t *indices, int32_t vertex_offset, uint32_t max_count, vertex_cache *cache, uint32_t windup, uint32_t advance, uint32_t cache_offset, uint8_t *mtx_index) +{ + vertex_cache_clear(cache); + + uint32_t count = 0; + uint32_t required = windup + advance; + + while (count + required <= max_count) + { + bool restart = false; + uint32_t need_insertion = 0; + for (size_t i = 0; i < required; i++) + { + uint16_t index = indices[count + i]; + if (assembly_parms->primitive_restart_enabled && index == SPECIAL_INDEX) { + required = windup + advance; + need_insertion = 0; + count++; + restart = true; + break; + } + + uint8_t dummy; + need_insertion += vertex_cache_find(cache, index, &dummy) ? 0 : 1; + } + + if (restart) { + continue; + } + + /* + TODO: + Doesn't accurately predict whether cache will be overflown because of the multiple of 2 restriction + Possible approaches to fix this: + 1) Try to predict whether the next n vertices (where n is the number required for a full primitive) will overflow the cache + Will likely be very complex and might require some sort of "rollback" functionality for the cache + 2) Abandon trying to create perfectly efficient vertex loading batches and simplify this algorithm so that predicting a cache + overflow becomes easier. Mesh data will be pre-optimized most of the time anyway. + 3) Discard the mg_draw_indexed function altogether. However, GL will have to roll its own version of this algorithm anyway. + 4) Re-consider how vertex caching works in Magma. As an idea: Indices are assumed to be "optimized" already and vertex data is + automatically "paged" into DMEM. This would naturally lead to index buffers and triangle strips as well. + */ + if (cache->total_count + need_insertion + cache_offset > MG_VERTEX_CACHE_COUNT) { + break; + } + + for (size_t i = 0; i < required; i++) + { + vertex_cache_try_insert(cache, indices[count + i]); + } + + count += required; + required = advance; + } + + assertf(cache->total_count + cache_offset <= MG_VERTEX_CACHE_COUNT, "Vertex batch is too big! This is a bug within magma."); + + #ifdef MG_DEBUG_VERTEX_CACHE + vertex_cache_dump(cache); + #endif + + vertex_cache_load(cache, vertex_offset, cache_offset, assembly_parms, mtx_index); + return count; +} + +static void draw_triangle_list_batch(const uint16_t *indices, uint32_t current_index, uint32_t batch_count, const vertex_cache *cache) +{ + uint8_t prim_indices[3]; + for (size_t i = 0; i < batch_count; i++) + { + uint8_t cache_index = vertex_cache_get(cache, indices[current_index + i]); + + prim_indices[i%3] = cache_index; + if (i%3 == 2) { + mg_draw_triangle(prim_indices[0], prim_indices[1], prim_indices[2]); + } + } +} + +static void draw_triangle_strip_batch(const uint16_t *indices, uint32_t current_index, uint32_t batch_count, const vertex_cache *cache, bool restart_enabled) +{ + size_t prim_counter = 0; + uint8_t prim_indices[3]; + for (size_t i = 0; i < batch_count; i++) + { + uint16_t index = indices[current_index + i]; + + if (restart_enabled && index == SPECIAL_INDEX) { + prim_counter = 0; + continue; + } + + uint8_t cache_index = vertex_cache_get(cache, index); + + prim_indices[prim_counter%3] = cache_index; + if (prim_counter>1) { + int p = prim_counter-2; + int p0 = p; + int p1 = p+(1+p%2); + int p2 = p+(2-p%2); + mg_draw_triangle(prim_indices[p0%3], prim_indices[p1%3], prim_indices[p2%3]); + } + + prim_counter++; + } +} + +static void draw_triangle_fan_batch(const uint16_t *indices, uint32_t current_index, uint32_t batch_count, const vertex_cache *cache, bool restart_enabled, uint32_t cache_offset) +{ + uint8_t prim_indices[3] = {0}; + size_t prim_counter = cache_offset; + for (size_t i = 0; i < batch_count; i++) + { + uint16_t index = indices[current_index + i]; + + if (restart_enabled && index == SPECIAL_INDEX) { + prim_counter = 0; + continue; + } + + uint8_t cache_index = vertex_cache_get(cache, index) + cache_offset; + + if (prim_counter == 0) { + prim_indices[2] = cache_index; + } else { + prim_indices[prim_counter%2] = cache_index; + } + + if (prim_counter>1) { + int p = prim_counter-2; + int p0 = p+1; + int p1 = p+2; + mg_draw_triangle(prim_indices[p0%2], prim_indices[p1%2], prim_indices[2]); + } + + prim_counter++; + } +} + +static uint32_t get_advance_count(mg_primitive_topology_t topology) +{ + switch (topology) { + case MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST: + return 3; + case MG_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP: + case MG_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN: + return 1; + default: + assertf(0, "Unknown topology"); + return 3; + } +} + +static uint32_t get_windup_count(mg_primitive_topology_t topology) +{ + switch (topology) { + case MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST: + return 0; + case MG_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP: + case MG_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN: + return 2; + default: + assertf(0, "Unknown topology"); + return 0; + } +} + +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) +{ + assertf(input_assembly_parms != NULL, "input_assembly_parms must not be NULL!"); + assertf(indices != NULL, "indices must not be NULL!"); + + vertex_cache cache; + uint32_t current_index = 0; + + mg_primitive_topology_t topology = input_assembly_parms->primitive_topology; + bool restart_enabled = input_assembly_parms->primitive_restart_enabled; + + assertf(!(restart_enabled && topology == MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST), "Primitive restart is not supported for triangle lists!"); + + uint32_t windup = get_windup_count(topology); + uint32_t advance = get_advance_count(topology); + + uint32_t cache_offset = 0; + + uint8_t mtx_index = UINT8_MAX; + + while (current_index < index_count - windup + cache_offset) + { + uint32_t batch_index_count = prepare_batch(input_assembly_parms, indices + current_index, vertex_offset, index_count - current_index, &cache, windup, advance, cache_offset, &mtx_index); + + switch (topology) { + case MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST: + draw_triangle_list_batch(indices, current_index, batch_index_count, &cache); + break; + case MG_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP: + draw_triangle_strip_batch(indices, current_index, batch_index_count, &cache, restart_enabled); + break; + case MG_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN: + draw_triangle_fan_batch(indices, current_index, batch_index_count, &cache, restart_enabled, cache_offset); + cache_offset = 1; + break; + } + + current_index += batch_index_count - windup + cache_offset; + } +} + +/* TODO: Index buffers + Instead of emitting one command for each triangle, store vertex indices in a buffer + and emit command a command that reads n indices from this buffer and then draws those triangles. + Tiny3D has shown that this improves performance. + Possible API: + + // Binds the index buffer used by mg_draw_indices + void mg_bind_index_buffer(const uint16_t *buffer); + + // Loads indices from the buffer bound using mg_bind_index_buffer. + // It starts loading from first_index and loads index_count indices. + // TODO: which topologies are supported? Only triangle list? + // TODO: naming + void mg_draw_indices(uint32_t index_count, uint32_t first_index); +*/ + +/* TODO: Instanced draw calls? + There is probably no real benefit other than having a fancy API. + On modern hardware, instancing is a technique to reduce the number of draw calls, + thus reducing the amount of data sent from CPU to GPU. + + On N64, this doesn't really apply + unless the RSP would drive the input assembly. We could investigate that possibility, + but it is questionable how much performance this could realistically save, since this workload + can't really be vectorized and the vertex/index data still needs to be transferred from RDRAM anyway. + + As it stands, the assembly of vertex loading and triangle drawing commands is + done on the CPU and therefore instancing would not lead to a performance gain. +*/ + +/* TODO: Indirect draw calls? + As opposed to instancing, indirect rendering could have some actual benefits. + For example, one could compute visibility of objects on the RSP and dynamically emit + draw calls only for visible objects, which would then be run indirectly. + + The implementation might simply use dynamically assembled rspq blocks internally. + So perhaps this could be a generic feature of rspq instead of being specific to magma. +*/ + + +/// @cond +extern inline void mg_cmd_set_byte(uint32_t offset, uint8_t value); +extern inline void mg_cmd_set_short(uint32_t offset, uint16_t value); +extern inline void mg_cmd_set_word(uint32_t offset, uint32_t value); +extern inline void mg_cmd_set_quad(uint32_t offset, uint32_t value0, uint32_t value1, uint32_t value2, uint32_t value3); +extern inline uint8_t mg_culling_parms_to_rsp_state(const mg_culling_parms_t *culling); +extern inline void mg_set_culling(const mg_culling_parms_t *culling); +extern inline void mg_set_geometry_flags(mg_geometry_flags_t flags); +extern inline void mg_set_viewport_fullscreen(resolution_t res); +extern inline void mg_set_clip_factor(uint32_t factor); +extern inline void mg_uniform_load(const mg_uniform_t *uniform, const void *data); +extern inline void mg_uniform_load_inline(const mg_uniform_t *uniform, const void *data); +extern inline void mg_bind_vertex_buffer(const void *buffer); +/// @endcond diff --git a/src/magma/magma_internal.h b/src/magma/magma_internal.h new file mode 100644 index 000000000..ac01b0288 --- /dev/null +++ b/src/magma/magma_internal.h @@ -0,0 +1,32 @@ +/** + * @file magma_internal.h + * @author Dennis Heinze + */ +#ifndef __LIBDRAGON_MAGMA_INTERNAL_H +#define __LIBDRAGON_MAGMA_INTERNAL_H + +#include "magma.h" + +/** @brief A pipeline instance */ +typedef struct mg_pipeline_s +{ + void *shader_code; ///< Pointer to the duplicated and patched shader ucode text. + uint32_t shader_code_size; ///< Size of the duplicated and patched shader ucode text. + uint32_t vertex_stride; ///< Stride of the vertex layout. + uint32_t uniform_count; ///< Number of uniforms. + mg_uniform_t *uniforms; ///< List of uniforms. +} mg_pipeline_t; + +/** + * @brief Returns a pointer to the magma ucode state in RDRAM. + */ +mg_rsp_state_t *mg_get_rsp_state(); + +/** + * @brief Sets the current vertex stride that is used to load vertices. + * + * This is an internal method that is normally implicitly called by #mg_pipeline_bind. + */ +void mg_set_vertex_stride(uint32_t vertex_stride); + +#endif diff --git a/src/magma/magma_math.c b/src/magma/magma_math.c new file mode 100644 index 000000000..8b3deceea --- /dev/null +++ b/src/magma/magma_math.c @@ -0,0 +1,38 @@ +/** + * @file magma_math.c + * @author Dennis Heinze + */ +#include "magma_math.h" +#include "assert.h" + +void mg_mat4_perspective(fm_mat4_t *out, float fovy, float aspect, float z_near, float z_far) +{ + assert(fovy != 0.0f); + assert(aspect != 0.0f); + assert(z_near != z_far); + + float sine, cosine, cotangent, deltaZ; + float radians = fovy / 2; + deltaZ = z_near - z_far; + fm_sincosf(radians, &sine, &cosine); + cotangent = cosine / sine; + + // Positive Y points downwards in screen space. Therefore we flip it here to make it point upwards in view space. + *out = (fm_mat4_t){{ + { cotangent / aspect, 0, 0, 0 }, + { 0, -cotangent, 0, 0 }, + { 0, 0, z_far / deltaZ, -1 }, + { 0, 0, z_near * z_far / deltaZ, 0 }, + }}; +} + +void mg_mat4_ortho(fm_mat4_t *out, float l, float r, float b, float t, float n, float f) +{ + // Positive Y points downwards in screen space. Therefore we flip it here to make it point upwards in view space. + *out = (fm_mat4_t){{ + { 2.0f/(r-l), 0, 0, 0 }, + { 0, -2.0f/(t-b), 0, 0 }, + { 0, 0, -2.0f/(f-n), 0 }, + { -(r+l)/(r-l), -(t+b)/(t-b), -(f+n)/(f-n), 1 } + }}; +} diff --git a/src/magma/mgfx.c b/src/magma/mgfx.c new file mode 100644 index 000000000..4bd82e8dd --- /dev/null +++ b/src/magma/mgfx.c @@ -0,0 +1,146 @@ +/** + * @file mgfx.c + * @author Dennis Heinze + */ +#include "mgfx.h" +#include +#include +#include "utils.h" +#include "rsp_asm.h" + +/** @brief Helper macro to convert normalized u8 to normalized i16 */ +#define U8_TO_I16(x) ((x) << 7) +/** @brief Helper macro to convert float to normalized i16 */ +#define FLOAT_TO_I16(x) (CLAMP(x, -1.f, 1.f) * 0x7FFF) + +/** @brief mgfx standard ucode overlay */ +DEFINE_RSP_UCODE(rsp_mgfx); +/** @brief mgfx environment mapping ucode overlay */ +DEFINE_RSP_UCODE(rsp_mgfx_env); + +static void mgfx_convert_matrix(mgfx_matrix_t *dst, const float *src) +{ + for (uint32_t i = 0; i < 16; i++) + { + int32_t fixed = src[i] * (1<<16); + dst->i[i] = (int16_t)((fixed & 0xFFFF0000) >> 16); + dst->f[i] = (uint16_t)(fixed & 0x0000FFFF); + } +} + +void mgfx_get_matrices(mgfx_matrices_t *dst, const mgfx_matrices_parms_t *parms) +{ + mgfx_convert_matrix(&dst->mvp, parms->model_view_projection); + mgfx_convert_matrix(&dst->mv, parms->model_view); + + for (uint32_t i = 0; i < 16; i++) + { + int32_t v = parms->normal[i] * (1<<15); + dst->normal[i] = CLAMP(v, INT16_MIN, INT16_MAX); + } +} + +void mgfx_get_texturing(mgfx_texturing_t *dst, const mgfx_texturing_parms_t *parms) +{ + dst->tex_scale[0] = parms->scale[0]; + dst->tex_scale[1] = parms->scale[1]; + dst->tex_offset[0] = parms->offset[0]; + dst->tex_offset[1] = parms->offset[1]; +} + +static inline void color_to_i16(int16_t *dst, color_t color) +{ + dst[0] = U8_TO_I16(color.r); + dst[1] = U8_TO_I16(color.g); + dst[2] = U8_TO_I16(color.b); +} + +static int mgfx_get_light(mgfx_light_t *dst, const mgfx_light_parms_t *parms) +{ + color_to_i16(dst->color, parms->color); + + // The user should pre-transform positional lights into eye-space + + const fm_vec4_t *p = &parms->position; + // If W is zero then the light is directional + if (p->w == 0.0f) { + // Pre-normalize the light direction + float magnitude = sqrtf(p->x * p->x + p->y * p->y + p->z * p->z); + dst->position[0] = -FLOAT_TO_I16(p->x / magnitude); + dst->position[1] = -FLOAT_TO_I16(p->y / magnitude); + dst->position[2] = -FLOAT_TO_I16(p->z / magnitude); + dst->position[3] = 0; + dst->intensity = 0; + return 0; + } else { + dst->position[0] = p->x; + dst->position[1] = p->y; + dst->position[2] = p->z; + dst->position[3] = 1; + dst->intensity = sqrtf(parms->intensity) * (1<<6); + return 1; + } +} + +void mgfx_get_lighting(mgfx_lighting_t *dst, const mgfx_lighting_parms_t *parms) +{ + assertf(parms->light_count <= MGFX_LIGHT_COUNT_MAX, "Light count must be %d or less!", MGFX_LIGHT_COUNT_MAX); + + dst->count = parms->light_count; + color_to_i16(dst->ambient, parms->ambient_color); + int point_light_count = 0; + for (size_t i = 0; i < parms->light_count; i++) + { + point_light_count += mgfx_get_light(&dst->lights[i], &parms->lights[i]); + } + dst->has_points = point_light_count; +} + +void mgfx_get_fog(mgfx_fog_t *dst, const mgfx_fog_parms_t *parms) +{ + float diff = parms->start - parms->end; + // start == end is undefined, so disable fog by setting the factor to 0 + bool is_disabled = fabsf(diff) < FLT_MIN; + float factor = is_disabled ? 0.0f : 1.0f / diff; + float offset = -parms->end; + + int32_t offset_fx = offset * (1<<16); + // Premultiply with 1.15 conversion factor + int16_t factor_fx = factor * (1<<15); + + int16_t offset_i = offset_fx >> 16; + uint16_t offset_f = offset_fx & 0xFFFF; + + dst->factor_int = factor_fx; + dst->offset_int = offset_i; + dst->offset_frac = offset_f; + dst->mask = is_disabled ? 0xFF : 0x77; +} + +void mgfx_set_matrices_inline(const mg_uniform_t *uniform, const mgfx_matrices_parms_t *parms) +{ + mgfx_matrices_t matrices = {}; + mgfx_get_matrices(&matrices, parms); + mg_uniform_load_inline(uniform, &matrices); +} + +void mgfx_set_texturing_inline(const mg_uniform_t *uniform, const mgfx_texturing_parms_t *parms) +{ + mgfx_texturing_t texturing = {}; + mgfx_get_texturing(&texturing, parms); + mg_uniform_load_inline(uniform, &texturing); +} + +void mgfx_set_lighting_inline(const mg_uniform_t *uniform, const mgfx_lighting_parms_t *parms) +{ + mgfx_lighting_t lighting = {}; + mgfx_get_lighting(&lighting, parms); + mg_uniform_load_inline(uniform, &lighting); +} + +void mgfx_set_fog_inline(const mg_uniform_t *uniform, const mgfx_fog_parms_t *parms) +{ + mgfx_fog_t fog = {}; + mgfx_get_fog(&fog, parms); + mg_uniform_load_inline(uniform, &fog); +} diff --git a/src/magma/rsp_magma.S b/src/magma/rsp_magma.S new file mode 100644 index 000000000..6adbedd12 --- /dev/null +++ b/src/magma/rsp_magma.S @@ -0,0 +1,17 @@ +############################################################ +# Main magma ucode overlay +# +# Authors: Dennis Heinze +# +############################################################ +#include + +_MgDefineUniformRegion + +_MgBeginShader + # Bootstrap shader code + # This code is only executed when the magma ucode has just been switched to. + # Otherwise, if magma is already loaded, the shader code replaces this one. + # In the latter case rspq will directly jump into the shader code when loading vertices. + _MgBootstrapShader +MgEndShader diff --git a/src/magma/rsp_magma_clipping.S b/src/magma/rsp_magma_clipping.S new file mode 100644 index 000000000..7af8fc8a5 --- /dev/null +++ b/src/magma/rsp_magma_clipping.S @@ -0,0 +1,438 @@ +############################################################ +# Magma clipping ucode sub-overlay +# +# Authors: Giovanni Bajo +# Dennis Heinze +# +############################################################ +#include + +_MgDefineUniformRegion + +_MgBeginShader + # We also need to bootstrap the shader here, so that it gets reloaded when required after clipping is done. + _MgBootstrapShader + +# IMPORTANT: We want to define the actual clipping routine instead of the bootstrapper that is contained in "MgEndShader", +# which is why that routine is omitted here deliberately. + +Mg_Clipping: + #define vtx1 a0 + #define vtx2 a2 + #define vtx3 a1 + #define clip_flags t5 + #define out_count v1 + #define plane_flag t6 + #define in_count t7 + #define in_end t8 + #define in_list s0 + #define out_list s1 + #define plane s2 + #define intersection s3 + #define cur_ptr s4 + #define prev_ptr s5 + #define cur_vtx s6 + #define prev_vtx s7 + #define p0 k0 + #define p1 k1 + + #define vplane $v01 + #define vint_f $v02 + #define vint_i $v03 + #define vdot_i $v04 + #define vdot_f $v05 + #define vdiff_i $v06 + #define vdiff_f $v07 + #define va_i $v08 + #define va_f $v09 + #define vpos_i $v10 + #define vpos_f $v11 + #define vattr0 $v12 + #define vattr1 $v13 + #define voff0 $v14 + #define voff1 $v15 + #define vcache0 $v16 + #define vcache1 $v17 + #define vclip_factors $v18 + #define vviewscale $v19 + #define vviewoff $v22 + #define v___ $v29 + + srl clip_flags, t3, 8 + li in_list, %lo(MG_CLIP_LIST1) + + # Copy the three original vertices to the out_list + # (after the initial swap they will be in the in_list) + li out_list, %lo(MG_CLIP_LIST0) + li out_count, 3*2 + addi t0, rspq_dmem_buf_ptr, %lo(RSPQ_DMEM_BUFFER) - 6 + ldv v___, 0,t0 + sdv v___, 0,out_list + + li plane, %lo(MG_CLIP_PLANES) + li plane_flag, 1 + + # Load cache offsets + li t0, %lo(MG_CLIP_CACHE_OFFSETS) + vxor voff1, voff1 + lqv voff0, 0,t0 + lsv voff1, 16,t0 + + # Temporarily use the output buffer as a map of which cache slots are used + # Init to zero + li t0, %lo(MG_CLIP_CACHE_USED) + sqv vzero, 0,t0 + sqv vzero, 16,t0 + + li t0, %lo(MG_VIEWPORT) + ldv vviewscale, 0,t0 + ldv vviewoff, 8,t0 + + li t0, %lo(MG_CLIP_FACTORS) + ldv vclip_factors.e0, 0x0,t0 + ldv vclip_factors.e4, 0x0,t0 + + # Iterate over the 6 clipping planes +mg_clip_plane_loop: + and t0, clip_flags, plane_flag + beqz t0, mg_clip_plane_loop_end + move t1, in_list + + # Swap in and out lists + + # If the out list is empty from the last iteration, + # the triangle has no visible points and we are done + beqz out_count, RSPQ_Loop + move in_list, out_list + move out_list, t1 + move in_count, out_count + move out_count, zero + + # Repeat plane coefficients twice + ldv vplane.e0, 0,plane + ldv vplane.e4, 0,plane + + vmudh vplane, vclip_factors + + # Iterate over the egdes of the polygon in the input list + # The current edge is between cur_vtx and prev_vtx + move cur_ptr, in_list + add in_end, in_list, in_count + # Init the "previous" vertex to the last in the list for the wrap-around + addi prev_ptr, in_end, -2 + +mg_clip_edge_loop: + #define cur_flag t3 + #define prev_flag t4 + + # Check which side of the plane the two vertices are on + lhu cur_vtx, 0(cur_ptr) + lhu prev_vtx, 0(prev_ptr) + lbu cur_flag, MG_VTX_CLIP_CODE(cur_vtx) + lbu prev_flag, MG_VTX_CLIP_CODE(prev_vtx) + and cur_flag, plane_flag + and prev_flag, plane_flag + + # If they are on opposite sides, there is an intersection + xor t0, cur_flag, prev_flag + beqz t0, mg_clip_no_intersection + move p0, cur_vtx + + # Swap the two points if necessary to make intersection calculation consistent + # This will make sure p0 is always inside and p1 is always outside + bnez prev_flag, 1f + move p1, prev_vtx + xor p0, p0, p1 + xor p1, p0, p1 + xor p0, p0, p1 + + #undef prev_flag + +1: + # Calculate intersection of the line segment and the plane + + li t0, %lo(MG_CLIP_CACHE_USED) + lqv vcache0, 0,t0 + lqv vcache1, 16,t0 + + # vpos: x0 y0 z0 w0 x1 y1 z1 w1 + ldv vpos_i.e0, MG_VTX_CS_POSi,p0 + ldv vpos_f.e0, MG_VTX_CS_POSf,p0 + ldv vpos_i.e4, MG_VTX_CS_POSi,p1 + ldv vpos_f.e4, MG_VTX_CS_POSf,p1 + + # vint: x1 y1 z1 w1 + ldv vint_i.e0, MG_VTX_CS_POSi,p1 + ldv vint_f.e0, MG_VTX_CS_POSf,p1 + + # vattr0: r0 g0 b0 a0 s0 t0 + luv vattr0.e0, MG_VTX_RGBA ,p0 + llv vattr0.e4, MG_VTX_ST ,p0 + + # vattr1: r1 g1 b1 a1 s1 t1 + luv vattr1.e0, MG_VTX_RGBA ,p1 + llv vattr1.e4, MG_VTX_ST ,p1 + + # Find first free slot in clip cache + + # Add the values from the "used slots map" to the cache offsets + # After this, each lane will contain the offset of its corresponding cache slot, + # but only if the slot is not used. If it is used, it will contain some large value. + vaddc vcache0, voff0 + vaddc vcache1, voff1 + + # Look for the smallest value, which will end up in vcache.e0 + # Because used slots are marked as large values, they will never be found. + vlt vcache0, vcache0.q1 + vlt vcache0, vcache0.h2 + vlt vcache0, vcache0.e4 + vlt vcache0, vcache1.e0 + + mfc2 t0, vcache0.e0 + + # Mark slot as used by storing some large value (careful of overflows!) + li t1, 0xFF + sh t1, %lo(MG_CLIP_CACHE_USED)-2(t0) + + # t0 is the index multiplied by 2 + # intersection = t0 * 20 = t0 * 16 + t0 * 4 + sll intersection, t0, 4 + sll t1, t0, 2 + add intersection, t1 + + # CAUTION: intersection might point to the same address as either p0 or p1, + # because one of them is the previous point, which could have been marked unused + # in the previous iteration. As long as we don't access p0 or p1 after writing to + # intersection, this is fine. + addi intersection, %lo(MG_CLIP_CACHE) - MG_VTX_SIZE + + # Store the cache offset in unused memory (used later when finding the cache slot to mark as unused) + sb t0, MG_VTX_TR_CODE(intersection) + + # Compute dot products of both positions with the clip plane + # vdot.e0: d0 = dot(p0, plane) + # vdot.e4: d1 = dot(p1, plane) + vmudn vdot_f, vpos_f, vplane + vmadh vdot_i, vpos_i, vplane + vaddc vdot_f, vdot_f.q1 + vadd vdot_i, vdot_i.q1 + vaddc vdot_f, vdot_f.h2 + vadd vdot_i, vdot_i.h2 + + # d0 - d1 + vsubc vdiff_f, vdot_f, vdot_f.e4 + vsub vdiff_i, vdot_i, vdot_i.e4 + + # 1 / (d0 - d1) + vrcph v___.e0, vdiff_i.e0 + vrcpl va_f.e0, vdiff_f.e0 + vrcph va_i.e0, vzero.e0 + + # a = d0 / (d0 - d1) + vmudl v___, va_f, vdot_f.e0 + vmadm v___, va_i, vdot_f.e0 + vmadn va_f, va_f, vdot_i.e0 + + # Prepare 0x7FFF in va_i.e0 + vsubc va_i, vshift8, K1 + + # a = min(a, 1) + vge v___, va_f, vzero + vmrg va_f, va_f, va_i.e0 + + # Account for right shift introduced by vrcp + vmudn va_f, va_f, K2 + + # p1 - p0 + vsubc vint_f, vpos_f + vsub vint_i, vpos_i + # attr1 - attr0 + vsubc vattr1, vattr0 + + # Result of linear interpolation: + # p0 + a * (p1 - p0) + vmudl v___, vint_f, va_f.e0 + vmadm v___, vint_i, va_f.e0 + vmadn vint_f, vpos_f, K1 + vmadh vint_i, vpos_i, K1 + + # a * (attr1 - attr0) + vmudm vattr1, vattr1, va_f.e0 + + # attr0 + a * (attr1 - attr0) + vaddc vattr0, vattr1 + + # Store results + sdv vint_i.e0, MG_VTX_CS_POSi,intersection + sdv vint_f.e0, MG_VTX_CS_POSf,intersection + suv vattr0.e0, MG_VTX_RGBA ,intersection + slv vattr0.e4, MG_VTX_ST ,intersection + + # Re-calculate clip codes + vmudn vint_f, vclip_factors + vmadh vint_i, vclip_factors + vch v___, vint_i, vint_i.h3 + vcl v___, vint_f, vint_f.h3 + cfc2 t0, COP2_CTRL_VCC + andi t0, 0x707 + srl t1, t0, 5 + or t0, t1 + # Set the MSB of clip codes to 1 to mark this vertex as used + ori t0, 0x80 + sb t0, MG_VTX_CLIP_CODE(intersection) + + # Add intersection to the output list + add t0, out_list, out_count + sh intersection, 0(t0) + addi out_count, 2 + +mg_clip_no_intersection: + # If cur_vtx is inside, add it to the output list + bnez cur_flag, 1f + add t0, out_list, out_count + sh cur_vtx, 0(t0) + b mg_clip_edge_loop_end + addi out_count, 2 + + #undef cur_flag + +1: + # Check if the vertex is stored in the clip cache + lb t0, MG_VTX_CLIP_CODE(cur_vtx) + bgez t0, mg_clip_edge_loop_end + lb t1, MG_VTX_TR_CODE(cur_vtx) + + # Set the MSB of the clip codes to 0 to mark this vertex as unused + andi t0, 0x7F + sb t0, MG_VTX_CLIP_CODE(cur_vtx) + # Also mark it as unused within the cache so it can be found again when searching for unused vertices + sh zero, %lo(MG_CLIP_CACHE_USED)-2(t1) + +mg_clip_edge_loop_end: + # Advance to the next edge + addi cur_ptr, 2 + blt cur_ptr, in_end, mg_clip_edge_loop + addi prev_ptr, cur_ptr, -2 + +mg_clip_plane_loop_end: + # Advance to the next clipping plane + sll plane_flag, 1 + blt plane_flag, (1< + +.set noreorder +.set noat +.set nomacro + +#undef zero +#undef at +#undef v0 +#undef v1 +#undef a0 +#undef a1 +#undef a2 +#undef a3 +#undef t0 +#undef t1 +#undef t2 +#undef t3 +#undef t4 +#undef t5 +#undef t6 +#undef t7 +#undef s0 +#undef s1 +#undef s2 +#undef s3 +#undef s4 +#undef s5 +#undef s6 +#undef s7 +#undef t8 +#undef t9 +#undef k0 +#undef k1 +#undef gp +#undef sp +#undef fp +#undef ra +.equ hex.$zero, 0 +.equ hex.$at, 1 +.equ hex.$v0, 2 +.equ hex.$v1, 3 +.equ hex.$a0, 4 +.equ hex.$a1, 5 +.equ hex.$a2, 6 +.equ hex.$a3, 7 +.equ hex.$t0, 8 +.equ hex.$t1, 9 +.equ hex.$t2, 10 +.equ hex.$t3, 11 +.equ hex.$t4, 12 +.equ hex.$t5, 13 +.equ hex.$t6, 14 +.equ hex.$t7, 15 +.equ hex.$s0, 16 +.equ hex.$s1, 17 +.equ hex.$s2, 18 +.equ hex.$s3, 19 +.equ hex.$s4, 20 +.equ hex.$s5, 21 +.equ hex.$s6, 22 +.equ hex.$s7, 23 +.equ hex.$t8, 24 +.equ hex.$t9, 25 +.equ hex.$k0, 26 +.equ hex.$k1, 27 +.equ hex.$gp, 28 +.equ hex.$sp, 29 +.equ hex.$fp, 30 +.equ hex.$ra, 31 +#define vco 0 +#define vcc 1 +#define vce 2 + +MgBeginShaderUniforms + MgBeginUniform MATRICES, 0 + .align 4 + MATRICES_MVP: .ds.b 64 + .align 4 + MATRICES_MV: .ds.b 64 + .align 4 + MATRICES_NORMAL: .ds.b 32 + MgEndUniform + + MgBeginUniform TEXTURING, 1 + .align 1 + TEXTURING_SCALE: .ds.b 4 + .align 1 + TEXTURING_OFFSET: .ds.b 4 + MgEndUniform + + MgBeginUniform LIGHTING, 2 + LIGHTING_LIGHTS: .ds.b 128 + .align 1 + LIGHTING_AMBIENT: .ds.b 6 + LIGHTING_HAS_POINTS: .ds.b 1 + LIGHTING_COUNT: .ds.b 1 + MgEndUniform + + MgBeginUniform FOG, 3 + .align 1 + FOG_PARAMS: .ds.b 6 + FOG_MASK: .ds.b 1 + MgEndUniform + +MgEndShaderUniforms + +MgBeginVertexInput + MgBeginVertexAttribute 0, 0 + MgVertexAttributeLoaders LOAD_POSITION380, LOAD_POSITION381, LOAD_POSITION645, LOAD_POSITION646 + MgEndVertexAttribute + + MgBeginVertexAttribute 1, 1 + MgVertexAttributeLoaders LOAD_NORMAL378, LOAD_NORMAL379, LOAD_NORMAL643, LOAD_NORMAL644 + MgEndVertexAttribute + + MgBeginVertexAttribute 2, 1 + MgVertexAttributeLoaders LOAD_COLOR382, LOAD_COLOR383, LOAD_COLOR648, LOAD_COLOR647 + MgBeginVertexAttributePatch PATCH_COLOR572 + vnop + MgEndVertexAttributePatch + MgEndVertexAttribute + + MgBeginVertexAttribute 3, 1 + MgVertexAttributeLoaders LOAD_TEXCOORD410, LOAD_TEXCOORD413 + MgEndVertexAttribute + +MgEndVertexInput + +MgBeginShader + ori $at, $zero, %lo(MG_VERTEX_SIZE) ## L:292 | ^ | vec16 vvtx_size = load(MG_VERTEX_SIZE).xyzw; + addiu $t0, $gp, %lo(RSPQ_DMEM_BUFFER) -6 ## L:291 | 2 | u32 inputs_ptr = get_cmd_address(0x2); + ldv $v02, 0, 0, $t0 ## L:293 | 3 | vec16 vinputs = load(inputs_ptr).xyzw; + ldv $v01, 0, 0, $at ## L:292 | 4 | vec16 vvtx_size = load(MG_VERTEX_SIZE).xyzw; + ori $at, $zero, %lo(RSPQ_SCRATCH_MEM) ## L:296 | ***8 | @Barrier("inputs0") store(vproducts:ufract.xyzw, RSPQ_SCRATCH_MEM); ## Barrier: 0x1 + vmudh $v04, $v02, $v01.v ## L:295 | ^ | vec32 vproducts = vinputs * vvtx_size; + ori $s4, $zero, %lo(MG_VERTEX_CACHE_END) ## L:317 | 9 | vtx_in0_ptr = MG_VERTEX_CACHE_END; + vsar $v03, COP2_ACC_HI ## L:295 | ^ | vec32 vproducts = vinputs * vvtx_size; + lw $s0, %lo(MG_VERTEX_BUFFER + 0) ## L:309 | 10 | u32<$s0> vtx_buffer = load(MG_VERTEX_BUFFER); + vsar $v04, COP2_ACC_MD ## L:295 | ^ | vec32 vproducts = vinputs * vvtx_size; + ssv $v03, 0, 8, $at ## L:297 | **13 | @Barrier("inputs1") store(vproducts.x, RSPQ_SCRATCH_MEM, 0x8); ## Barrier: 0x2 + ssv $v04, 0, 10, $at ## L:297 | 14 | @Barrier("inputs1") store(vproducts.x, RSPQ_SCRATCH_MEM, 0x8); ## Barrier: 0x2 + lw $t2, %lo(RSPQ_SCRATCH_MEM + 8) ## L:307 | 15 | @Barrier("inputs1") buffer_offset = load(RSPQ_SCRATCH_MEM, 0x8); ## Barrier: 0x2 + sdv $v04, 0, 0, $at ## L:296 | 16 | @Barrier("inputs0") store(vproducts:ufract.xyzw, RSPQ_SCRATCH_MEM); ## Barrier: 0x1 + lhu $s7, %lo(RSPQ_SCRATCH_MEM + 2) ## L:303 | 17 | @Barrier("inputs0") vtx_out0_ptr = load(RSPQ_SCRATCH_MEM, 0x2); ## Barrier: 0x1 + lhu $t1, %lo(RSPQ_SCRATCH_MEM + 4) ## L:305 | 18 | @Barrier("inputs0") vertices_size = load(RSPQ_SCRATCH_MEM, 0x4); ## Barrier: 0x1 + andi $t3, $t2, 7 ## L:312 | 19 | u32 misalignment = buffer_offset & 0x7; + addu $s0, $s0, $t2 ## L:310 | 20 | vtx_buffer += buffer_offset; + addu $t0, $t1, $t3 ## L:313 | 21 | dma_size = vertices_size + misalignment; + addiu $t0, $t0, 7 ## L:314 | 22 | dma_size += 0x7; + addiu $t2, $zero, 12 ## L:320 | 23 | dma_in(vtx_in0_ptr, vtx_buffer, dma_size); + andi $t0, $t0, 4088 ## L:315 | 24 | dma_size &= 0xFF8; + subu $s4, $s4, $t0 ## L:318 | 25 | vtx_in0_ptr -= dma_size; + jal DMAExec ## L:320 | 26 | dma_in(vtx_in0_ptr, vtx_buffer, dma_size); ## Args: $t0, $t1, $s0, $s4, $t2 + addiu $t0, $t0, -1 ## L:320 | *28 | dma_in(vtx_in0_ptr, vtx_buffer, dma_size); + ori $at, $zero, %lo(MATRICES_NORMAL) ## L:339 | 29 | vmn0 = load(MATRICES_NORMAL, 0x00).xyzwxyzw; + addu $s6, $s4, $t1 ## L:325 | 30 | vtx_in_end = vtx_in0_ptr + vertices_size; + ori $t0, $zero, %lo(MATRICES_MVP) ## L:334 | 31 | u16 mvpaddr = MATRICES_MVP; + ldv $v02, 0, 32, $t0 ## L:273 | 32 | m0:ufract = load(address, 0x20).xyzwxyzw; + ldv $v03, 0, 8, $t0 ## L:274 | 33 | m1:sint = load(address, 0x08).xyzwxyzw; + ldv $v01, 0, 0, $t0 ## L:272 | 34 | m0:sint = load(address, 0x00).xyzwxyzw; + ldv $v07, 0, 24, $t0 ## L:278 | 35 | m3:sint = load(address, 0x18).xyzwxyzw; + ldv $v06, 0, 48, $t0 ## L:277 | 36 | m2:ufract = load(address, 0x30).xyzwxyzw; + ldv $v10, 0, 8, $at ## L:340 | 37 | vmn1 = load(MATRICES_NORMAL, 0x08).xyzwxyzw; + lhu $t8, %lo(MG_VERTEX_SIZE + 0) ## L:322 | 38 | vtx_size = load(MG_VERTEX_SIZE); + ldv $v06, 8, 48, $t0 ## L:277 | 39 | m2:ufract = load(address, 0x30).xyzwxyzw; + ldv $v08, 0, 56, $t0 ## L:279 | 40 | m3:ufract = load(address, 0x38).xyzwxyzw; + ldv $v05, 0, 16, $t0 ## L:276 | 41 | m2:sint = load(address, 0x10).xyzwxyzw; + ori $t4, $zero, %lo(MG_SCRATCH_MEM) ## L:366 | 42 | u16 prev_out0 = MG_SCRATCH_MEM; + ldv $v11, 0, 16, $at ## L:341 | 43 | vmn2 = load(MATRICES_NORMAL, 0x10).xyzwxyzw; + ldv $v08, 8, 56, $t0 ## L:279 | 44 | m3:ufract = load(address, 0x38).xyzwxyzw; + addu $t2, $s4, $t8 ## L:363 | 45 | u16 vtx_in1_ptr = vtx_in0_ptr + vtx_size; + ldv $v01, 8, 0, $t0 ## L:272 | 46 | m0:sint = load(address, 0x00).xyzwxyzw; + addiu $s7, $s7, %lo(MG_VERTEX_CACHE) ## L:326 | 47 | vtx_out0_ptr += MG_VERTEX_CACHE; + ldv $v02, 8, 32, $t0 ## L:273 | 48 | m0:ufract = load(address, 0x20).xyzwxyzw; + LOAD_POSITION380: ldv $v19, 0, 0, $s4 ## L:380 | 49 | @AttrLoader("POSITION") vpos.xyzw = load(vtx_in0_ptr).xyzw; + LOAD_COLOR382: llv $v23, 0, 0, $s4 ## L:382 | 50 | @AttrLoader("COLOR") vrgba_in.xy = load(vtx_in0_ptr).xy; + ldv $v04, 0, 40, $t0 ## L:275 | 51 | m1:ufract = load(address, 0x28).xyzwxyzw; + LOAD_NORMAL378: lsv $v20, 0, 0, $s4 ## L:378 | 52 | @AttrLoader("NORMAL") vnorm.x = load(vtx_in0_ptr).x; + ldv $v10, 8, 8, $at ## L:340 | 53 | vmn1 = load(MATRICES_NORMAL, 0x08).xyzwxyzw; + ldv $v05, 8, 16, $t0 ## L:276 | 54 | m2:sint = load(address, 0x10).xyzwxyzw; + ori $t5, $zero, %lo(MG_SCRATCH_MEM) ## L:367 | 55 | u16 prev_out1 = MG_SCRATCH_MEM; + ldv $v07, 8, 24, $t0 ## L:278 | 56 | m3:sint = load(address, 0x18).xyzwxyzw; + lbu $t1, %lo(FOG_MASK + 0) ## L:361 | 57 | u8 fog_mask = load(FOG_MASK); + ldv $v11, 8, 16, $at ## L:341 | 58 | vmn2 = load(MATRICES_NORMAL, 0x10).xyzwxyzw; + ldv $v04, 8, 40, $t0 ## L:275 | 59 | m1:ufract = load(address, 0x28).xyzwxyzw; + ldv $v09, 0, 0, $at ## L:339 | 60 | vmn0 = load(MATRICES_NORMAL, 0x00).xyzwxyzw; + ldv $v03, 8, 8, $t0 ## L:274 | 61 | m1:sint = load(address, 0x08).xyzwxyzw; + ldv $v09, 8, 0, $at ## L:339 | 62 | vmn0 = load(MATRICES_NORMAL, 0x00).xyzwxyzw; + sll $t9, $t8, 1 ## L:323 | 63 | vtx_size2 = vtx_size * 2; + ori $at, $zero, %lo(MG_NORMAL_MASK) ## L:343 | 64 | vec16 vnormmask = load(MG_NORMAL_MASK,0x0).xyzwxyzw; + ldv $v12, 0, 0, $at ## L:343 | 65 | vec16 vnormmask = load(MG_NORMAL_MASK,0x0).xyzwxyzw; + ldv $v13, 0, 8, $at ## L:344 | 66 | vec16 vnormfactor = load(MG_NORMAL_MASK,0x8).xyzwxyzw; + ldv $v13, 8, 8, $at ## L:344 | 67 | vec16 vnormfactor = load(MG_NORMAL_MASK,0x8).xyzwxyzw; + ldv $v12, 8, 0, $at ## L:343 | 68 | vec16 vnormmask = load(MG_NORMAL_MASK,0x0).xyzwxyzw; + ori $at, $zero, %lo(MG_VIEWPORT) ## L:347 | 69 | vec16 vviewscale = load(MG_VIEWPORT, 0).xyzwxyzw; + ldv $v15, 0, 8, $at ## L:348 | 70 | vec16 vviewoff = load(MG_VIEWPORT, 8).xyzwxyzw; + ldv $v14, 0, 0, $at ## L:347 | 71 | vec16 vviewscale = load(MG_VIEWPORT, 0).xyzwxyzw; + ldv $v15, 8, 8, $at ## L:348 | 72 | vec16 vviewoff = load(MG_VIEWPORT, 8).xyzwxyzw; + LOAD_NORMAL379: lsv $v20, 8, 0, $t2 ## L:379 | 73 | @AttrLoader("NORMAL") vnorm.X = load(vtx_in1_ptr).x; + ldv $v14, 8, 0, $at ## L:347 | 74 | vec16 vviewscale = load(MG_VIEWPORT, 0).xyzwxyzw; + ori $at, $zero, %lo(TEXTURING_SCALE) ## L:351 | 75 | vtexscale.xy = load(TEXTURING_SCALE).xy; + llv $v16, 0, 0, $at ## L:351 | 76 | vtexscale.xy = load(TEXTURING_SCALE).xy; + llv $v16, 8, 0, $at ## L:352 | 77 | vtexscale.XY = load(TEXTURING_SCALE).xy; + ori $at, $zero, %lo(TEXTURING_OFFSET) ## L:353 | 78 | vtexoffset.xy = load(TEXTURING_OFFSET).xy; + LOAD_POSITION381: ldv $v19, 8, 0, $t2 ## L:381 | 79 | @AttrLoader("POSITION") vpos.XYZW = load(vtx_in1_ptr).xyzw; + lbu $t0, %lo(LIGHTING_HAS_POINTS + 0) ## L:356 | 80 | u8 has_points = load(LIGHTING_HAS_POINTS); + LOAD_COLOR383: llv $v23, 4, 0, $t2 ## L:383 | 81 | @AttrLoader("COLOR") vrgba_in.zw = load(vtx_in1_ptr).xy; + llv $v17, 0, 0, $at ## L:353 | 82 | vtexoffset.xy = load(TEXTURING_OFFSET).xy; + llv $v17, 8, 0, $at ## L:354 | 83 | vtexoffset.XY = load(TEXTURING_OFFSET).xy; + ori $at, $zero, %lo(FOG_PARAMS) ## L:360 | 84 | vfog_parms = load(FOG_PARAMS).xyzwxyzw; + ldv $v18, 0, 0, $at ## L:360 | 85 | vfog_parms = load(FOG_PARAMS).xyzwxyzw; + ldv $v18, 8, 0, $at ## L:360 | 86 | vfog_parms = load(FOG_PARAMS).xyzwxyzw; + addiu $t3, $s7, 40 ## L:364 | 87 | u16 vtx_out1_ptr = vtx_out0_ptr + 40; + VERTEX_LOOP: + vand $v20, $v12, $v20.h0 ## L:417 | ^ | vnorm = vnormmask & vnorm.xxxxXXXX; + ori $at, $zero, %lo(RSPQ_SCRATCH_MEM) ## L:405 | 88 | @Barrier("unpack_col") store(vrgba_in.xyzw, RSPQ_SCRATCH_MEM); ## Barrier: 0x100 + sdv $v23, 0, 0, $at ## L:405 | 89 | @Barrier("unpack_col") store(vrgba_in.xyzw, RSPQ_SCRATCH_MEM); ## Barrier: 0x100 + suv $v22, 4, 8, $t5 ## L:400 | 90 | @Barrier("st_tex1") store_vec_u8(vrgba.X, prev_out1, 8); ## Barrier: 0x80 + suv $v22, 0, 8, $t4 ## L:399 | 91 | @Barrier("st_tex0") store_vec_u8(vrgba.x, prev_out0, 8); ## Barrier: 0x40 + sdv $v24, 0, 0, $t4 ## L:391 | 92 | @Barrier("st_c0") @Barrier("st_tr0") store(vpos_clip:sint.xyzw, prev_out0, 0); ## Barrier: 0xC + vmudn $v20, $v20, $v13.v ## L:418 | ^ | vnorm *= vnormfactor; + luv $v23, 0, 0, $at ## L:406 | 93 | @Barrier("unpack_col") vrgba_in = load_vec_u8(RSPQ_SCRATCH_MEM); ## Barrier: 0x100 + vmudn $v29, $v02, $v19.h0 ## L:421 | ^ | VTEMP = vmvp0 * vpos.xxxxXXXX; + vmadh $v29, $v01, $v19.h0 ## L:421 | 94 | VTEMP = vmvp0 * vpos.xxxxXXXX; + ori $at, $zero, %lo(LIGHTING_AMBIENT) ## L:430 | ^ | vrgba = load(LIGHTING_AMBIENT).xyzwxyzw; + ldv $v22, 0, 0, $at ## L:430 | 95 | vrgba = load(LIGHTING_AMBIENT).xyzwxyzw; + vmadn $v29, $v06, $v19.h2 ## L:422 | ^ | VTEMP = vmvp2 +* vpos.zzzzZZZZ; + ldv $v22, 8, 0, $at ## L:430 | 96 | vrgba = load(LIGHTING_AMBIENT).xyzwxyzw; + vmadh $v29, $v05, $v19.h2 ## L:422 | ^ | VTEMP = vmvp2 +* vpos.zzzzZZZZ; + vmadn $v29, $v04, $v19.h1 ## L:423 | 97 | VTEMP = vmvp1 +* vpos.yyyyYYYY; + ori $at, $zero, %lo(MG_CLIP_FACTORS) ## L:449 | ^ | vec16 vclip_factors = load(MG_CLIP_FACTORS).xyzwxyzw; + vmadh $v29, $v03, $v19.h1 ## L:423 | 98 | VTEMP = vmvp1 +* vpos.yyyyYYYY; + ldv $v26, 0, 0, $at ## L:449 | ^ | vec16 vclip_factors = load(MG_CLIP_FACTORS).xyzwxyzw; + vmadn $v25, $v08, $v30.e7 ## L:424 | 99 | vpos_clip = vmvp3 +* 1; + sdv $v24, 8, 0, $t5 ## L:392 | ^ | @Barrier("st_c1") @Barrier("st_tr1") store(vpos_clip:sint.XYZW, prev_out1, 0); ## Barrier: 0x30 + vmadh $v24, $v07, $v30.e7 ## L:424 | 100 | vpos_clip = vmvp3 +* 1; + ldv $v26, 8, 0, $at ## L:449 | ^ | vec16 vclip_factors = load(MG_CLIP_FACTORS).xyzwxyzw; + slv $v21, 8, 12, $t5 ## L:403 | 101 | @Barrier("st_tex1") store(vtex.XY, prev_out1, 12); ## Barrier: 0x80 + vmulf $v29, $v09, $v20.h0 ## L:426 | ^ | VTEMP = vmn0:sfract * vnorm:sfract.xxxxXXXX; + lbu $sp, %lo(LIGHTING_COUNT + 0) ## L:431 | 102 | u8 light_count = load(LIGHTING_COUNT); + vmacf $v29, $v10, $v20.h1 ## L:427 | ^ | VTEMP = vmn1:sfract +* vnorm:sfract.yyyyYYYY; + vmacf $v20, $v11, $v20.h2 ## L:428 | 103 | vnorm = vmn2:sfract +* vnorm:sfract.zzzzZZZZ; + slv $v21, 0, 12, $t4 ## L:402 | ^ | @Barrier("st_tex0") store(vtex.xy, prev_out0, 12); ## Barrier: 0x40 + LOAD_TEXCOORD410: llv $v21, 0, 0, $s4 ## L:410 | 104 | @AttrLoader("TEXCOORD") vtex.xy = load(vtx_in0_ptr).xy; ## Barrier: 0x7E00 + vmudn $v28, $v25, $v26.v ## L:450 | ^ | vec32 vguard = vpos_clip * vclip_factors; + sb $k1, 7($t5) ## L:397 | 105 | @Barrier("st_tr1") store(tr1, prev_out1, 7); ## Barrier: 0x20 + vmadh $v27, $v24, $v26.v ## L:450 | ^ | vec32 vguard = vpos_clip * vclip_factors; + LOAD_TEXCOORD413: llv $v21, 8, 0, $t2 ## L:413 | 106 | @AttrLoader("TEXCOORD") vtex.XY = load(vtx_in1_ptr).xy; ## Barrier: 0x7E00 + sdv $v24, 0, 24, $s7 ## L:459 | 107 | @Barrier("st_cp0") store(vpos_clip.xyzw, vtx_out0_ptr, 24); ## Barrier: 0x200 + sdv $v25, 0, 32, $s7 ## L:459 | *109 | @Barrier("st_cp0") store(vpos_clip.xyzw, vtx_out0_ptr, 24); ## Barrier: 0x200 + vch $v29, $v27, $v27.h3 ## L:451 | ^ | u16 clip_codes = clip(vguard, vguard.wwwwWWWW); + sb $t6, 6($t4) ## L:394 | 110 | @Barrier("st_c0") store(c0, prev_out0, 6); ## Barrier: 0x4 + vcl $v29, $v28, $v28.h3 ## L:451 | ^ | u16 clip_codes = clip(vguard, vguard.wwwwWWWW); + cfc2 $fp, $vcc ## L:451 | 111 | u16 clip_codes = clip(vguard, vguard.wwwwWWWW); + vch $v29, $v24, $v24.h3 ## L:455 | ^ | u16 tr_codes = clip(vpos_clip, vpos_clip.wwwwWWWW); + vcl $v29, $v25, $v25.h3 ## L:455 | 112 | u16 tr_codes = clip(vpos_clip, vpos_clip.wwwwWWWW); + sdv $v24, 8, 24, $t3 ## L:460 | ^ | @Barrier("st_cp1") store(vpos_clip.XYZW, vtx_out1_ptr, 24); ## Barrier: 0x400 + vaddc $v27, $v25, $v18.h1 ## L:464 | 113 | vec16 vtmp:ufract = vpos_clip:ufract + vfog_parms:ufract.yyyyYYYY; + vadd $v26, $v24, $v18.h0 ## L:465 | 114 | vfog:sint = vpos_clip:sint + vfog_parms:sint.xxxxXXXX; + andi $s1, $fp, 1799 ## L:238 | ^ | t0 = codes & 0x707; + srl $s2, $s1, 5 ## L:239 | 115 | t1 = t0 >> 5; + srl $s0, $fp, 4 ## L:258 | *117 | u16 codes2 = codes >> 4; + vmudn $v29, $v27, $v18.h2 ## L:467 | ^ | VTEMP = vtmp:ufract * vfog_parms:sint.zzzzZZZZ; + vmadh $v26, $v26, $v18.h2 ## L:468 | 118 | vfog = vfog:sint +* vfog_parms:sint.zzzzZZZZ; + sb $k0, 7($t4) ## L:396 | ^ | @Barrier("st_tr0") store(tr0, prev_out0, 7); ## Barrier: 0x8 + or $t6, $s1, $s2 ## L:246 | 119 | result = t0 | t1; + andi $s1, $s0, 1799 ## L:238 | 120 | t0 = codes & 0x707; + cfc2 $s0, $vcc ## L:455 | 121 | u16 tr_codes = clip(vpos_clip, vpos_clip.wwwwWWWW); + sdv $v25, 8, 32, $t3 ## L:460 | 122 | @Barrier("st_cp1") store(vpos_clip.XYZW, vtx_out1_ptr, 24); ## Barrier: 0x400 + vge $v26, $v00, $v26.h2 ## L:469 | ^ | vfog = max(VZERO, vfog.zzzzZZZZ); + srl $s2, $s1, 5 ## L:239 | 123 | t1 = t0 >> 5; + vmudl $v25, $v25, $v14.h3 ## L:477 | ^ | vpos_clip *= vviewscale:sfract.wwwwWWWW; + vmadm $v24, $v24, $v14.h3 ## L:477 | 124 | vpos_clip *= vviewscale:sfract.wwwwWWWW; + sb $t7, 6($t5) ## L:395 | ^ | @Barrier("st_c1") store(c1, prev_out1, 6); ## Barrier: 0x10 + or $t7, $s1, $s2 ## L:246 | 125 | result = t0 | t1; + vmadn $v25, $v00, $v00 ## L:477 | ^ | vpos_clip *= vviewscale:sfract.wwwwWWWW; + andi $s2, $s0, 1799 ## L:238 | 126 | t0 = codes & 0x707; + srl $s1, $s0, 4 ## L:265 | 127 | u16 codes2 = codes >> 4; + srl $s3, $s2, 5 ## L:239 | 128 | t1 = t0 >> 5; + vrcph $v27.e3, $v24.e3 ## L:484 | ^ | vinvw.w = invert_half(vpos_clip).w; + vrcpl $v28.e3, $v25.e3 ## L:484 | 129 | vinvw.w = invert_half(vpos_clip).w; + ssv $v24, 14, 16, $t3 ## L:481 | ^ | @Barrier("st_w1") store(vpos_clip.W, vtx_out1_ptr, 16); ## Barrier: 0x1000 + ssv $v25, 14, 18, $t3 ## L:481 | 130 | @Barrier("st_w1") store(vpos_clip.W, vtx_out1_ptr, 16); ## Barrier: 0x1000 + vrcph $v27.e3, $v24.e7 ## L:485 | ^ | vinvw.W = invert_half(vpos_clip).W; + vrcpl $v28.e7, $v25.e7 ## L:485 | 131 | vinvw.W = invert_half(vpos_clip).W; + nor $k0, $s2, $s3 ## L:253 | ^ | result = t0 ~| t1; + andi $s2, $s1, 1799 ## L:238 | 132 | t0 = codes & 0x707; + vrcph $v27.e7, $v00.e7 ## L:485 | ^ | vinvw.W = invert_half(vpos_clip).W; + ssv $v24, 6, 16, $s7 ## L:480 | 133 | @Barrier("st_w0") store(vpos_clip.w, vtx_out0_ptr, 16); ## Barrier: 0x800 + ssv $v25, 6, 18, $s7 ## L:480 | 134 | @Barrier("st_w0") store(vpos_clip.w, vtx_out0_ptr, 16); ## Barrier: 0x800 + srl $s3, $s2, 5 ## L:239 | 135 | t1 = t0 >> 5; + vmudl $v29, $v25, $v28.h3 ## L:490 | ^ | vpos_clip *= vinvw.wwwwWWWW; + vmadm $v29, $v24, $v28.h3 ## L:490 | 136 | vpos_clip *= vinvw.wwwwWWWW; + ssv $v27, 14, 20, $t3 ## L:488 | ^ | @Barrier("st_iw1") store(vinvw.W, vtx_out1_ptr, 20); ## Barrier: 0x4000 + vmadn $v25, $v25, $v27.h3 ## L:490 | 137 | vpos_clip *= vinvw.wwwwWWWW; + nor $k1, $s2, $s3 ## L:253 | ^ | result = t0 ~| t1; + vmadh $v24, $v24, $v27.h3 ## L:490 | 138 | vpos_clip *= vinvw.wwwwWWWW; + ssv $v27, 6, 20, $s7 ## L:487 | ^ | @Barrier("st_iw0") store(vinvw.w, vtx_out0_ptr, 20); ## Barrier: 0x2000 + ssv $v28, 6, 22, $s7 ## L:487 | 139 | @Barrier("st_iw0") store(vinvw.w, vtx_out0_ptr, 20); ## Barrier: 0x2000 + ssv $v28, 14, 22, $t3 ## L:488 | 140 | @Barrier("st_iw1") store(vinvw.W, vtx_out1_ptr, 20); ## Barrier: 0x4000 + vmudh $v29, $v15, $v30.e7 ## L:494 | ^ | VTEMP = vviewoff:sint * 1; + vmadn $v25, $v25, $v14.v ## L:495 | 141 | vpos_clip = vpos_clip +* vviewscale; + beq $t0, $zero, LABEL_mgfx_0001 ## L:503 | ^ | if (has_points != 0) + vmadh $v24, $v24, $v14.v ## L:495 | *143 | vpos_clip = vpos_clip +* vviewscale; + ori $fp, $zero, %lo(MATRICES_MV) ## L:506 | 144 | u16 addr = MATRICES_MV; + ldv $v03, 0, 8, $fp ## L:274 | 145 | m1:sint = load(address, 0x08).xyzwxyzw; + ldv $v02, 0, 32, $fp ## L:273 | 146 | m0:ufract = load(address, 0x20).xyzwxyzw; + ldv $v01, 0, 0, $fp ## L:272 | 147 | m0:sint = load(address, 0x00).xyzwxyzw; + ldv $v01, 8, 0, $fp ## L:272 | 148 | m0:sint = load(address, 0x00).xyzwxyzw; + ldv $v08, 0, 56, $fp ## L:279 | 149 | m3:ufract = load(address, 0x38).xyzwxyzw; + ldv $v03, 8, 8, $fp ## L:274 | 150 | m1:sint = load(address, 0x08).xyzwxyzw; + ldv $v04, 0, 40, $fp ## L:275 | 151 | m1:ufract = load(address, 0x28).xyzwxyzw; + ldv $v02, 8, 32, $fp ## L:273 | 152 | m0:ufract = load(address, 0x20).xyzwxyzw; + ldv $v04, 8, 40, $fp ## L:275 | 153 | m1:ufract = load(address, 0x28).xyzwxyzw; + ldv $v05, 0, 16, $fp ## L:276 | 154 | m2:sint = load(address, 0x10).xyzwxyzw; + ldv $v06, 0, 48, $fp ## L:277 | 155 | m2:ufract = load(address, 0x30).xyzwxyzw; + ldv $v06, 8, 48, $fp ## L:277 | 156 | m2:ufract = load(address, 0x30).xyzwxyzw; + vmudn $v29, $v02, $v19.h0 ## L:509 | ^ | VTEMP = vmvp0 * vpos.xxxxXXXX; + vmadh $v29, $v01, $v19.h0 ## L:509 | 157 | VTEMP = vmvp0 * vpos.xxxxXXXX; + ldv $v08, 8, 56, $fp ## L:279 | ^ | m3:ufract = load(address, 0x38).xyzwxyzw; + vmadn $v29, $v04, $v19.h1 ## L:510 | 158 | VTEMP = vmvp1 +* vpos.yyyyYYYY; + ldv $v05, 8, 16, $fp ## L:276 | ^ | m2:sint = load(address, 0x10).xyzwxyzw; + ldv $v07, 0, 24, $fp ## L:278 | 159 | m3:sint = load(address, 0x18).xyzwxyzw; + vmadh $v29, $v03, $v19.h1 ## L:510 | ^ | VTEMP = vmvp1 +* vpos.yyyyYYYY; + ldv $v07, 8, 24, $fp ## L:278 | 160 | m3:sint = load(address, 0x18).xyzwxyzw; + vmadn $v29, $v06, $v19.h2 ## L:511 | ^ | VTEMP = vmvp2 +* vpos.zzzzZZZZ; + vmadh $v29, $v05, $v19.h2 ## L:511 | *162 | VTEMP = vmvp2 +* vpos.zzzzZZZZ; + vmadn $v29, $v08, $v30.e7 ## L:512 | 163 | vpos = vmvp3 +* 1; + vmadh $v19, $v07, $v30.e7 ## L:512 | 164 | vpos = vmvp3 +* 1; + LABEL_mgfx_0001: + beq $sp, $zero, LIGHT_END ## L:516 | ^ | if (light_count == 0) goto LIGHT_END; + ori $fp, $zero, %lo(LIGHTING_LIGHTS) ## L:515 | *166 | u16 light_ptr = LIGHTING_LIGHTS; + LIGHT_LOOP: + ldv $v28, 0, 0, $fp ## L:520 | 167 | vec16 vlpos = load(light_ptr, 0).xyzwxyzw; + lh $s0, 6($fp) ## L:522 | 168 | s16 light_w = load(light_ptr, 6); + ldv $v28, 8, 0, $fp ## L:520 | 169 | vec16 vlpos = load(light_ptr, 0).xyzwxyzw; + ldv $v27, 0, 8, $fp ## L:519 | 170 | vec16 vlcol = load(light_ptr, 8).xyzwxyzw; + beq $s0, $zero, LABEL_mgfx_0003 ## L:523 | 171 | if (light_w != 0) + ldv $v27, 8, 8, $fp ## L:519 | *173 | vec16 vlcol = load(light_ptr, 8).xyzwxyzw; + ori $s1, $zero, %lo(MATRICES_MVP) ## L:551 | 174 | u16 addr = MATRICES_MVP; + vsubc $v28, $v28, $v19.v ## L:531 | ^ | vlpos -= vpos; + ldv $v06, 8, 48, $s1 ## L:277 | ***178 | m2:ufract = load(address, 0x30).xyzwxyzw; + vmudh $v02, $v28, $v28.v ## L:532 | ^ | vec32 vsqdist = vlpos * vlpos; + vsar $v02, COP2_ACC_MD ## L:532 | 179 | vec32 vsqdist = vlpos * vlpos; + vsar $v01, COP2_ACC_HI ## L:532 | 180 | vec32 vsqdist = vlpos * vlpos; + vaddc $v04, $v02, $v02.h1 ## L:533 | **183 | vec32 vtmp = vsqdist + vsqdist.yyyyYYYY; + vadd $v03, $v01, $v01.h1 ## L:533 | 184 | vec32 vtmp = vsqdist + vsqdist.yyyyYYYY; + vaddc $v02, $v04, $v02.h2 ## L:534 | **187 | vsqdist = vtmp + vsqdist.zzzzZZZZ; + vadd $v01, $v03, $v01.h2 ## L:534 | 188 | vsqdist = vtmp + vsqdist.zzzzZZZZ; + ldv $v07, 8, 24, $s1 ## L:278 | ^ | m3:sint = load(address, 0x18).xyzwxyzw; + ldv $v07, 0, 24, $s1 ## L:278 | 189 | m3:sint = load(address, 0x18).xyzwxyzw; + ldv $v08, 0, 56, $s1 ## L:279 | **192 | m3:ufract = load(address, 0x38).xyzwxyzw; + vrsqh $v03.e0, $v01.e0 ## L:536 | ^ | vtmp.x = invert_half_sqrt(vsqdist).x; + vrsql $v04.e0, $v02.e0 ## L:536 | 193 | vtmp.x = invert_half_sqrt(vsqdist).x; + vrsqh $v03.e0, $v01.e4 ## L:537 | 194 | vtmp.X = invert_half_sqrt(vsqdist).X; + ldv $v08, 8, 56, $s1 ## L:279 | ^ | m3:ufract = load(address, 0x38).xyzwxyzw; + ldv $v01, 8, 0, $s1 ## L:272 | 195 | m0:sint = load(address, 0x00).xyzwxyzw; + vrsql $v04.e4, $v02.e4 ## L:537 | ^ | vtmp.X = invert_half_sqrt(vsqdist).X; + vrsqh $v03.e4, $v00.e0 ## L:537 | 196 | vtmp.X = invert_half_sqrt(vsqdist).X; + ldv $v02, 0, 32, $s1 ## L:273 | ^ | m0:ufract = load(address, 0x20).xyzwxyzw; + vmudm $v29, $v28, $v04.h0 ## L:539 | **199 | vlpos *= vtmp.xxxxXXXX; + ldv $v02, 8, 32, $s1 ## L:273 | ^ | m0:ufract = load(address, 0x20).xyzwxyzw; + vmadh $v28, $v28, $v03.h0 ## L:539 | 200 | vlpos *= vtmp.xxxxXXXX; + ldv $v01, 0, 0, $s1 ## L:272 | ^ | m0:sint = load(address, 0x00).xyzwxyzw; + ldv $v06, 0, 48, $s1 ## L:277 | 201 | m2:ufract = load(address, 0x30).xyzwxyzw; + vmudn $v29, $v04, $v27.h3 ## L:540 | ^ | vec16 vatten = vtmp * vlcol.wwwwWWWW; + ldv $v04, 8, 40, $s1 ## L:275 | 202 | m1:ufract = load(address, 0x28).xyzwxyzw; + vmadh $v05, $v03, $v27.h3 ## L:540 | ^ | vec16 vatten = vtmp * vlcol.wwwwWWWW; + ldv $v03, 0, 8, $s1 ## L:274 | 203 | m1:sint = load(address, 0x08).xyzwxyzw; + ldv $v03, 8, 8, $s1 ## L:274 | 204 | m1:sint = load(address, 0x08).xyzwxyzw; + ldv $v04, 0, 40, $s1 ## L:275 | *206 | m1:ufract = load(address, 0x28).xyzwxyzw; + vmulf $v05, $v05, $v05.v ## L:541 | ^ | vatten:sfract *= vatten:sfract; + vmulf $v27, $v27, $v05.h0 ## L:542 | ***210 | vlcol:sfract *= vatten:sfract.xxxxXXXX; + ldv $v05, 0, 16, $s1 ## L:276 | ^ | m2:sint = load(address, 0x10).xyzwxyzw; + ldv $v05, 8, 16, $s1 ## L:276 | 211 | m2:sint = load(address, 0x10).xyzwxyzw; + LABEL_mgfx_0003: + vmulf $v28, $v28, $v20.v ## L:556 | ^ | vlpos *= vnorm:sfract; + addiu $fp, $fp, 16 ## L:564 | ***215 | light_ptr += 16; + vmulf $v29, $v27, $v28.h0 ## L:557 | ^ | VTEMP = vlcol:sfract * vlpos:sfract.xxxxXXXX; + addiu $sp, $sp, 65535 ## L:565 | 216 | light_count -= 1; + vmacf $v29, $v27, $v28.h1 ## L:558 | ^ | VTEMP = vlcol:sfract +* vlpos:sfract.yyyyYYYY; + vmacf $v27, $v27, $v28.h2 ## L:559 | 217 | vlcol = vlcol:sfract +* vlpos:sfract.zzzzZZZZ; + vge $v27, $v27, $v00 ## L:560 | ***221 | vlcol = max(vlcol, VZERO); + bne $sp, $zero, LIGHT_LOOP ## L:566 | ^ | if (light_count != 0) goto LIGHT_LOOP; + vadd $v22, $v22, $v27.v ## L:562 | ****226 | vrgba:sint += vlcol; + LIGHT_END: + addu $s4, $s4, $t9 ## L:638 | 227 | vtx_in0_ptr += vtx_size2; + vmov $v22.e3, $v12.e3 ## L:570 | ^ | vrgba.w = vnormmask.w; + LOAD_NORMAL643: lsv $v20, 0, 0, $s4 ## L:643 | 228 | @AttrLoader("NORMAL") vnorm.x = load(vtx_in0_ptr).x; + vmov $v22.e7, $v12.e3 ## L:571 | ^ | vrgba.W = vnormmask.w; + sltu $at, $s4, $s6 ## L:650 | 229 | if (vtx_in0_ptr < vtx_in_end) goto VERTEX_LOOP; + or $t5, $zero, $t3 ## L:637 | 230 | prev_out1 = vtx_out1_ptr; + ctc2 $t1, $vcc ## L:574 | 231 | set_vcc(fog_mask); + or $t4, $zero, $s7 ## L:636 | 232 | prev_out0 = vtx_out0_ptr; + addu $t2, $t2, $t9 ## L:639 | 233 | vtx_in1_ptr += vtx_size2; + PATCH_COLOR572: vmulf $v22, $v22, $v23.v ## L:572 | ^ | @AttrPatch("COLOR:vnop") vrgba:sfract *= vrgba_in:sfract; + LOAD_NORMAL644: lsv $v20, 8, 0, $t2 ## L:644 | 234 | @AttrLoader("NORMAL") vnorm.X = load(vtx_in1_ptr).x; + addiu $s7, $s7, 80 ## L:640 | 235 | vtx_out0_ptr += 80; + LOAD_POSITION645: ldv $v19, 0, 0, $s4 ## L:645 | 236 | @AttrLoader("POSITION") vpos.xyzw = load(vtx_in0_ptr).xyzw; + LOAD_COLOR648: llv $v23, 4, 0, $t2 ## L:648 | 237 | @AttrLoader("COLOR") vrgba_in.zw = load(vtx_in1_ptr).xy; + vmrg $v22, $v22, $v26.h2 ## L:575 | ^ | vrgba = select(vrgba, vfog.zzzzZZZZ); + LOAD_POSITION646: ldv $v19, 8, 0, $t2 ## L:646 | 238 | @AttrLoader("POSITION") vpos.XYZW = load(vtx_in1_ptr).xyzw; + vmudh $v29, $v17, $v30.e7 ## L:633 | ^ | VTEMP = vtexoffset:sint * 1; + LOAD_COLOR647: llv $v23, 0, 0, $s4 ## L:647 | 239 | @AttrLoader("COLOR") vrgba_in.xy = load(vtx_in0_ptr).xy; + vmadh $v21, $v21, $v16.v ## L:634 | ^ | vtex:sint = vtex:sint +* vtexscale; + bne $at, $zero, VERTEX_LOOP ## L:650 | 240 | if (vtx_in0_ptr < vtx_in_end) goto VERTEX_LOOP; + addiu $t3, $t3, 80 ## L:641 | *242 | vtx_out1_ptr += 80; + sdv $v24, 0, 0, $t4 ## L:656 | 243 | @Barrier("st_c0") @Barrier("st_tr0") store(vpos_clip:sint.xyzw, prev_out0, 0); ## Barrier: 0xC + sdv $v24, 8, 0, $t5 ## L:657 | 244 | @Barrier("st_c1") @Barrier("st_tr1") store(vpos_clip:sint.XYZW, prev_out1, 0); ## Barrier: 0x30 + suv $v22, 0, 8, $t4 ## L:664 | 245 | @Barrier("st_tex0") store_vec_u8(vrgba.x, prev_out0, 8); ## Barrier: 0x40 + sb $t7, 6($t5) ## L:660 | 246 | @Barrier("st_c1") store(c1, prev_out1, 6); ## Barrier: 0x10 + sb $k1, 7($t5) ## L:662 | 247 | @Barrier("st_tr1") store(tr1, prev_out1, 7); ## Barrier: 0x20 + sb $k0, 7($t4) ## L:661 | 248 | @Barrier("st_tr0") store(tr0, prev_out0, 7); ## Barrier: 0x8 + suv $v22, 4, 8, $t5 ## L:665 | 249 | @Barrier("st_tex1") store_vec_u8(vrgba.X, prev_out1, 8); ## Barrier: 0x80 + slv $v21, 0, 12, $t4 ## L:667 | 250 | @Barrier("st_tex0") store(vtex.xy, prev_out0, 12); ## Barrier: 0x40 + sb $t6, 6($t4) ## L:659 | 251 | @Barrier("st_c0") store(c0, prev_out0, 6); ## Barrier: 0x4 + j RSPQ_Loop ## L:669 | 252 | } + slv $v21, 8, 12, $t5 ## L:668 | *254 | @Barrier("st_tex1") store(vtex.XY, prev_out1, 12); ## Barrier: 0x80 + +MgEndShader + +#define zero $0 +#define v0 $2 +#define v1 $3 +#define a0 $4 +#define a1 $5 +#define a2 $6 +#define a3 $7 +#define t0 $8 +#define t1 $9 +#define t2 $10 +#define t3 $11 +#define t4 $12 +#define t5 $13 +#define t6 $14 +#define t7 $15 +#define s0 $16 +#define s1 $17 +#define s2 $18 +#define s3 $19 +#define s4 $20 +#define s5 $21 +#define s6 $22 +#define s7 $23 +#define t8 $24 +#define t9 $25 +#define k0 $26 +#define k1 $27 +#define gp $28 +#define sp $29 +#define fp $30 +#define ra $31 + +.set at +.set macro \ No newline at end of file diff --git a/src/magma/rsp_mgfx.rspl b/src/magma/rsp_mgfx.rspl new file mode 100644 index 000000000..6bf882000 --- /dev/null +++ b/src/magma/rsp_mgfx.rspl @@ -0,0 +1,506 @@ +/* + * rsp_mgfx.rspl + * + * Authors: Dennis Heinze + * + */ + +include "rsp_magma.inc" +#include "../../include/magma_constants.h" +#include "../../include/mgfx_constants.h" + +#define SCRATCH_TEXSCALE 0x80 + +//#define TRACE_VTX_LOOP 1 + +state +{ + extern u16 RSPQ_SCRATCH_MEM; + extern u16 MG_SCRATCH_MEM; + + extern vec16 MG_NORMAL_MASK[8]; + + extern u16 MG_VERTEX_CACHE; + extern u16 MG_VERTEX_CACHE_END; + + extern vec16 MG_VIEWPORT[8]; + extern vec16 MG_CLIP_FACTORS[4]; + extern vec16 MG_VERTEX_SIZE[4]; + extern u32 MG_VERTEX_BUFFER; +} + +uniform MATRICES +{ + vec16 MATRICES_MVP[4]; + vec16 MATRICES_MV[4]; + vec16 MATRICES_NORMAL[2]; +} + +uniform TEXTURING +{ + s16 TEXTURING_SCALE[2]; + s16 TEXTURING_OFFSET[2]; +} + +uniform LIGHTING +{ + u8 LIGHTING_LIGHTS[MGFX_LIGHT_SIZE][MGFX_LIGHT_COUNT_MAX]; + s16 LIGHTING_AMBIENT[3]; + u8 LIGHTING_HAS_POINTS; + u8 LIGHTING_COUNT; +} + +uniform FOG +{ + s16 FOG_PARAMS[3]; + u8 FOG_MASK; +} + +attribute s16 POSITION[3]; +attribute s16 NORMAL?; +attribute u32 COLOR?; +attribute s16 TEXCOORD[2]?; + +macro __pack_clip_codes(u16 codes, u16 t0, u16 t1) +{ + t0 = codes & 0x707; + t1 = t0 >> 5; +} + +macro pack_clip_codes(u8 result, u16 codes) +{ + u16 t0, t1; + __pack_clip_codes(codes, t0, t1); + result = t0 | t1; +} + +macro pack_clip_codes_inv(u8 result, u16 codes) +{ + u16 t0, t1; + __pack_clip_codes(codes, t0, t1); + result = t0 ~| t1; +} + +macro pack_clip_codes2(u8 result0, u8 result1, u16 codes) +{ + u16 codes2 = codes >> 4; + pack_clip_codes(result0, codes); + pack_clip_codes(result1, codes2); +} + +macro pack_clip_codes2_inv(u8 result0, u8 result1, u16 codes) +{ + u16 codes2 = codes >> 4; + pack_clip_codes_inv(result0, codes); + pack_clip_codes_inv(result1, codes2); +} + +macro load_mat4x4(vec32 m0, vec32 m1, vec32 m2, vec32 m3, u16 address) +{ + m0:sint = load(address, 0x00).xyzwxyzw; + m0:ufract = load(address, 0x20).xyzwxyzw; + m1:sint = load(address, 0x08).xyzwxyzw; + m1:ufract = load(address, 0x28).xyzwxyzw; + m2:sint = load(address, 0x10).xyzwxyzw; + m2:ufract = load(address, 0x30).xyzwxyzw; + m3:sint = load(address, 0x18).xyzwxyzw; + m3:ufract = load(address, 0x38).xyzwxyzw; +} + +shader mgfx() +{ + u16<$t8> vtx_size; + u16<$t9> vtx_size2; + u16<$s4> vtx_in0_ptr; + u16<$s6> vtx_in_end; + u16<$s7> vtx_out0_ptr; + + { + u32 inputs_ptr = get_cmd_address(0x2); + vec16 vvtx_size = load(MG_VERTEX_SIZE).xyzw; + vec16 vinputs = load(inputs_ptr).xyzw; + + vec32 vproducts = vinputs * vvtx_size; + @Barrier("inputs0") store(vproducts:ufract.xyzw, RSPQ_SCRATCH_MEM); + @Barrier("inputs1") store(vproducts.x, RSPQ_SCRATCH_MEM, 0x8); + } + + { + s16<$t0> dma_size; + + @Barrier("inputs0") vtx_out0_ptr = load(RSPQ_SCRATCH_MEM, 0x2); + u16 vertices_size; + @Barrier("inputs0") vertices_size = load(RSPQ_SCRATCH_MEM, 0x4); + u32 buffer_offset; + @Barrier("inputs1") buffer_offset = load(RSPQ_SCRATCH_MEM, 0x8); + + u32<$s0> vtx_buffer = load(MG_VERTEX_BUFFER); + vtx_buffer += buffer_offset; + + u32 misalignment = buffer_offset & 0x7; + dma_size = vertices_size + misalignment; + dma_size += 0x7; + dma_size &= 0xFF8; + + vtx_in0_ptr = MG_VERTEX_CACHE_END; + vtx_in0_ptr -= dma_size; + + dma_in(vtx_in0_ptr, vtx_buffer, dma_size); + + vtx_size = load(MG_VERTEX_SIZE); + vtx_size2 = vtx_size * 2; + + vtx_in_end = vtx_in0_ptr + vertices_size; + vtx_out0_ptr += MG_VERTEX_CACHE; + } + + vec32<$v01> vmvp0; + vec32<$v03> vmvp1; + vec32<$v05> vmvp2; + vec32<$v07> vmvp3; + { + u16 mvpaddr = MATRICES_MVP; + load_mat4x4(vmvp0, vmvp1, vmvp2, vmvp3, mvpaddr); + } + + vec16 vmn0, vmn1, vmn2; + vmn0 = load(MATRICES_NORMAL, 0x00).xyzwxyzw; + vmn1 = load(MATRICES_NORMAL, 0x08).xyzwxyzw; + vmn2 = load(MATRICES_NORMAL, 0x10).xyzwxyzw; + + vec16 vnormmask = load(MG_NORMAL_MASK,0x0).xyzwxyzw; + vec16 vnormfactor = load(MG_NORMAL_MASK,0x8).xyzwxyzw; + + vec16 vtexscale; + vtexscale.xy = load(TEXTURING_SCALE).xy; + vtexscale.XY = load(TEXTURING_SCALE).xy; + + #ifdef ENABLE_ENV_MAP + { + vec32 vtexscale2 = vtexscale >> 3; + store(vtexscale2, MG_SCRATCH_MEM, SCRATCH_TEXSCALE); + } + undef vtexscale; + #else + vec16 vviewscale = load(MG_VIEWPORT, 0).xyzwxyzw; + vec16 vviewoff = load(MG_VIEWPORT, 8).xyzwxyzw; + + vec16 vtexoffset; + vtexoffset.xy = load(TEXTURING_OFFSET).xy; + vtexoffset.XY = load(TEXTURING_OFFSET).xy; + + u8 has_points = load(LIGHTING_HAS_POINTS); + #endif + + vec16 vfog_parms; + vfog_parms = load(FOG_PARAMS).xyzwxyzw; + u8 fog_mask = load(FOG_MASK); + + u16 vtx_in1_ptr = vtx_in0_ptr + vtx_size; + u16 vtx_out1_ptr = vtx_out0_ptr + MG_VTX_SIZE; + + u16 prev_out0 = MG_SCRATCH_MEM; + u16 prev_out1 = MG_SCRATCH_MEM; + + u8 c0, c1, tr0, tr1; + + vec16 vpos; + vec16 vnorm; + vec16 vtex; + vec16 vrgba; + vec16 vrgba_in; + vec32 vpos_clip; + + @AttrLoader("NORMAL") vnorm.x = load(vtx_in0_ptr).x; + @AttrLoader("NORMAL") vnorm.X = load(vtx_in1_ptr).x; + @AttrLoader("POSITION") vpos.xyzw = load(vtx_in0_ptr).xyzw; + @AttrLoader("POSITION") vpos.XYZW = load(vtx_in1_ptr).xyzw; + @AttrLoader("COLOR") vrgba_in.xy = load(vtx_in0_ptr).xy; + @AttrLoader("COLOR") vrgba_in.zw = load(vtx_in1_ptr).xy; + + #ifdef TRACE_VTX_LOOP + asm("emux_trace_start"); + #endif + { + VERTEX_LOOP: + + @Barrier("st_c0") @Barrier("st_tr0") store(vpos_clip:sint.xyzw, prev_out0, MG_VTX_XYZ); + @Barrier("st_c1") @Barrier("st_tr1") store(vpos_clip:sint.XYZW, prev_out1, MG_VTX_XYZ); + + @Barrier("st_c0") store(c0, prev_out0, MG_VTX_CLIP_CODE); + @Barrier("st_c1") store(c1, prev_out1, MG_VTX_CLIP_CODE); + @Barrier("st_tr0") store(tr0, prev_out0, MG_VTX_TR_CODE); + @Barrier("st_tr1") store(tr1, prev_out1, MG_VTX_TR_CODE); + + @Barrier("st_tex0") store_vec_u8(vrgba.x, prev_out0, MG_VTX_RGBA); + @Barrier("st_tex1") store_vec_u8(vrgba.X, prev_out1, MG_VTX_RGBA); + + @Barrier("st_tex0") store(vtex.xy, prev_out0, MG_VTX_ST); + @Barrier("st_tex1") store(vtex.XY, prev_out1, MG_VTX_ST); + + @Barrier("unpack_col") store(vrgba_in.xyzw, RSPQ_SCRATCH_MEM); + @Barrier("unpack_col") vrgba_in = load_vec_u8(RSPQ_SCRATCH_MEM); + + #ifndef ENABLE_ENV_MAP + @Barrier("st_cp0") @Barrier("st_cp1") @Barrier("st_w0") @Barrier("st_w1") @Barrier("st_iw0") @Barrier("st_iw1") + @AttrLoader("TEXCOORD") vtex.xy = load(vtx_in0_ptr).xy; + + @Barrier("st_cp0") @Barrier("st_cp1") @Barrier("st_w0") @Barrier("st_w1") @Barrier("st_iw0") @Barrier("st_iw1") + @AttrLoader("TEXCOORD") vtex.XY = load(vtx_in1_ptr).xy; + #endif + + // View space normals + vnorm = vnormmask & vnorm.xxxxXXXX; + vnorm *= vnormfactor; + + // Clip space position + VTEMP = vmvp0 * vpos.xxxxXXXX; + VTEMP = vmvp2 +* vpos.zzzzZZZZ; + VTEMP = vmvp1 +* vpos.yyyyYYYY; + vpos_clip = vmvp3 +* 1; + + VTEMP = vmn0:sfract * vnorm:sfract.xxxxXXXX; + VTEMP = vmn1:sfract +* vnorm:sfract.yyyyYYYY; + vnorm = vmn2:sfract +* vnorm:sfract.zzzzZZZZ; + + vrgba = load(LIGHTING_AMBIENT).xyzwxyzw; + u8 light_count = load(LIGHTING_COUNT); + + #ifdef ENABLE_NORMALIZE + { + vec32 vsqdist = vnorm * vnorm; + vec32 vtmp = vsqdist + vsqdist.yyyyYYYY; + vsqdist = vtmp + vsqdist.zzzzZZZZ; + + vec32 vinvdist; + vinvdist.x = invert_half_sqrt(vsqdist).x; + vinvdist.X = invert_half_sqrt(vsqdist).X; + + vnorm *= vinvdist.xxxxXXXX; + } + #endif + + { // Clip codes + + vec16 vclip_factors = load(MG_CLIP_FACTORS).xyzwxyzw; + vec32 vguard = vpos_clip * vclip_factors; + u16 clip_codes = clip(vguard, vguard.wwwwWWWW); + pack_clip_codes2(c0, c1, clip_codes); + + // Trivial reject codes + u16 tr_codes = clip(vpos_clip, vpos_clip.wwwwWWWW); + pack_clip_codes2_inv(tr0, tr1, tr_codes); + } + + @Barrier("st_cp0") store(vpos_clip.xyzw, vtx_out0_ptr, MG_VTX_CS_POSi); + @Barrier("st_cp1") store(vpos_clip.XYZW, vtx_out1_ptr, MG_VTX_CS_POSi); + + vec16 vfog; + { // Fog + vec16 vtmp:ufract = vpos_clip:ufract + vfog_parms:ufract.yyyyYYYY; + vfog:sint = vpos_clip:sint + vfog_parms:sint.xxxxXXXX; + + VTEMP = vtmp:ufract * vfog_parms:sint.zzzzZZZZ; + vfog = vfog:sint +* vfog_parms:sint.zzzzZZZZ; + vfog = max(VZERO, vfog.zzzzZZZZ); + } + + #ifdef ENABLE_ENV_MAP + vec16 vviewscale = load(MG_VIEWPORT, 0).xyzwxyzw; + vec16 vviewoff = load(MG_VIEWPORT, 8).xyzwxyzw; + #endif + + vpos_clip *= vviewscale:sfract.wwwwWWWW; + + // Screen space position + normalized W + @Barrier("st_w0") store(vpos_clip.w, vtx_out0_ptr, MG_VTX_Wi); + @Barrier("st_w1") store(vpos_clip.W, vtx_out1_ptr, MG_VTX_Wi); + + vec32 vinvw; + vinvw.w = invert_half(vpos_clip).w; + vinvw.W = invert_half(vpos_clip).W; + + @Barrier("st_iw0") store(vinvw.w, vtx_out0_ptr, MG_VTX_INVWi); + @Barrier("st_iw1") store(vinvw.W, vtx_out1_ptr, MG_VTX_INVWi); + + vpos_clip *= vinvw.wwwwWWWW; + + undef vinvw; + + VTEMP = vviewoff:sint * 1; + vpos_clip = vpos_clip +* vviewscale; + + #ifdef ENABLE_ENV_MAP + undef vviewscale; + undef vviewoff; + #endif + + #ifndef ENABLE_ENV_MAP + if (has_points != 0) + #endif + { + u16 addr = MATRICES_MV; + load_mat4x4(vmvp0, vmvp1, vmvp2, vmvp3, addr); + + VTEMP = vmvp0 * vpos.xxxxXXXX; + VTEMP = vmvp1 +* vpos.yyyyYYYY; + VTEMP = vmvp2 +* vpos.zzzzZZZZ; + vpos = vmvp3 +* 1; + } + + u16 light_ptr = LIGHTING_LIGHTS; + if (light_count == 0) goto LIGHT_END; + { + LIGHT_LOOP: + vec16 vlcol = load(light_ptr, MGFX_LIGHT_COLOR).xyzwxyzw; + vec16 vlpos = load(light_ptr, MGFX_LIGHT_POSITION).xyzwxyzw; + + s16 light_w = load(light_ptr, MGFX_LIGHT_POSITIONW); + if (light_w != 0) + { + undef vmvp0; + undef vmvp1; + undef vmvp2; + undef vmvp3; + + { + vlpos -= vpos; + vec32 vsqdist = vlpos * vlpos; + vec32 vtmp = vsqdist + vsqdist.yyyyYYYY; + vsqdist = vtmp + vsqdist.zzzzZZZZ; + + vtmp.x = invert_half_sqrt(vsqdist).x; + vtmp.X = invert_half_sqrt(vsqdist).X; + + vlpos *= vtmp.xxxxXXXX; + vec16 vatten = vtmp * vlcol.wwwwWWWW; + vatten:sfract *= vatten:sfract; + vlcol:sfract *= vatten:sfract.xxxxXXXX; + } + + vec32<$v01> vmvp0; + vec32<$v03> vmvp1; + vec32<$v05> vmvp2; + vec32<$v07> vmvp3; + + #ifndef ENABLE_ENV_MAP + u16 addr = MATRICES_MVP; + load_mat4x4(vmvp0, vmvp1, vmvp2, vmvp3, addr); + #endif + } + + vlpos *= vnorm:sfract; + VTEMP = vlcol:sfract * vlpos:sfract.xxxxXXXX; + VTEMP = vlcol:sfract +* vlpos:sfract.yyyyYYYY; + vlcol = vlcol:sfract +* vlpos:sfract.zzzzZZZZ; + vlcol = max(vlcol, VZERO); + + vrgba:sint += vlcol; // Cast to sint to force saturated addition + + light_ptr += MGFX_LIGHT_SIZE; + light_count -= 1; + if (light_count != 0) goto LIGHT_LOOP; + } + + LIGHT_END: + vrgba.w = vnormmask.w; + vrgba.W = vnormmask.w; + @AttrPatch("COLOR:vnop") vrgba:sfract *= vrgba_in:sfract; + + set_vcc(fog_mask); + vrgba = select(vrgba, vfog.zzzzZZZZ); + undef vfog; + + #ifdef ENABLE_ENV_MAP + + u16 addr = MATRICES_MVP; + load_mat4x4(vmvp0, vmvp1, vmvp2, vmvp3, addr); + + { // Environment mapping + vec32 vsqdist = vpos * vpos; + vec32 vtmp = vsqdist + vsqdist.yyyyYYYY; + vsqdist = vtmp + vsqdist.zzzzZZZZ; + + vtmp.x = invert_half_sqrt(vsqdist).x; + vtmp.X = invert_half_sqrt(vsqdist).X; + + vpos *= vtmp.xxxxXXXX; + undef vtmp; + + vec16 vdot = vpos:sfract * vnorm:sfract; + vec16 vtmp:sint = vdot + vdot.yyyyYYYY; + vdot:sint = vtmp + vdot.zzzzZZZZ; + undef vtmp; + vdot = min(vdot, VZERO); + vdot = VZERO - vdot; + + vec16 vzunit = VZERO; + vzunit.z = vnormmask.w; + vzunit.Z = vnormmask.w; + + VTEMP = vnorm:sfract * vdot.xxxxXXXX; + VTEMP = vnorm:sfract +* vdot.xxxxXXXX; + VTEMP = vpos:sint +* 1; + VTEMP = vzunit:sint +* 1; + undef vzunit; + undef vdot; + vec32 vrefl = get_acc(); + + vsqdist = vrefl * vrefl; + vec32 vtmp = vsqdist + vsqdist.yyyyYYYY; + vsqdist = vtmp + vsqdist.zzzzZZZZ; + + vtmp.x = invert_half_sqrt(vsqdist).x; + vtmp.X = invert_half_sqrt(vsqdist).X; + + vtex = 0x80; + VTEMP = vtex:sint * 1; + vtex:sint = vrefl +* vtmp.xxxxXXXX; + } + + vec32 vtexscale; + vec16 vtexoffset; + vtexscale = load(MG_SCRATCH_MEM, SCRATCH_TEXSCALE); + vtexoffset.xy = load(TEXTURING_OFFSET).xy; + vtexoffset.XY = load(TEXTURING_OFFSET).xy; + #endif + + // Texture coords + VTEMP = vtexoffset:sint * 1; + vtex:sint = vtex:sint +* vtexscale; + + prev_out0 = vtx_out0_ptr; + prev_out1 = vtx_out1_ptr; + vtx_in0_ptr += vtx_size2; + vtx_in1_ptr += vtx_size2; + vtx_out0_ptr += MG_VTX_SIZE2; + vtx_out1_ptr += MG_VTX_SIZE2; + + @AttrLoader("NORMAL") vnorm.x = load(vtx_in0_ptr).x; + @AttrLoader("NORMAL") vnorm.X = load(vtx_in1_ptr).x; + @AttrLoader("POSITION") vpos.xyzw = load(vtx_in0_ptr).xyzw; + @AttrLoader("POSITION") vpos.XYZW = load(vtx_in1_ptr).xyzw; + @AttrLoader("COLOR") vrgba_in.xy = load(vtx_in0_ptr).xy; + @AttrLoader("COLOR") vrgba_in.zw = load(vtx_in1_ptr).xy; + + if (vtx_in0_ptr < vtx_in_end) goto VERTEX_LOOP; + + #ifdef TRACE_VTX_LOOP + asm("emux_trace_stop"); + #endif + + @Barrier("st_c0") @Barrier("st_tr0") store(vpos_clip:sint.xyzw, prev_out0, MG_VTX_XYZ); + @Barrier("st_c1") @Barrier("st_tr1") store(vpos_clip:sint.XYZW, prev_out1, MG_VTX_XYZ); + + @Barrier("st_c0") store(c0, prev_out0, MG_VTX_CLIP_CODE); + @Barrier("st_c1") store(c1, prev_out1, MG_VTX_CLIP_CODE); + @Barrier("st_tr0") store(tr0, prev_out0, MG_VTX_TR_CODE); + @Barrier("st_tr1") store(tr1, prev_out1, MG_VTX_TR_CODE); + + @Barrier("st_tex0") store_vec_u8(vrgba.x, prev_out0, MG_VTX_RGBA); + @Barrier("st_tex1") store_vec_u8(vrgba.X, prev_out1, MG_VTX_RGBA); + + @Barrier("st_tex0") store(vtex.xy, prev_out0, MG_VTX_ST); + @Barrier("st_tex1") store(vtex.XY, prev_out1, MG_VTX_ST); + } +} diff --git a/src/magma/rsp_mgfx_env.S b/src/magma/rsp_mgfx_env.S new file mode 100644 index 000000000..f98e82cb3 --- /dev/null +++ b/src/magma/rsp_mgfx_env.S @@ -0,0 +1,548 @@ +## Auto-generated file, transpiled with RSPL +#define ENABLE_ENV_MAP +#define __MAGMA_CONSTANTS +#define MG_VERTEX_CACHE_COUNT 48 +#define MG_DEFAULT_GUARD_BAND 2 +#define MG_INLINE_UNIFORM_HEADER 12 +#define MG_MAX_UNIFORM_PAYLOAD_SIZE (RSPQ_MAX_COMMAND_SIZE*4 - 12) +#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 +#define __MGFX_CONSTANTS +#define MGFX_ATTRIBUTE_POSITION 0 +#define MGFX_ATTRIBUTE_NORMAL 1 +#define MGFX_ATTRIBUTE_COLOR 2 +#define MGFX_ATTRIBUTE_TEXCOORD 3 +#define MGFX_BINDING_MATRICES 0 +#define MGFX_BINDING_TEXTURING 1 +#define MGFX_BINDING_LIGHTING 2 +#define MGFX_BINDING_FOG 3 +#define MGFX_LIGHT_COUNT_MAX 8 +#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 +#define SCRATCH_TEXSCALE 0x80 +#include + +.set noreorder +.set noat +.set nomacro + +#undef zero +#undef at +#undef v0 +#undef v1 +#undef a0 +#undef a1 +#undef a2 +#undef a3 +#undef t0 +#undef t1 +#undef t2 +#undef t3 +#undef t4 +#undef t5 +#undef t6 +#undef t7 +#undef s0 +#undef s1 +#undef s2 +#undef s3 +#undef s4 +#undef s5 +#undef s6 +#undef s7 +#undef t8 +#undef t9 +#undef k0 +#undef k1 +#undef gp +#undef sp +#undef fp +#undef ra +.equ hex.$zero, 0 +.equ hex.$at, 1 +.equ hex.$v0, 2 +.equ hex.$v1, 3 +.equ hex.$a0, 4 +.equ hex.$a1, 5 +.equ hex.$a2, 6 +.equ hex.$a3, 7 +.equ hex.$t0, 8 +.equ hex.$t1, 9 +.equ hex.$t2, 10 +.equ hex.$t3, 11 +.equ hex.$t4, 12 +.equ hex.$t5, 13 +.equ hex.$t6, 14 +.equ hex.$t7, 15 +.equ hex.$s0, 16 +.equ hex.$s1, 17 +.equ hex.$s2, 18 +.equ hex.$s3, 19 +.equ hex.$s4, 20 +.equ hex.$s5, 21 +.equ hex.$s6, 22 +.equ hex.$s7, 23 +.equ hex.$t8, 24 +.equ hex.$t9, 25 +.equ hex.$k0, 26 +.equ hex.$k1, 27 +.equ hex.$gp, 28 +.equ hex.$sp, 29 +.equ hex.$fp, 30 +.equ hex.$ra, 31 +#define vco 0 +#define vcc 1 +#define vce 2 + +MgBeginShaderUniforms + MgBeginUniform MATRICES, 0 + .align 4 + MATRICES_MVP: .ds.b 64 + .align 4 + MATRICES_MV: .ds.b 64 + .align 4 + MATRICES_NORMAL: .ds.b 32 + MgEndUniform + + MgBeginUniform TEXTURING, 1 + .align 1 + TEXTURING_SCALE: .ds.b 4 + .align 1 + TEXTURING_OFFSET: .ds.b 4 + MgEndUniform + + MgBeginUniform LIGHTING, 2 + LIGHTING_LIGHTS: .ds.b 128 + .align 1 + LIGHTING_AMBIENT: .ds.b 6 + LIGHTING_HAS_POINTS: .ds.b 1 + LIGHTING_COUNT: .ds.b 1 + MgEndUniform + + MgBeginUniform FOG, 3 + .align 1 + FOG_PARAMS: .ds.b 6 + FOG_MASK: .ds.b 1 + MgEndUniform + +MgEndShaderUniforms + +MgBeginVertexInput + MgBeginVertexAttribute 0, 0 + MgVertexAttributeLoaders LOAD_POSITION389, LOAD_POSITION390, LOAD_POSITION655, LOAD_POSITION654 + MgEndVertexAttribute + + MgBeginVertexAttribute 1, 1 + MgVertexAttributeLoaders LOAD_NORMAL387, LOAD_NORMAL388, LOAD_NORMAL652, LOAD_NORMAL653 + MgEndVertexAttribute + + MgBeginVertexAttribute 2, 1 + MgVertexAttributeLoaders LOAD_COLOR391, LOAD_COLOR392, LOAD_COLOR657, LOAD_COLOR656 + MgBeginVertexAttributePatch PATCH_COLOR581 + vnop + MgEndVertexAttributePatch + MgEndVertexAttribute + + MgBeginVertexAttribute 3, 1 + MgEndVertexAttribute + +MgEndVertexInput + +MgBeginShader + ori $at, $zero, %lo(MG_VERTEX_SIZE) ## L:293 | ^ | vec16 vvtx_size = load(MG_VERTEX_SIZE).xyzw; + addiu $t0, $gp, %lo(RSPQ_DMEM_BUFFER) -6 ## L:292 | 2 | u32 inputs_ptr = get_cmd_address(0x2); + ldv $v01, 0, 0, $at ## L:293 | 3 | vec16 vvtx_size = load(MG_VERTEX_SIZE).xyzw; + ldv $v02, 0, 0, $t0 ## L:294 | 4 | vec16 vinputs = load(inputs_ptr).xyzw; + vmudh $v04, $v02, $v01.v ## L:296 | ***8 | vec32 vproducts = vinputs * vvtx_size; + ori $s4, $zero, %lo(MG_VERTEX_CACHE_END) ## L:318 | ^ | vtx_in0_ptr = MG_VERTEX_CACHE_END; + ori $at, $zero, %lo(RSPQ_SCRATCH_MEM) ## L:297 | 9 | @Barrier("inputs0") store(vproducts:ufract.xyzw, RSPQ_SCRATCH_MEM); ## Barrier: 0x1 + vsar $v04, COP2_ACC_MD ## L:296 | ^ | vec32 vproducts = vinputs * vvtx_size; + vsar $v03, COP2_ACC_HI ## L:296 | 10 | vec32 vproducts = vinputs * vvtx_size; + lw $s0, %lo(MG_VERTEX_BUFFER + 0) ## L:310 | ^ | u32<$s0> vtx_buffer = load(MG_VERTEX_BUFFER); + sdv $v04, 0, 0, $at ## L:297 | **13 | @Barrier("inputs0") store(vproducts:ufract.xyzw, RSPQ_SCRATCH_MEM); ## Barrier: 0x1 + ssv $v03, 0, 8, $at ## L:298 | 14 | @Barrier("inputs1") store(vproducts.x, RSPQ_SCRATCH_MEM, 0x8); ## Barrier: 0x2 + ssv $v04, 0, 10, $at ## L:298 | 15 | @Barrier("inputs1") store(vproducts.x, RSPQ_SCRATCH_MEM, 0x8); ## Barrier: 0x2 + lw $t2, %lo(RSPQ_SCRATCH_MEM + 8) ## L:308 | 16 | @Barrier("inputs1") buffer_offset = load(RSPQ_SCRATCH_MEM, 0x8); ## Barrier: 0x2 + lhu $s7, %lo(RSPQ_SCRATCH_MEM + 2) ## L:304 | 17 | @Barrier("inputs0") vtx_out0_ptr = load(RSPQ_SCRATCH_MEM, 0x2); ## Barrier: 0x1 + lhu $t1, %lo(RSPQ_SCRATCH_MEM + 4) ## L:306 | 18 | @Barrier("inputs0") vertices_size = load(RSPQ_SCRATCH_MEM, 0x4); ## Barrier: 0x1 + addu $s0, $s0, $t2 ## L:311 | 19 | vtx_buffer += buffer_offset; + andi $t3, $t2, 7 ## L:313 | 20 | u32 misalignment = buffer_offset & 0x7; + addu $t0, $t1, $t3 ## L:314 | 21 | dma_size = vertices_size + misalignment; + addiu $t0, $t0, 7 ## L:315 | 22 | dma_size += 0x7; + addiu $t2, $zero, 12 ## L:321 | 23 | dma_in(vtx_in0_ptr, vtx_buffer, dma_size); + andi $t0, $t0, 4088 ## L:316 | 24 | dma_size &= 0xFF8; + subu $s4, $s4, $t0 ## L:319 | 25 | vtx_in0_ptr -= dma_size; + jal DMAExec ## L:321 | 26 | dma_in(vtx_in0_ptr, vtx_buffer, dma_size); ## Args: $t0, $t1, $s0, $s4, $t2 + addiu $t0, $t0, -1 ## L:321 | *28 | dma_in(vtx_in0_ptr, vtx_buffer, dma_size); + ori $at, $zero, %lo(MATRICES_NORMAL) ## L:340 | 29 | vmn0 = load(MATRICES_NORMAL, 0x00).xyzwxyzw; + ldv $v10, 0, 8, $at ## L:341 | 30 | vmn1 = load(MATRICES_NORMAL, 0x08).xyzwxyzw; + ldv $v09, 0, 0, $at ## L:340 | 31 | vmn0 = load(MATRICES_NORMAL, 0x00).xyzwxyzw; + ldv $v11, 0, 16, $at ## L:342 | 32 | vmn2 = load(MATRICES_NORMAL, 0x10).xyzwxyzw; + ldv $v10, 8, 8, $at ## L:341 | 33 | vmn1 = load(MATRICES_NORMAL, 0x08).xyzwxyzw; + ldv $v11, 8, 16, $at ## L:342 | 34 | vmn2 = load(MATRICES_NORMAL, 0x10).xyzwxyzw; + ldv $v09, 8, 0, $at ## L:340 | 35 | vmn0 = load(MATRICES_NORMAL, 0x00).xyzwxyzw; + ori $at, $zero, %lo(MG_NORMAL_MASK) ## L:344 | 36 | vec16 vnormmask = load(MG_NORMAL_MASK,0x0).xyzwxyzw; + ldv $v12, 0, 0, $at ## L:344 | 37 | vec16 vnormmask = load(MG_NORMAL_MASK,0x0).xyzwxyzw; + ldv $v12, 8, 0, $at ## L:344 | 38 | vec16 vnormmask = load(MG_NORMAL_MASK,0x0).xyzwxyzw; + ori $t0, $zero, %lo(MATRICES_MVP) ## L:335 | 39 | u16 mvpaddr = MATRICES_MVP; + ldv $v03, 0, 8, $t0 ## L:275 | 40 | m1:sint = load(address, 0x08).xyzwxyzw; + ldv $v13, 0, 8, $at ## L:345 | 41 | vec16 vnormfactor = load(MG_NORMAL_MASK,0x8).xyzwxyzw; + ldv $v13, 8, 8, $at ## L:345 | 42 | vec16 vnormfactor = load(MG_NORMAL_MASK,0x8).xyzwxyzw; + ori $at, $zero, %lo(TEXTURING_SCALE) ## L:348 | 43 | vtexscale.xy = load(TEXTURING_SCALE).xy; + ldv $v07, 0, 24, $t0 ## L:279 | 44 | m3:sint = load(address, 0x18).xyzwxyzw; + ldv $v08, 0, 56, $t0 ## L:280 | 45 | m3:ufract = load(address, 0x38).xyzwxyzw; + ori $t4, $zero, %lo(MG_SCRATCH_MEM) ## L:376 | 46 | u16 prev_out1 = MG_SCRATCH_MEM; + llv $v14, 0, 0, $at ## L:348 | 47 | vtexscale.xy = load(TEXTURING_SCALE).xy; + llv $v14, 8, 0, $at ## L:349 | 48 | vtexscale.XY = load(TEXTURING_SCALE).xy; + ldv $v04, 0, 40, $t0 ## L:276 | 49 | m1:ufract = load(address, 0x28).xyzwxyzw; + ldv $v04, 8, 40, $t0 ## L:276 | 50 | m1:ufract = load(address, 0x28).xyzwxyzw; + lhu $t8, %lo(MG_VERTEX_SIZE + 0) ## L:323 | 51 | vtx_size = load(MG_VERTEX_SIZE); + addiu $s7, $s7, %lo(MG_VERTEX_CACHE) ## L:327 | 52 | vtx_out0_ptr += MG_VERTEX_CACHE; + ldv $v05, 0, 16, $t0 ## L:277 | 53 | m2:sint = load(address, 0x10).xyzwxyzw; + ori $at, $zero, %lo(MG_SCRATCH_MEM) ## L:354 | 54 | store(vtexscale2, MG_SCRATCH_MEM, 0x80); + vmudm $v15, $v14, $v31.e2 ## L:353 | ^ | vec32 vtexscale2 = vtexscale >> 3; + vmadn $v16, $v00, $v00 ## L:353 | 55 | vec32 vtexscale2 = vtexscale >> 3; + ldv $v07, 8, 24, $t0 ## L:279 | ^ | m3:sint = load(address, 0x18).xyzwxyzw; + ldv $v06, 0, 48, $t0 ## L:278 | 56 | m2:ufract = load(address, 0x30).xyzwxyzw; + ldv $v02, 0, 32, $t0 ## L:274 | 57 | m0:ufract = load(address, 0x20).xyzwxyzw; + ldv $v03, 8, 8, $t0 ## L:275 | 58 | m1:sint = load(address, 0x08).xyzwxyzw; + ldv $v01, 0, 0, $t0 ## L:273 | 59 | m0:sint = load(address, 0x00).xyzwxyzw; + ldv $v02, 8, 32, $t0 ## L:274 | 60 | m0:ufract = load(address, 0x20).xyzwxyzw; + ldv $v06, 8, 48, $t0 ## L:278 | 61 | m2:ufract = load(address, 0x30).xyzwxyzw; + addu $s6, $s4, $t1 ## L:326 | 62 | vtx_in_end = vtx_in0_ptr + vertices_size; + ldv $v01, 8, 0, $t0 ## L:273 | 63 | m0:sint = load(address, 0x00).xyzwxyzw; + sqv $v16, 0, 144, $at ## L:354 | 64 | store(vtexscale2, MG_SCRATCH_MEM, 0x80); + LOAD_NORMAL387: lsv $v16, 0, 0, $s4 ## L:387 | 65 | @AttrLoader("NORMAL") vnorm.x = load(vtx_in0_ptr).x; + sqv $v15, 0, 128, $at ## L:354 | 66 | store(vtexscale2, MG_SCRATCH_MEM, 0x80); + addu $t1, $s4, $t8 ## L:372 | 67 | u16 vtx_in1_ptr = vtx_in0_ptr + vtx_size; + ori $at, $zero, %lo(FOG_PARAMS) ## L:369 | 68 | vfog_parms = load(FOG_PARAMS).xyzwxyzw; + ldv $v05, 8, 16, $t0 ## L:277 | 69 | m2:sint = load(address, 0x10).xyzwxyzw; + sll $t9, $t8, 1 ## L:324 | 70 | vtx_size2 = vtx_size * 2; + LOAD_POSITION389: ldv $v15, 0, 0, $s4 ## L:389 | 71 | @AttrLoader("POSITION") vpos.xyzw = load(vtx_in0_ptr).xyzw; + LOAD_NORMAL388: lsv $v16, 8, 0, $t1 ## L:388 | 72 | @AttrLoader("NORMAL") vnorm.X = load(vtx_in1_ptr).x; + ldv $v14, 0, 0, $at ## L:369 | 73 | vfog_parms = load(FOG_PARAMS).xyzwxyzw; + ldv $v08, 8, 56, $t0 ## L:280 | 74 | m3:ufract = load(address, 0x38).xyzwxyzw; + LOAD_COLOR391: llv $v19, 0, 0, $s4 ## L:391 | 75 | @AttrLoader("COLOR") vrgba_in.xy = load(vtx_in0_ptr).xy; + ldv $v14, 8, 0, $at ## L:369 | 76 | vfog_parms = load(FOG_PARAMS).xyzwxyzw; + lbu $t0, %lo(FOG_MASK + 0) ## L:370 | 77 | u8 fog_mask = load(FOG_MASK); + LOAD_COLOR392: llv $v19, 4, 0, $t1 ## L:392 | 78 | @AttrLoader("COLOR") vrgba_in.zw = load(vtx_in1_ptr).xy; + ori $t3, $zero, %lo(MG_SCRATCH_MEM) ## L:375 | 79 | u16 prev_out0 = MG_SCRATCH_MEM; + addiu $t2, $s7, 40 ## L:373 | 80 | u16 vtx_out1_ptr = vtx_out0_ptr + 40; + LOAD_POSITION390: ldv $v15, 8, 0, $t1 ## L:390 | 81 | @AttrLoader("POSITION") vpos.XYZW = load(vtx_in1_ptr).xyzw; + VERTEX_LOOP: + vand $v16, $v12, $v16.h0 ## L:426 | ^ | vnorm = vnormmask & vnorm.xxxxXXXX; + ori $at, $zero, %lo(RSPQ_SCRATCH_MEM) ## L:414 | 82 | @Barrier("unpack_col") store(vrgba_in.xyzw, RSPQ_SCRATCH_MEM); ## Barrier: 0x100 + sdv $v20, 8, 0, $t4 ## L:401 | *84 | @Barrier("st_c1") @Barrier("st_tr1") store(vpos_clip:sint.XYZW, prev_out1, 0); ## Barrier: 0x30 + suv $v18, 4, 8, $t4 ## L:409 | 85 | @Barrier("st_tex1") store_vec_u8(vrgba.X, prev_out1, 8); ## Barrier: 0x80 + vmudn $v16, $v16, $v13.v ## L:427 | ^ | vnorm *= vnormfactor; + vmudn $v29, $v02, $v15.h0 ## L:430 | 86 | VTEMP = vmvp0 * vpos.xxxxXXXX; + sdv $v20, 0, 0, $t3 ## L:400 | ^ | @Barrier("st_c0") @Barrier("st_tr0") store(vpos_clip:sint.xyzw, prev_out0, 0); ## Barrier: 0xC + sdv $v19, 0, 0, $at ## L:414 | 87 | @Barrier("unpack_col") store(vrgba_in.xyzw, RSPQ_SCRATCH_MEM); ## Barrier: 0x100 + vmadh $v29, $v01, $v15.h0 ## L:430 | ^ | VTEMP = vmvp0 * vpos.xxxxXXXX; + sb $t7, 7($t3) ## L:405 | 88 | @Barrier("st_tr0") store(tr0, prev_out0, 7); ## Barrier: 0x8 + vmadn $v29, $v06, $v15.h2 ## L:431 | ^ | VTEMP = vmvp2 +* vpos.zzzzZZZZ; + vmadh $v29, $v05, $v15.h2 ## L:431 | 89 | VTEMP = vmvp2 +* vpos.zzzzZZZZ; + suv $v18, 0, 8, $t3 ## L:408 | ^ | @Barrier("st_tex0") store_vec_u8(vrgba.x, prev_out0, 8); ## Barrier: 0x40 + vmadn $v29, $v04, $v15.h1 ## L:432 | 90 | VTEMP = vmvp1 +* vpos.yyyyYYYY; + luv $v19, 0, 0, $at ## L:415 | ^ | @Barrier("unpack_col") vrgba_in = load_vec_u8(RSPQ_SCRATCH_MEM); ## Barrier: 0x100 + ori $at, $zero, %lo(LIGHTING_AMBIENT) ## L:439 | 91 | vrgba = load(LIGHTING_AMBIENT).xyzwxyzw; + vmadh $v29, $v03, $v15.h1 ## L:432 | ^ | VTEMP = vmvp1 +* vpos.yyyyYYYY; + ldv $v18, 0, 0, $at ## L:439 | 92 | vrgba = load(LIGHTING_AMBIENT).xyzwxyzw; + vmadn $v21, $v08, $v30.e7 ## L:433 | ^ | vpos_clip = vmvp3 +* 1; + ldv $v18, 8, 0, $at ## L:439 | 93 | vrgba = load(LIGHTING_AMBIENT).xyzwxyzw; + ori $at, $zero, %lo(MG_CLIP_FACTORS) ## L:458 | 94 | vec16 vclip_factors = load(MG_CLIP_FACTORS).xyzwxyzw; + vmadh $v20, $v07, $v30.e7 ## L:433 | ^ | vpos_clip = vmvp3 +* 1; + ldv $v22, 0, 0, $at ## L:458 | 95 | vec16 vclip_factors = load(MG_CLIP_FACTORS).xyzwxyzw; + ldv $v22, 8, 0, $at ## L:458 | 96 | vec16 vclip_factors = load(MG_CLIP_FACTORS).xyzwxyzw; + vmulf $v29, $v09, $v16.h0 ## L:435 | ^ | VTEMP = vmn0:sfract * vnorm:sfract.xxxxXXXX; + ori $at, $zero, %lo(MG_VIEWPORT) ## L:482 | 97 | vec16 vviewscale = load(MG_VIEWPORT, 0).xyzwxyzw; + vmacf $v29, $v10, $v16.h1 ## L:436 | ^ | VTEMP = vmn1:sfract +* vnorm:sfract.yyyyYYYY; + vmacf $v16, $v11, $v16.h2 ## L:437 | *99 | vnorm = vmn2:sfract +* vnorm:sfract.zzzzZZZZ; + slv $v17, 8, 12, $t4 ## L:412 | ^ | @Barrier("st_tex1") store(vtex.XY, prev_out1, 12); ## Barrier: 0x80 + sb $k0, 7($t4) ## L:406 | 100 | @Barrier("st_tr1") store(tr1, prev_out1, 7); ## Barrier: 0x20 + vmudn $v24, $v21, $v22.v ## L:459 | ^ | vec32 vguard = vpos_clip * vclip_factors; + vmadh $v23, $v20, $v22.v ## L:459 | 101 | vec32 vguard = vpos_clip * vclip_factors; + sb $t6, 6($t4) ## L:404 | ^ | @Barrier("st_c1") store(c1, prev_out1, 6); ## Barrier: 0x10 + sdv $v20, 8, 24, $t2 ## L:469 | 102 | @Barrier("st_cp1") store(vpos_clip.XYZW, vtx_out1_ptr, 24); ## Barrier: 0x400 + slv $v17, 0, 12, $t3 ## L:411 | 103 | @Barrier("st_tex0") store(vtex.xy, prev_out0, 12); ## Barrier: 0x40 + sdv $v21, 8, 32, $t2 ## L:469 | 104 | @Barrier("st_cp1") store(vpos_clip.XYZW, vtx_out1_ptr, 24); ## Barrier: 0x400 + sdv $v20, 0, 24, $s7 ## L:468 | 105 | @Barrier("st_cp0") store(vpos_clip.xyzw, vtx_out0_ptr, 24); ## Barrier: 0x200 + vch $v29, $v23, $v23.h3 ## L:460 | ^ | u16 clip_codes = clip(vguard, vguard.wwwwWWWW); + vcl $v29, $v24, $v24.h3 ## L:460 | 106 | u16 clip_codes = clip(vguard, vguard.wwwwWWWW); + sb $t5, 6($t3) ## L:403 | ^ | @Barrier("st_c0") store(c0, prev_out0, 6); ## Barrier: 0x4 + cfc2 $sp, $vcc ## L:460 | 107 | u16 clip_codes = clip(vguard, vguard.wwwwWWWW); + sdv $v21, 0, 32, $s7 ## L:468 | 108 | @Barrier("st_cp0") store(vpos_clip.xyzw, vtx_out0_ptr, 24); ## Barrier: 0x200 + ldv $v24, 0, 8, $at ## L:483 | 109 | vec16 vviewoff = load(MG_VIEWPORT, 8).xyzwxyzw; + vch $v29, $v20, $v20.h3 ## L:464 | ^ | u16 tr_codes = clip(vpos_clip, vpos_clip.wwwwWWWW); + lbu $k1, %lo(LIGHTING_COUNT + 0) ## L:440 | 110 | u8 light_count = load(LIGHTING_COUNT); + vcl $v29, $v21, $v21.h3 ## L:464 | ^ | u16 tr_codes = clip(vpos_clip, vpos_clip.wwwwWWWW); + andi $s0, $sp, 1799 ## L:239 | 111 | t0 = codes & 0x707; + vaddc $v23, $v21, $v14.h1 ## L:473 | ^ | vec16 vtmp:ufract = vpos_clip:ufract + vfog_parms:ufract.yyyyYYYY; + vadd $v22, $v20, $v14.h0 ## L:474 | 112 | vfog:sint = vpos_clip:sint + vfog_parms:sint.xxxxXXXX; + srl $fp, $sp, 4 ## L:259 | ^ | u16 codes2 = codes >> 4; + srl $s1, $s0, 5 ## L:240 | 113 | t1 = t0 >> 5; + ori $sp, $zero, %lo(MATRICES_MV) ## L:515 | 114 | u16 addr = MATRICES_MV; + ldv $v03, 0, 8, $sp ## L:275 | 115 | m1:sint = load(address, 0x08).xyzwxyzw; + vmudn $v29, $v23, $v14.h2 ## L:476 | ^ | VTEMP = vtmp:ufract * vfog_parms:sint.zzzzZZZZ; + ldv $v23, 0, 0, $at ## L:482 | 116 | vec16 vviewscale = load(MG_VIEWPORT, 0).xyzwxyzw; + vmadh $v22, $v22, $v14.h2 ## L:477 | ^ | vfog = vfog:sint +* vfog_parms:sint.zzzzZZZZ; + or $t5, $s0, $s1 ## L:247 | 117 | result = t0 | t1; + ldv $v23, 8, 0, $at ## L:482 | 118 | vec16 vviewscale = load(MG_VIEWPORT, 0).xyzwxyzw; + andi $s0, $fp, 1799 ## L:239 | 119 | t0 = codes & 0x707; + ldv $v08, 0, 56, $sp ## L:280 | 120 | m3:ufract = load(address, 0x38).xyzwxyzw; + cfc2 $fp, $vcc ## L:464 | 121 | u16 tr_codes = clip(vpos_clip, vpos_clip.wwwwWWWW); + vge $v22, $v00, $v22.h2 ## L:478 | ^ | vfog = max(VZERO, vfog.zzzzZZZZ); + vmudl $v21, $v21, $v23.h3 ## L:486 | 122 | vpos_clip *= vviewscale:sfract.wwwwWWWW; + ldv $v07, 0, 24, $sp ## L:279 | ^ | m3:sint = load(address, 0x18).xyzwxyzw; + vmadm $v20, $v20, $v23.h3 ## L:486 | 123 | vpos_clip *= vviewscale:sfract.wwwwWWWW; + ldv $v04, 0, 40, $sp ## L:276 | ^ | m1:ufract = load(address, 0x28).xyzwxyzw; + ldv $v04, 8, 40, $sp ## L:276 | 124 | m1:ufract = load(address, 0x28).xyzwxyzw; + vmadn $v21, $v00, $v00 ## L:486 | ^ | vpos_clip *= vviewscale:sfract.wwwwWWWW; + ldv $v02, 0, 32, $sp ## L:274 | 125 | m0:ufract = load(address, 0x20).xyzwxyzw; + ldv $v08, 8, 56, $sp ## L:280 | 126 | m3:ufract = load(address, 0x38).xyzwxyzw; + ldv $v24, 8, 8, $at ## L:483 | 127 | vec16 vviewoff = load(MG_VIEWPORT, 8).xyzwxyzw; + vrcph $v25.e3, $v20.e3 ## L:493 | ^ | vinvw.w = invert_half(vpos_clip).w; + ldv $v05, 0, 16, $sp ## L:277 | 128 | m2:sint = load(address, 0x10).xyzwxyzw; + srl $s1, $s0, 5 ## L:240 | 129 | t1 = t0 >> 5; + ldv $v02, 8, 32, $sp ## L:274 | 130 | m0:ufract = load(address, 0x20).xyzwxyzw; + vrcpl $v26.e3, $v21.e3 ## L:493 | ^ | vinvw.w = invert_half(vpos_clip).w; + ssv $v20, 14, 16, $t2 ## L:490 | 131 | @Barrier("st_w1") store(vpos_clip.W, vtx_out1_ptr, 16); ## Barrier: 0x1000 + vrcph $v25.e3, $v20.e7 ## L:494 | ^ | vinvw.W = invert_half(vpos_clip).W; + ldv $v01, 0, 0, $sp ## L:273 | 132 | m0:sint = load(address, 0x00).xyzwxyzw; + vrcpl $v26.e7, $v21.e7 ## L:494 | ^ | vinvw.W = invert_half(vpos_clip).W; + vrcph $v25.e7, $v00.e7 ## L:494 | 133 | vinvw.W = invert_half(vpos_clip).W; + or $t6, $s0, $s1 ## L:247 | ^ | result = t0 | t1; + andi $s1, $fp, 1799 ## L:239 | 134 | t0 = codes & 0x707; + ssv $v20, 6, 16, $s7 ## L:489 | 135 | @Barrier("st_w0") store(vpos_clip.w, vtx_out0_ptr, 16); ## Barrier: 0x800 + ssv $v21, 14, 18, $t2 ## L:490 | 136 | @Barrier("st_w1") store(vpos_clip.W, vtx_out1_ptr, 16); ## Barrier: 0x1000 + ssv $v21, 6, 18, $s7 ## L:489 | 137 | @Barrier("st_w0") store(vpos_clip.w, vtx_out0_ptr, 16); ## Barrier: 0x800 + vmudl $v29, $v21, $v26.h3 ## L:499 | ^ | vpos_clip *= vinvw.wwwwWWWW; + vmadm $v29, $v20, $v26.h3 ## L:499 | 138 | vpos_clip *= vinvw.wwwwWWWW; + srl $s2, $s1, 5 ## L:240 | ^ | t1 = t0 >> 5; + vmadn $v21, $v21, $v25.h3 ## L:499 | 139 | vpos_clip *= vinvw.wwwwWWWW; + ldv $v06, 0, 48, $sp ## L:278 | ^ | m2:ufract = load(address, 0x30).xyzwxyzw; + vmadh $v20, $v20, $v25.h3 ## L:499 | 140 | vpos_clip *= vinvw.wwwwWWWW; + ldv $v07, 8, 24, $sp ## L:279 | ^ | m3:sint = load(address, 0x18).xyzwxyzw; + ldv $v03, 8, 8, $sp ## L:275 | 141 | m1:sint = load(address, 0x08).xyzwxyzw; + vmudh $v29, $v24, $v30.e7 ## L:503 | ^ | VTEMP = vviewoff:sint * 1; + srl $s0, $fp, 4 ## L:266 | 142 | u16 codes2 = codes >> 4; + ldv $v01, 8, 0, $sp ## L:273 | 143 | m0:sint = load(address, 0x00).xyzwxyzw; + vmadn $v21, $v21, $v23.v ## L:504 | ^ | vpos_clip = vpos_clip +* vviewscale; + vmadh $v20, $v20, $v23.v ## L:504 | 144 | vpos_clip = vpos_clip +* vviewscale; + nor $t7, $s1, $s2 ## L:254 | ^ | result = t0 ~| t1; + vmudn $v29, $v02, $v15.h0 ## L:518 | 145 | VTEMP = vmvp0 * vpos.xxxxXXXX; + ldv $v06, 8, 48, $sp ## L:278 | ^ | m2:ufract = load(address, 0x30).xyzwxyzw; + ldv $v05, 8, 16, $sp ## L:277 | 146 | m2:sint = load(address, 0x10).xyzwxyzw; + andi $s1, $s0, 1799 ## L:239 | 147 | t0 = codes & 0x707; + vmadh $v29, $v01, $v15.h0 ## L:518 | ^ | VTEMP = vmvp0 * vpos.xxxxXXXX; + srl $s2, $s1, 5 ## L:240 | 148 | t1 = t0 >> 5; + ssv $v25, 14, 20, $t2 ## L:497 | 149 | @Barrier("st_iw1") store(vinvw.W, vtx_out1_ptr, 20); ## Barrier: 0x4000 + vmadn $v29, $v04, $v15.h1 ## L:519 | ^ | VTEMP = vmvp1 +* vpos.yyyyYYYY; + nor $k0, $s1, $s2 ## L:254 | 150 | result = t0 ~| t1; + vmadh $v29, $v03, $v15.h1 ## L:519 | ^ | VTEMP = vmvp1 +* vpos.yyyyYYYY; + ori $sp, $zero, %lo(LIGHTING_LIGHTS) ## L:524 | 151 | u16 light_ptr = LIGHTING_LIGHTS; + vmadn $v29, $v06, $v15.h2 ## L:520 | ^ | VTEMP = vmvp2 +* vpos.zzzzZZZZ; + vmadh $v29, $v05, $v15.h2 ## L:520 | 152 | VTEMP = vmvp2 +* vpos.zzzzZZZZ; + ssv $v26, 14, 22, $t2 ## L:497 | ^ | @Barrier("st_iw1") store(vinvw.W, vtx_out1_ptr, 20); ## Barrier: 0x4000 + ssv $v25, 6, 20, $s7 ## L:496 | 153 | @Barrier("st_iw0") store(vinvw.w, vtx_out0_ptr, 20); ## Barrier: 0x2000 + vmadn $v29, $v08, $v30.e7 ## L:521 | ^ | vpos = vmvp3 +* 1; + vmadh $v15, $v07, $v30.e7 ## L:521 | 154 | vpos = vmvp3 +* 1; + beq $k1, $zero, LIGHT_END ## L:525 | ^ | if (light_count == 0) goto LIGHT_END; + ssv $v26, 6, 22, $s7 ## L:496 | *156 | @Barrier("st_iw0") store(vinvw.w, vtx_out0_ptr, 20); ## Barrier: 0x2000 + LIGHT_LOOP: + lh $fp, 6($sp) ## L:531 | 157 | s16 light_w = load(light_ptr, 6); + ldv $v24, 0, 0, $sp ## L:529 | 158 | vec16 vlpos = load(light_ptr, 0).xyzwxyzw; + ldv $v24, 8, 0, $sp ## L:529 | 159 | vec16 vlpos = load(light_ptr, 0).xyzwxyzw; + ldv $v23, 0, 8, $sp ## L:528 | 160 | vec16 vlcol = load(light_ptr, 8).xyzwxyzw; + beq $fp, $zero, LABEL_mgfx_0002 ## L:532 | 161 | if (light_w != 0) + ldv $v23, 8, 8, $sp ## L:528 | *163 | vec16 vlcol = load(light_ptr, 8).xyzwxyzw; + vsubc $v24, $v24, $v15.v ## L:540 | 164 | vlpos -= vpos; + vmudh $v02, $v24, $v24.v ## L:541 | ***168 | vec32 vsqdist = vlpos * vlpos; + vsar $v02, COP2_ACC_MD ## L:541 | 169 | vec32 vsqdist = vlpos * vlpos; + vsar $v01, COP2_ACC_HI ## L:541 | 170 | vec32 vsqdist = vlpos * vlpos; + vaddc $v04, $v02, $v02.h1 ## L:542 | **173 | vec32 vtmp = vsqdist + vsqdist.yyyyYYYY; + vadd $v03, $v01, $v01.h1 ## L:542 | 174 | vec32 vtmp = vsqdist + vsqdist.yyyyYYYY; + vaddc $v02, $v04, $v02.h2 ## L:543 | **177 | vsqdist = vtmp + vsqdist.zzzzZZZZ; + vadd $v01, $v03, $v01.h2 ## L:543 | 178 | vsqdist = vtmp + vsqdist.zzzzZZZZ; + vrsqh $v03.e0, $v01.e0 ## L:545 | ***182 | vtmp.x = invert_half_sqrt(vsqdist).x; + vrsql $v04.e0, $v02.e0 ## L:545 | 183 | vtmp.x = invert_half_sqrt(vsqdist).x; + vrsqh $v03.e0, $v01.e4 ## L:546 | 184 | vtmp.X = invert_half_sqrt(vsqdist).X; + vrsql $v04.e4, $v02.e4 ## L:546 | 185 | vtmp.X = invert_half_sqrt(vsqdist).X; + vrsqh $v03.e4, $v00.e0 ## L:546 | 186 | vtmp.X = invert_half_sqrt(vsqdist).X; + vmudm $v29, $v24, $v04.h0 ## L:548 | **189 | vlpos *= vtmp.xxxxXXXX; + vmadh $v24, $v24, $v03.h0 ## L:548 | 190 | vlpos *= vtmp.xxxxXXXX; + vmudn $v29, $v04, $v23.h3 ## L:549 | 191 | vec16 vatten = vtmp * vlcol.wwwwWWWW; + vmadh $v05, $v03, $v23.h3 ## L:549 | 192 | vec16 vatten = vtmp * vlcol.wwwwWWWW; + vmulf $v05, $v05, $v05.v ## L:550 | ***196 | vatten:sfract *= vatten:sfract; + vmulf $v23, $v23, $v05.h0 ## L:551 | ***200 | vlcol:sfract *= vatten:sfract.xxxxXXXX; + LABEL_mgfx_0002: + vmulf $v24, $v24, $v16.v ## L:565 | 201 | vlpos *= vnorm:sfract; + vmulf $v29, $v23, $v24.h0 ## L:566 | ***205 | VTEMP = vlcol:sfract * vlpos:sfract.xxxxXXXX; + vmacf $v29, $v23, $v24.h1 ## L:567 | 206 | VTEMP = vlcol:sfract +* vlpos:sfract.yyyyYYYY; + addiu $sp, $sp, 16 ## L:573 | ^ | light_ptr += 16; + vmacf $v23, $v23, $v24.h2 ## L:568 | 207 | vlcol = vlcol:sfract +* vlpos:sfract.zzzzZZZZ; + addiu $k1, $k1, 65535 ## L:574 | ^ | light_count -= 1; + vge $v23, $v23, $v00 ## L:569 | ***211 | vlcol = max(vlcol, VZERO); + bne $k1, $zero, LIGHT_LOOP ## L:575 | ^ | if (light_count != 0) goto LIGHT_LOOP; + vadd $v18, $v18, $v23.v ## L:571 | ****216 | vrgba:sint += vlcol; + LIGHT_END: + or $t3, $zero, $s7 ## L:645 | 217 | prev_out0 = vtx_out0_ptr; + vmov $v18.e3, $v12.e3 ## L:579 | ^ | vrgba.w = vnormmask.w; + vmov $v18.e7, $v12.e3 ## L:580 | 218 | vrgba.W = vnormmask.w; + addu $s4, $s4, $t9 ## L:647 | ^ | vtx_in0_ptr += vtx_size2; + ori $fp, $zero, %lo(MATRICES_MVP) ## L:589 | 219 | u16 addr = MATRICES_MVP; + ctc2 $t0, $vcc ## L:583 | **222 | set_vcc(fog_mask); + PATCH_COLOR581: vmulf $v18, $v18, $v19.v ## L:581 | ^ | @AttrPatch("COLOR:vnop") vrgba:sfract *= vrgba_in:sfract; + vmrg $v18, $v18, $v22.h2 ## L:584 | ***226 | vrgba = select(vrgba, vfog.zzzzZZZZ); + ldv $v08, 8, 56, $fp ## L:280 | ^ | m3:ufract = load(address, 0x38).xyzwxyzw; + vmudh $v23, $v15, $v15.v ## L:593 | 227 | vec32 vsqdist = vpos * vpos; + addu $t1, $t1, $t9 ## L:648 | ^ | vtx_in1_ptr += vtx_size2; + ori $at, $zero, %lo(MG_SCRATCH_MEM) ## L:636 | 228 | vtexscale = load(MG_SCRATCH_MEM, 0x80); + vsar $v23, COP2_ACC_MD ## L:593 | ^ | vec32 vsqdist = vpos * vpos; + addiu $s7, $s7, 80 ## L:649 | 229 | vtx_out0_ptr += 80; + vsar $v22, COP2_ACC_HI ## L:593 | ^ | vec32 vsqdist = vpos * vpos; + vaddc $v25, $v23, $v23.h1 ## L:594 | **232 | vec32 vtmp = vsqdist + vsqdist.yyyyYYYY; + LOAD_COLOR657: llv $v19, 4, 0, $t1 ## L:657 | ^ | @AttrLoader("COLOR") vrgba_in.zw = load(vtx_in1_ptr).xy; + ldv $v05, 8, 16, $fp ## L:277 | 233 | m2:sint = load(address, 0x10).xyzwxyzw; + vadd $v24, $v22, $v22.h1 ## L:594 | ^ | vec32 vtmp = vsqdist + vsqdist.yyyyYYYY; + ldv $v03, 0, 8, $fp ## L:275 | **236 | m1:sint = load(address, 0x08).xyzwxyzw; + vaddc $v23, $v25, $v23.h2 ## L:595 | ^ | vsqdist = vtmp + vsqdist.zzzzZZZZ; + vadd $v22, $v24, $v22.h2 ## L:595 | 237 | vsqdist = vtmp + vsqdist.zzzzZZZZ; + ldv $v02, 8, 32, $fp ## L:274 | ^ | m0:ufract = load(address, 0x20).xyzwxyzw; + ldv $v07, 0, 24, $fp ## L:279 | ***241 | m3:sint = load(address, 0x18).xyzwxyzw; + vrsqh $v24.e0, $v22.e0 ## L:597 | ^ | vtmp.x = invert_half_sqrt(vsqdist).x; + vrsql $v25.e0, $v23.e0 ## L:597 | 242 | vtmp.x = invert_half_sqrt(vsqdist).x; + vrsqh $v24.e0, $v22.e4 ## L:598 | 243 | vtmp.X = invert_half_sqrt(vsqdist).X; + vrsql $v25.e4, $v23.e4 ## L:598 | 244 | vtmp.X = invert_half_sqrt(vsqdist).X; + ldv $v07, 8, 24, $fp ## L:279 | ^ | m3:sint = load(address, 0x18).xyzwxyzw; + or $t4, $zero, $t2 ## L:646 | 245 | prev_out1 = vtx_out1_ptr; + vrsqh $v24.e4, $v00.e0 ## L:598 | ^ | vtmp.X = invert_half_sqrt(vsqdist).X; + vmudm $v29, $v15, $v25.h0 ## L:600 | **248 | vpos *= vtmp.xxxxXXXX; + vmadh $v15, $v15, $v24.h0 ## L:600 | 249 | vpos *= vtmp.xxxxXXXX; + ldv $v02, 0, 32, $fp ## L:274 | ^ | m0:ufract = load(address, 0x20).xyzwxyzw; + vmulf $v24, $v15, $v16.v ## L:603 | ***253 | vec16 vdot = vpos:sfract * vnorm:sfract; + vadd $v25, $v24, $v24.h1 ## L:604 | ***257 | vec16 vtmp:sint = vdot + vdot.yyyyYYYY; + vadd $v24, $v25, $v24.h2 ## L:605 | ***261 | vdot:sint = vtmp + vdot.zzzzZZZZ; + ldv $v01, 0, 0, $fp ## L:273 | ^ | m0:sint = load(address, 0x00).xyzwxyzw; + LOAD_COLOR656: llv $v19, 0, 0, $s4 ## L:656 | ***265 | @AttrLoader("COLOR") vrgba_in.xy = load(vtx_in0_ptr).xy; + vlt $v24, $v24, $v00 ## L:607 | ^ | vdot = min(vdot, VZERO); + vor $v25, $v00, $v00 ## L:610 | 266 | vec16 vzunit = VZERO; + vsubc $v24, $v00, $v24.v ## L:608 | **269 | vdot = VZERO - vdot; + vmov $v25.e6, $v12.e3 ## L:612 | 270 | vzunit.Z = vnormmask.w; + vmov $v25.e2, $v12.e3 ## L:611 | 271 | vzunit.z = vnormmask.w; + vmulf $v29, $v16, $v24.h0 ## L:614 | *273 | VTEMP = vnorm:sfract * vdot.xxxxXXXX; + vmacf $v29, $v16, $v24.h0 ## L:615 | 274 | VTEMP = vnorm:sfract +* vdot.xxxxXXXX; + vmadh $v29, $v15, $v30.e7 ## L:616 | 275 | VTEMP = vpos:sint +* 1; + addiu $t2, $t2, 80 ## L:650 | ^ | vtx_out1_ptr += 80; + LOAD_POSITION655: ldv $v15, 8, 0, $t1 ## L:655 | 276 | @AttrLoader("POSITION") vpos.XYZW = load(vtx_in1_ptr).xyzw; + vmadh $v29, $v25, $v30.e7 ## L:617 | ^ | VTEMP = vzunit:sint +* 1; + vsar $v25, COP2_ACC_MD ## L:620 | 277 | vec32 vrefl = get_acc(); + vsar $v24, COP2_ACC_HI ## L:620 | 278 | vec32 vrefl = get_acc(); + vmudl $v29, $v25, $v25.v ## L:622 | **281 | vsqdist = vrefl * vrefl; + ldv $v05, 0, 16, $fp ## L:277 | ^ | m2:sint = load(address, 0x10).xyzwxyzw; + vmadm $v29, $v24, $v25.v ## L:622 | 282 | vsqdist = vrefl * vrefl; + LOAD_NORMAL652: lsv $v16, 0, 0, $s4 ## L:652 | ^ | @AttrLoader("NORMAL") vnorm.x = load(vtx_in0_ptr).x; + vmadn $v23, $v25, $v24.v ## L:622 | 283 | vsqdist = vrefl * vrefl; + vmadh $v22, $v24, $v24.v ## L:622 | 284 | vsqdist = vrefl * vrefl; + ldv $v08, 0, 56, $fp ## L:280 | ^ | m3:ufract = load(address, 0x38).xyzwxyzw; + vaddc $v27, $v23, $v23.h1 ## L:623 | **287 | vec32 vtmp = vsqdist + vsqdist.yyyyYYYY; + ldv $v04, 8, 40, $fp ## L:276 | ^ | m1:ufract = load(address, 0x28).xyzwxyzw; + LOAD_NORMAL653: lsv $v16, 8, 0, $t1 ## L:653 | 288 | @AttrLoader("NORMAL") vnorm.X = load(vtx_in1_ptr).x; + vadd $v26, $v22, $v22.h1 ## L:623 | ^ | vec32 vtmp = vsqdist + vsqdist.yyyyYYYY; + vxor $v17, $v00, $v30.e0 ## L:629 | 289 | vtex = 0x80; + ldv $v06, 0, 48, $fp ## L:278 | ^ | m2:ufract = load(address, 0x30).xyzwxyzw; + ldv $v04, 0, 40, $fp ## L:276 | *291 | m1:ufract = load(address, 0x28).xyzwxyzw; + vaddc $v23, $v27, $v23.h2 ## L:624 | ^ | vsqdist = vtmp + vsqdist.zzzzZZZZ; + vadd $v22, $v26, $v22.h2 ## L:624 | 292 | vsqdist = vtmp + vsqdist.zzzzZZZZ; + vrsqh $v26.e0, $v22.e0 ## L:626 | ***296 | vtmp.x = invert_half_sqrt(vsqdist).x; + vrsql $v27.e0, $v23.e0 ## L:626 | 297 | vtmp.x = invert_half_sqrt(vsqdist).x; + vrsqh $v26.e0, $v22.e4 ## L:627 | 298 | vtmp.X = invert_half_sqrt(vsqdist).X; + ldv $v01, 8, 0, $fp ## L:273 | ^ | m0:sint = load(address, 0x00).xyzwxyzw; + vrsql $v27.e4, $v23.e4 ## L:627 | 299 | vtmp.X = invert_half_sqrt(vsqdist).X; + lqv $v23, 0, 144, $at ## L:636 | ^ | vtexscale = load(MG_SCRATCH_MEM, 0x80); + lqv $v22, 0, 128, $at ## L:636 | 300 | vtexscale = load(MG_SCRATCH_MEM, 0x80); + vrsqh $v26.e4, $v00.e0 ## L:627 | ^ | vtmp.X = invert_half_sqrt(vsqdist).X; + LOAD_POSITION654: ldv $v15, 0, 0, $s4 ## L:654 | 301 | @AttrLoader("POSITION") vpos.xyzw = load(vtx_in0_ptr).xyzw; + vmudh $v29, $v17, $v30.e7 ## L:630 | ^ | VTEMP = vtex:sint * 1; + vmadl $v29, $v25, $v27.h0 ## L:631 | *303 | vtex:sint = vrefl +* vtmp.xxxxXXXX; + ori $at, $zero, %lo(TEXTURING_OFFSET) ## L:637 | ^ | vtexoffset.xy = load(TEXTURING_OFFSET).xy; + vmadm $v29, $v24, $v27.h0 ## L:631 | 304 | vtex:sint = vrefl +* vtmp.xxxxXXXX; + ldv $v06, 8, 48, $fp ## L:278 | ^ | m2:ufract = load(address, 0x30).xyzwxyzw; + vmadn $v29, $v25, $v26.h0 ## L:631 | 305 | vtex:sint = vrefl +* vtmp.xxxxXXXX; + ldv $v03, 8, 8, $fp ## L:275 | ^ | m1:sint = load(address, 0x08).xyzwxyzw; + vmadh $v17, $v24, $v26.h0 ## L:631 | 306 | vtex:sint = vrefl +* vtmp.xxxxXXXX; + llv $v24, 0, 0, $at ## L:637 | ^ | vtexoffset.xy = load(TEXTURING_OFFSET).xy; + llv $v24, 8, 0, $at ## L:638 | 307 | vtexoffset.XY = load(TEXTURING_OFFSET).xy; + sltu $at, $s4, $s6 ## L:659 | ***311 | if (vtx_in0_ptr < vtx_in_end) goto VERTEX_LOOP; + vmudh $v29, $v24, $v30.e7 ## L:642 | ^ | VTEMP = vtexoffset:sint * 1; + vmadm $v29, $v17, $v23.v ## L:643 | 312 | vtex:sint = vtex:sint +* vtexscale; + bne $at, $zero, VERTEX_LOOP ## L:659 | ^ | if (vtx_in0_ptr < vtx_in_end) goto VERTEX_LOOP; + vmadh $v17, $v17, $v22.v ## L:643 | *314 | vtex:sint = vtex:sint +* vtexscale; + sdv $v20, 8, 0, $t4 ## L:666 | 315 | @Barrier("st_c1") @Barrier("st_tr1") store(vpos_clip:sint.XYZW, prev_out1, 0); ## Barrier: 0x30 + suv $v18, 4, 8, $t4 ## L:674 | 316 | @Barrier("st_tex1") store_vec_u8(vrgba.X, prev_out1, 8); ## Barrier: 0x80 + sdv $v20, 0, 0, $t3 ## L:665 | 317 | @Barrier("st_c0") @Barrier("st_tr0") store(vpos_clip:sint.xyzw, prev_out0, 0); ## Barrier: 0xC + suv $v18, 0, 8, $t3 ## L:673 | 318 | @Barrier("st_tex0") store_vec_u8(vrgba.x, prev_out0, 8); ## Barrier: 0x40 + sb $t7, 7($t3) ## L:670 | 319 | @Barrier("st_tr0") store(tr0, prev_out0, 7); ## Barrier: 0x8 + slv $v17, 0, 12, $t3 ## L:676 | 320 | @Barrier("st_tex0") store(vtex.xy, prev_out0, 12); ## Barrier: 0x40 + sb $t5, 6($t3) ## L:668 | 321 | @Barrier("st_c0") store(c0, prev_out0, 6); ## Barrier: 0x4 + sb $t6, 6($t4) ## L:669 | 322 | @Barrier("st_c1") store(c1, prev_out1, 6); ## Barrier: 0x10 + slv $v17, 8, 12, $t4 ## L:677 | 323 | @Barrier("st_tex1") store(vtex.XY, prev_out1, 12); ## Barrier: 0x80 + j RSPQ_Loop ## L:678 | 324 | } + sb $k0, 7($t4) ## L:671 | *326 | @Barrier("st_tr1") store(tr1, prev_out1, 7); ## Barrier: 0x20 + +MgEndShader + +#define zero $0 +#define v0 $2 +#define v1 $3 +#define a0 $4 +#define a1 $5 +#define a2 $6 +#define a3 $7 +#define t0 $8 +#define t1 $9 +#define t2 $10 +#define t3 $11 +#define t4 $12 +#define t5 $13 +#define t6 $14 +#define t7 $15 +#define s0 $16 +#define s1 $17 +#define s2 $18 +#define s3 $19 +#define s4 $20 +#define s5 $21 +#define s6 $22 +#define s7 $23 +#define t8 $24 +#define t9 $25 +#define k0 $26 +#define k1 $27 +#define gp $28 +#define sp $29 +#define fp $30 +#define ra $31 + +.set at +.set macro \ No newline at end of file diff --git a/src/math/fgeom.c b/src/math/fgeom.c new file mode 100644 index 000000000..553f10cdb --- /dev/null +++ b/src/math/fgeom.c @@ -0,0 +1,338 @@ +/** + * @file fgeom.c + * @author Dennis Heinze + */ +#include "fgeom.h" + +void fm_vec3_reflect(fm_vec3_t *out, const fm_vec3_t *i, const fm_vec3_t *n) +{ + fm_vec3_t tmp; + fm_vec3_scale(&tmp, n, 2.0f * fm_vec3_dot(i, n)); + fm_vec3_sub(out, i, &tmp); +} + +bool fm_vec3_refract(fm_vec3_t *out, const fm_vec3_t *i, const fm_vec3_t *n, float eta) +{ + float ndi = fm_vec3_dot(n, i); + float eni = eta * ndi; + float k = 1.0f - eta*eta + eni*eni; + + if (k < 0.0f) { + *out = (fm_vec3_t){}; + return false; + } + + fm_vec3_t tmp; + fm_vec3_scale(&tmp, n, eni + sqrtf(k)); + fm_vec3_scale(out, i, eta); + fm_vec3_sub(out, out, &tmp); + return true; +} + +void fm_vec3_rotate(fm_vec3_t *out, const fm_vec3_t *i, const fm_quat_t *q) +{ + float qx = q->x, qy = q->y, qz = q->z, qw = q->w; + float tx = 2.0f * (qy * i->z - qz * i->y); + float ty = 2.0f * (qz * i->x - qx * i->z); + float tz = 2.0f * (qx * i->y - qy * i->x); + + out->x = i->x + qw * tx + (qy * tz - qz * ty); + out->y = i->y + qw * ty + (qz * tx - qx * tz); + out->z = i->z + qw * tz + (qx * ty - qy * tx); +} + +void fm_quat_from_euler_zyx(fm_quat_t *out, float x, float y, float z) +{ + float xs, xc, ys, yc, zs, zc; + + fm_sincosf(x * 0.5f, &xs, &xc); + fm_sincosf(y * 0.5f, &ys, &yc); + fm_sincosf(z * 0.5f, &zs, &zc); + + out->v[0] = xs * yc * zc - xc * ys * zs; + out->v[1] = xc * ys * zc + xs * yc * zs; + out->v[2] = -xs * ys * zc + xc * yc * zs; + out->v[3] = xc * yc * zc + xs * ys * zs; +} + +inline void fm_quat_mul(fm_quat_t *out, const fm_quat_t *a, const fm_quat_t *b) +{ + *out = (fm_quat_t){{ a->v[3] * b->v[0] + a->v[0] * b->v[3] + a->v[1] * b->v[2] - a->v[2] * b->v[1], + a->v[3] * b->v[1] - a->v[0] * b->v[2] + a->v[1] * b->v[3] + a->v[2] * b->v[0], + a->v[3] * b->v[2] + a->v[0] * b->v[1] - a->v[1] * b->v[0] + a->v[2] * b->v[3], + a->v[3] * b->v[3] - a->v[0] * b->v[0] - a->v[1] * b->v[1] - a->v[2] * b->v[2] }}; +} + +void fm_quat_rotate(fm_quat_t *out, const fm_quat_t *q, const fm_vec3_t *axis, float angle) +{ + fm_quat_t r; + fm_quat_from_axis_angle(&r, axis, angle); + fm_quat_mul(out, q, &r); +} + +void fm_quat_nlerp(fm_quat_t *out, const fm_quat_t *a, const fm_quat_t *b, float t) +{ + float blend = 1.0f - t; + if (fm_quat_dot(a, b) < 0.0f) blend = -blend; + + *out = (fm_quat_t){{ a->v[0] * blend + b->v[0] * t, + a->v[1] * blend + b->v[1] * t, + a->v[2] * blend + b->v[2] * t, + a->v[3] * blend + b->v[3] * t }}; + fm_quat_norm(out, out); +} + +void fm_quat_slerp(fm_quat_t *out, const fm_quat_t *a, const fm_quat_t *b, float t) +{ + float dot = fm_quat_dot(a, b); + float negated = 1.0f; + if (dot < 0.0f) { + dot = -dot; + negated = -1.0f; + } + + if (dot > 1.0f) dot = 1.0f; + + /* Avoid division by very small values for nearly identical inputs. */ + if (dot > 0.9995f) { + float blend = 1.0f - t; + *out = (fm_quat_t){{ a->v[0] * blend + b->v[0] * t * negated, + a->v[1] * blend + b->v[1] * t * negated, + a->v[2] * blend + b->v[2] * t * negated, + a->v[3] * blend + b->v[3] * t * negated }}; + fm_quat_norm(out, out); + return; + } + + float theta = acosf(dot); + float sin_theta = sinf(theta); + float inv_sin_theta = 1.0f / sin_theta; + float ws = sinf((1.0f - t) * theta) * inv_sin_theta; + float we = sinf(t * theta) * inv_sin_theta * negated; + *out = (fm_quat_t){{ a->v[0] * ws + b->v[0] * we, + a->v[1] * ws + b->v[1] * we, + a->v[2] * ws + b->v[2] * we, + a->v[3] * ws + b->v[3] * we }}; +} + +void fm_mat4_rotate(fm_mat4_t *out, const fm_quat_t *rotation) +{ + fm_mat4_t rotation_matrix; + fm_mat4_from_rotation(&rotation_matrix, rotation); + fm_mat4_mul(out, &rotation_matrix, out); +} + +void fm_mat4_from_axis_angle(fm_mat4_t *out, const fm_vec3_t *axis, float angle) +{ + float s, c; + fm_sincosf(angle, &s, &c); + + float t = 1.0f - c; + float x = axis->x, y = axis->y, z = axis->z; + + *out = (fm_mat4_t){{ + { t * x * x + c, t * x * y - s * z, t * x * z + s * y, 0 }, + { t * x * y + s * z, t * y * y + c, t * y * z - s * x, 0 }, + { t * x * z - s * y, t * y * z + s * x, t * z * z + c, 0 }, + { 0, 0, 0, 1 } + }}; +} + +void fm_mat4_from_srt(fm_mat4_t *out, const fm_vec3_t *scale, const fm_quat_t *quat, const fm_vec3_t *translate) +{ + float x = quat->x, y = quat->y, z = quat->z, w = quat->w; + float x2 = x + x, y2 = y + y, z2 = z + z; + float xx = x * x2, xy = x * y2, xz = x * z2; + float yy = y * y2, yz = y * z2, zz = z * z2; + float wx = w * x2, wy = w * y2, wz = w * z2; + float sx = scale->x, sy = scale->y, sz = scale->z; + + *out = (fm_mat4_t){{ + { (1.0f - (yy + zz)) * sx, (xy + wz) * sx, (xz - wy) * sx, 0 }, + { (xy - wz) * sy, (1.0f - (xx + zz)) * sy, (yz + wx) * sy, 0 }, + { (xz + wy) * sz, (yz - wx) * sz, (1.0f - (xx + yy)) * sz, 0 }, + { translate->x, translate->y, translate->z, 1 } + }}; +} + +void fm_mat4_from_srt_euler(fm_mat4_t *out, const fm_vec3_t *scale, const float euler[3], const fm_vec3_t *translate) +{ + float s0, c0, s1, c1, s2, c2; + fm_sincosf(euler[0], &s0, &c0); + fm_sincosf(euler[1], &s1, &c1); + fm_sincosf(euler[2], &s2, &c2); + + *out = (fm_mat4_t){{ + { scale->x * c2 * c1, scale->x * (c2 * s1 * s0 - s2 * c0), scale->x * (c2 * s1 * c0 + s2 * s0), 0 }, + { scale->y * s2 * c1, scale->y * (s2 * s1 * s0 + c2 * c0), scale->y * (s2 * s1 * c0 - c2 * s0), 0 }, + {-scale->z * s1, scale->z * c1 * s0, scale->z * c1 * c0, 0 }, + { translate->x, translate->y, translate->z, 1 } + }}; +} + +float fm_mat4_det(const fm_mat4_t *m) +{ + float m00 = m->m[0][0], m01 = m->m[0][1], m02 = m->m[0][2], m03 = m->m[0][3], + m10 = m->m[1][0], m11 = m->m[1][1], m12 = m->m[1][2], m13 = m->m[1][3], + m20 = m->m[2][0], m21 = m->m[2][1], m22 = m->m[2][2], m23 = m->m[2][3], + m30 = m->m[3][0], m31 = m->m[3][1], m32 = m->m[3][2], m33 = m->m[3][3]; + + float t0 = m22 * m33 - m32 * m23; + float t1 = m21 * m33 - m31 * m23; + float t2 = m21 * m32 - m31 * m22; + float t3 = m20 * m33 - m30 * m23; + float t4 = m20 * m32 - m30 * m22; + float t5 = m20 * m31 - m30 * m21; + + return m00 * (m11 * t0 - m12 * t1 + m13 * t2) + - m01 * (m10 * t0 - m12 * t3 + m13 * t4) + + m02 * (m10 * t1 - m11 * t3 + m13 * t5) + - m03 * (m10 * t2 - m11 * t4 + m12 * t5); +} + +void fm_mat4_inverse(fm_mat4_t *out, const fm_mat4_t *m) +{ + fm_mat4_t tmp; + + float m00 = m->m[0][0], m01 = m->m[0][1], m02 = m->m[0][2], m03 = m->m[0][3], + m10 = m->m[1][0], m11 = m->m[1][1], m12 = m->m[1][2], m13 = m->m[1][3], + m20 = m->m[2][0], m21 = m->m[2][1], m22 = m->m[2][2], m23 = m->m[2][3], + m30 = m->m[3][0], m31 = m->m[3][1], m32 = m->m[3][2], m33 = m->m[3][3]; + + float t0 = m22 * m33 - m32 * m23; + float t1 = m21 * m33 - m31 * m23; + float t2 = m21 * m32 - m31 * m22; + float t3 = m20 * m33 - m30 * m23; + float t4 = m20 * m32 - m30 * m22; + float t5 = m20 * m31 - m30 * m21; + + tmp.m[0][0] = m11 * t0 - m12 * t1 + m13 * t2; + tmp.m[1][0] =-(m10 * t0 - m12 * t3 + m13 * t4); + tmp.m[2][0] = m10 * t1 - m11 * t3 + m13 * t5; + tmp.m[3][0] =-(m10 * t2 - m11 * t4 + m12 * t5); + + tmp.m[0][1] =-(m01 * t0 - m02 * t1 + m03 * t2); + tmp.m[1][1] = m00 * t0 - m02 * t3 + m03 * t4; + tmp.m[2][1] =-(m00 * t1 - m01 * t3 + m03 * t5); + tmp.m[3][1] = m00 * t2 - m01 * t4 + m02 * t5; + + t0 = m12 * m33 - m32 * m13; + t1 = m11 * m33 - m31 * m13; + t2 = m11 * m32 - m31 * m12; + t3 = m10 * m33 - m30 * m13; + t4 = m10 * m32 - m30 * m12; + t5 = m10 * m31 - m30 * m11; + + tmp.m[0][2] = m01 * t0 - m02 * t1 + m03 * t2; + tmp.m[1][2] =-(m00 * t0 - m02 * t3 + m03 * t4); + tmp.m[2][2] = m00 * t1 - m01 * t3 + m03 * t5; + tmp.m[3][2] =-(m00 * t2 - m01 * t4 + m02 * t5); + + t0 = m12 * m23 - m22 * m13; + t1 = m11 * m23 - m21 * m13; + t2 = m11 * m22 - m21 * m12; + t3 = m10 * m23 - m20 * m13; + t4 = m10 * m22 - m20 * m12; + t5 = m10 * m21 - m20 * m11; + + tmp.m[0][3] =-(m01 * t0 - m02 * t1 + m03 * t2); + tmp.m[1][3] = m00 * t0 - m02 * t3 + m03 * t4; + tmp.m[2][3] =-(m00 * t1 - m01 * t3 + m03 * t5); + tmp.m[3][3] = m00 * t2 - m01 * t4 + m02 * t5; + + float det = 1.0f / (m00 * tmp.m[0][0] + m01 * tmp.m[1][0] + + m02 * tmp.m[2][0] + m03 * tmp.m[3][0]); + + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 4; j++) + { + out->m[i][j] = tmp.m[i][j] * det; + } + } +} + +void fm_mat4_affine_to_normal_mat(fm_mat4_t *out, const fm_mat4_t *m) +{ + float m00 = m->m[0][0], m01 = m->m[0][1], m02 = m->m[0][2], + m10 = m->m[1][0], m11 = m->m[1][1], m12 = m->m[1][2], + m20 = m->m[2][0], m21 = m->m[2][1], m22 = m->m[2][2]; + + float tmp[3][3]; + + tmp[0][0] = m11 * m22 - m12 * m21; + tmp[0][1] = -(m01 * m22 - m21 * m02); + tmp[0][2] = m01 * m12 - m11 * m02; + tmp[1][0] = -(m10 * m22 - m20 * m12); + tmp[1][1] = m00 * m22 - m02 * m20; + tmp[1][2] = -(m00 * m12 - m10 * m02); + tmp[2][0] = m10 * m21 - m20 * m11; + tmp[2][1] = -(m00 * m21 - m20 * m01); + tmp[2][2] = m00 * m11 - m01 * m10; + + float det = 1.0f / (m00 * tmp[0][0] + m01 * tmp[1][0] + m02 * tmp[2][0]); + + *out = (fm_mat4_t){{ + {tmp[0][0] * det, tmp[1][0] * det, tmp[2][0] * det, 0}, + {tmp[0][1] * det, tmp[1][1] * det, tmp[2][1] * det, 0}, + {tmp[0][2] * det, tmp[1][2] * det, tmp[2][2] * det, 0}, + {0, 0, 0, 1}, + }}; +} + +void fm_mat4_look(fm_mat4_t *out, const fm_vec3_t *eye, const fm_vec3_t *dir, const fm_vec3_t *up) +{ + fm_vec3_t s, u; + + fm_vec3_cross(&s, dir, up); + fm_vec3_norm(&s, &s); + + fm_vec3_cross(&u, &s, dir); + + *out = (fm_mat4_t){{ + {s.x, u.x, -dir->x, 0}, + {s.y, u.y, -dir->y, 0}, + {s.z, u.z, -dir->z, 0}, + {-fm_vec3_dot(&s, eye), -fm_vec3_dot(&u, eye), fm_vec3_dot(dir, eye), 1}, + }}; +} + +void fm_mat4_lookat(fm_mat4_t *out, const fm_vec3_t *eye, const fm_vec3_t *target, const fm_vec3_t *up) +{ + fm_vec3_t dir; + fm_vec3_sub(&dir, target, eye); + fm_vec3_norm(&dir, &dir); + fm_mat4_look(out, eye, &dir, up); +} + +extern inline void fm_vec3_negate(fm_vec3_t *out, const fm_vec3_t *v); +extern inline void fm_vec3_add(fm_vec3_t *out, const fm_vec3_t *a, const fm_vec3_t *b); +extern inline void fm_vec3_sub(fm_vec3_t *out, const fm_vec3_t *a, const fm_vec3_t *b); +extern inline void fm_vec3_mul(fm_vec3_t *out, const fm_vec3_t *a, const fm_vec3_t *b); +extern inline void fm_vec3_div(fm_vec3_t *out, const fm_vec3_t *a, const fm_vec3_t *b); +extern inline void fm_vec3_scale(fm_vec3_t *out, const fm_vec3_t *a, float s); +extern inline float fm_vec3_dot(const fm_vec3_t *a, const fm_vec3_t *b); +extern inline float fm_vec3_len2(const fm_vec3_t *a); +extern inline float fm_vec3_len(const fm_vec3_t *a); +extern inline float fm_vec3_distance2(const fm_vec3_t *a, const fm_vec3_t *b); +extern inline float fm_vec3_distance(const fm_vec3_t *a, const fm_vec3_t *b); +extern inline void fm_vec3_norm(fm_vec3_t *out, const fm_vec3_t *a); +extern inline void fm_vec3_cross(fm_vec3_t *out, const fm_vec3_t *a, const fm_vec3_t *b); +extern inline void fm_vec3_lerp(fm_vec3_t *out, const fm_vec3_t *a, const fm_vec3_t *b, float t); +extern inline void fm_quat_identity(fm_quat_t *out); +extern inline void fm_quat_from_axis_angle(fm_quat_t *out, const fm_vec3_t *axis, float angle); +extern inline float fm_quat_dot(const fm_quat_t *a, const fm_quat_t *b); +extern inline void fm_quat_inverse(fm_quat_t *out, const fm_quat_t *q); +extern inline void fm_quat_norm(fm_quat_t *out, const fm_quat_t *q); +extern inline void fm_mat4_identity(fm_mat4_t *out); +extern inline void fm_mat4_scale(fm_mat4_t *out, const fm_vec3_t *scale); +extern inline void fm_mat4_translate(fm_mat4_t *out, const fm_vec3_t *translate); +extern inline void fm_mat4_from_rt(fm_mat4_t *out, const fm_quat_t *quat, const fm_vec3_t *translate); +extern inline void fm_mat4_from_rt_euler(fm_mat4_t *out, const float euler[3], const fm_vec3_t *translate); +extern inline void fm_mat4_from_translation(fm_mat4_t *out, const fm_vec3_t *translate); +extern inline void fm_mat4_from_rotation(fm_mat4_t *out, const fm_quat_t *rotation); +extern inline void fm_mat4_from_scale(fm_mat4_t *out, const fm_vec3_t *scale); +extern inline void fm_mat4_mul(fm_mat4_t *out, const fm_mat4_t *a, const fm_mat4_t *b); +extern inline void fm_mat4_transpose(fm_mat4_t *out, const fm_mat4_t *m); +extern inline void fm_mat4_mul_vec3(fm_vec4_t *out, const fm_mat4_t *m, const fm_vec3_t *v); +extern inline void fm_mat4_mul_vec4(fm_vec4_t *out, const fm_mat4_t *m, const fm_vec4_t *v); diff --git a/src/math/fgeom2d.c b/src/math/fgeom2d.c new file mode 100644 index 000000000..c03dd8a45 --- /dev/null +++ b/src/math/fgeom2d.c @@ -0,0 +1,104 @@ +/** + * @file fgeom2d.c + */ + +#include "fgeom2d.h" + + +void fm_mat3_rotate(fm_mat3_t *out, float rotation) +{ + fm_mat3_t rotation_matrix; + fm_mat3_from_rotation(&rotation_matrix, rotation); + fm_mat3_mul(out, &rotation_matrix, out); +} + +void fm_mat3_from_rst(fm_mat3_t *out, float rotation, const fm_vec2_t *scale, const fm_vec2_t *translate) +{ + float s, c; + + fm_sincosf(rotation, &s, &c); + float sx = scale->x, sy = scale->y; + + *out = (fm_mat3_t){{ + { c*sx, -s*sy, 0 }, + { s*sx, c*sy, 0 }, + { translate->x, translate->y, 1 } + }}; +} + +void fm_mat3_from_srt(fm_mat3_t *out, const fm_vec2_t *scale, float rotation, const fm_vec2_t *translate) +{ + float s, c; + + fm_sincosf(rotation, &s, &c); + float sx = scale->x, sy = scale->y; + + *out = (fm_mat3_t){{ + { c*sx, -s*sx, 0 }, + { s*sy, c*sy, 0 }, + { translate->x, translate->y, 1 } + }}; +} + +float fm_mat3_det(const fm_mat3_t *m) +{ + float m00 = m->m[0][0], m01 = m->m[0][1], m02 = m->m[0][2], + m10 = m->m[1][0], m11 = m->m[1][1], m12 = m->m[1][2], + m20 = m->m[2][0], m21 = m->m[2][1], m22 = m->m[2][2]; + + return m00 * (m11 * m22 - m21 * m12) - + m01 * (m10 * m22 - m12 * m20) + + m02 * (m10 * m21 - m11 * m20); +} + +void fm_mat3_inverse(fm_mat3_t *out, const fm_mat3_t *m) +{ + fm_mat3_t tmp; + float m00 = m->m[0][0], m01 = m->m[0][1], m02 = m->m[0][2], + m10 = m->m[1][0], m11 = m->m[1][1], m12 = m->m[1][2], + m20 = m->m[2][0], m21 = m->m[2][1], m22 = m->m[2][2]; + tmp.m[0][0] = (m11*m22)-(m12*m21); + tmp.m[0][1] = -((m01*m22)-(m02*m21)); + tmp.m[0][2] = (m01*m12)-(m02*m11); + + tmp.m[1][0] = (m10*m22)-(m12*m20); + tmp.m[1][1] = -((m00*m22)-(m02*m20)); + tmp.m[1][2] = (m00*m12)-(m02*m10); + + tmp.m[2][0] = (m10*m21)-(m11*m20); + tmp.m[2][1] = -((m00*m21)-(m01*m20)); + tmp.m[2][2] = (m00*m11)-(m01*m10); + float det = 1.0f/(m00 * (m11 * m22 - m21 * m12) - + m01 * (m10 * m22 - m12 * m20) + + m02 * (m10 * m21 - m11 * m20)); + for(int i=0; i<3; i++) { + for(int j=0; j<3; j++) { + out->m[i][j] = tmp.m[i][j]*det; + } + } +} + +extern inline void fm_vec2_negate(fm_vec2_t *out, const fm_vec2_t *v); +extern inline void fm_vec2_add(fm_vec2_t *out, const fm_vec2_t *a, const fm_vec2_t *b); +extern inline void fm_vec2_sub(fm_vec2_t *out, const fm_vec2_t *a, const fm_vec2_t *b); +extern inline void fm_vec2_mul(fm_vec2_t *out, const fm_vec2_t *a, const fm_vec2_t *b); +extern inline void fm_vec2_div(fm_vec2_t *out, const fm_vec2_t *a, const fm_vec2_t *b); +extern inline void fm_vec2_scale(fm_vec2_t *out, const fm_vec2_t *a, float s); +extern inline float fm_vec2_dot(const fm_vec2_t *a, const fm_vec2_t *b); +extern inline float fm_vec2_len2(const fm_vec2_t *a); +extern inline float fm_vec2_len(const fm_vec2_t *a); +extern inline float fm_vec2_distance2(const fm_vec2_t *a, const fm_vec2_t *b); +extern inline float fm_vec2_distance(const fm_vec2_t *a, const fm_vec2_t *b); +extern inline void fm_vec2_norm(fm_vec2_t *out, const fm_vec2_t *a); +extern inline void fm_vec2_lerp(fm_vec2_t *out, const fm_vec2_t *a, const fm_vec2_t *b, float t); +extern inline void fm_mat3_identity(fm_mat3_t *out); +extern inline void fm_mat3_scale(fm_mat3_t *out, const fm_vec2_t *scale); +extern inline void fm_mat3_translate(fm_mat3_t *out, const fm_vec2_t *translate); +extern inline void fm_mat3_from_rt(fm_mat3_t *out, float rotation, const fm_vec2_t *translate); +extern inline void fm_mat3_from_translation(fm_mat3_t *out, const fm_vec2_t *translate); +extern inline void fm_mat3_from_rotation(fm_mat3_t *out, float rotation); +extern inline void fm_mat3_from_scale(fm_mat3_t *out, const fm_vec2_t *scale); +extern inline void fm_mat3_mul(fm_mat3_t *out, const fm_mat3_t *a, const fm_mat3_t *b); +extern inline void fm_mat3_transpose(fm_mat3_t *out, const fm_mat3_t *m); +extern inline void fm_mat3_mul_vec2(fm_vec3_t *out, const fm_mat3_t *m, const fm_vec2_t *v); +extern inline void fm_mat3_mul_vec3(fm_vec3_t *out, const fm_mat3_t *m, const fm_vec3_t *v); diff --git a/src/fmath.c b/src/math/fmath.c similarity index 55% rename from src/fmath.c rename to src/math/fmath.c index 61558d710..71d77302b 100644 --- a/src/fmath.c +++ b/src/math/fmath.c @@ -26,13 +26,14 @@ static float sinf_approx(float x, int approx) { s = x * x; // Execute only a portion of the series, depending on the approximation level. // This generate the most efficient code among similar approaches. - if (LIKELY(--approx < 0)) p += 1.32729383e-10f, p *= s; - if (LIKELY(--approx < 0)) p += - 2.33177868e-8f, p *= s; - if (LIKELY(--approx < 0)) p += 2.52223435e-6f, p *= s; - if (LIKELY(--approx < 0)) p += - 1.73503853e-4f, p *= s; - if (LIKELY(--approx < 0)) p += 6.62087463e-3f, p *= s; - if (LIKELY(--approx < 0)) p += - 1.01321176e-1f; - return x * ((x - pi_hi) - pi_lo) * ((x + pi_hi) + pi_lo) * p; + if (LIKELY(--approx < 0)) p += 1.3291330536e-10f, p *= s; + if (LIKELY(--approx < 0)) p += - 2.3317808128e-8f, p *= s; + if (LIKELY(--approx < 0)) p += 2.5222900603e-6f, p *= s; + if (LIKELY(--approx < 0)) p += - 1.7350520647e-4f, p *= s; + if (LIKELY(--approx < 0)) p += 6.6208802163e-3f, p *= s; + if (LIKELY(--approx < 0)) p += - 1.0132116824e-1f; + x = x * ((x - pi_hi) - pi_lo) * ((x + pi_hi) + pi_lo) * p; + return x; } float fm_sinf_approx(float x, int approx) { @@ -42,10 +43,6 @@ float fm_sinf_approx(float x, int approx) { // to the 5 ULP figure. x = fm_wrapf(x+pi_hi, 2*pi_hi) - pi_hi; x = sinf_approx(x, approx); - // FIXME: workaround for a bug in our sinf approximation. We found at least - // one input (0xbfc915a2 => -1.570973) that produces an out of bounds result - // -1.000000119209289551 (0xbf800001). - x = CLAMP(x, -1.0f, 1.0f); return x; } @@ -84,3 +81,51 @@ float fm_atan2f(float y, float x) { return copysignf(r, y); } +float fm_expf(float x){ + // Approximation of exp(x) with a relative error <3%. + // This is several times faster than exp(x). The implementation uses a + // method by Nicole Schraudolph. + // Note that there's no bounds check and x will overflow if x is not in ~(-85,85) range + // Source: https://gist.github.com/jrade/293a73f89dfef51da6522428c857802d + const float a = (1 << 23) / 0.69314718f; + const float b = (1 << 23) * (127 - 0.043677448f); + x = a * x + b; + + uint32_t i = (uint32_t)x; + return BITCAST_I2F(i); +} + +float fm_exp2f(float x) +{ + // 2^x = 2^(n+f) = 2^n · 2^f, with f ∈ [0, 1). 2^n is a bit-shift of the + // IEEE754 exponent; 2^f is a degree-5 Remez polynomial (rel. err. ~1e-5). + int32_t n = (int32_t)x; + if ((float)n > x) n--; + float f = x - (float)n; + + // Remez minimax for 2^f on [0, 1], degree 5. + const float c0 = 1.0000000000e+00f; + const float c1 = 6.9314718056e-01f; + const float c2 = 2.4022650696e-01f; + const float c3 = 5.5503486648e-02f; + const float c4 = 9.6183710763e-03f; + const float c5 = 1.3333558146e-03f; + float y = c0 + f * (c1 + f * (c2 + f * (c3 + f * (c4 + f * c5)))); + + uint32_t bits = (uint32_t)(n + 127) << 23; + return y * BITCAST_I2F(bits); +} + +float fm_lerp_angle(float a, float b, float t) +{ + float diff = fm_fmodf((b - a), FM_PI*2); + float dist = fm_fmodf(diff*2, FM_PI*2) - diff; + return a + dist * t; +} + +float fm_wrap_angle(float angle) +{ + return fm_wrapf(angle, FM_PI*2); +} + +extern inline float fm_lerp(float a, float b, float t); diff --git a/src/math/libdragon.mk b/src/math/libdragon.mk new file mode 100644 index 000000000..e56a9045c --- /dev/null +++ b/src/math/libdragon.mk @@ -0,0 +1,4 @@ +LIBDRAGON_OBJS += \ + $(BUILD_DIR)/math/fmath.o \ + $(BUILD_DIR)/math/fgeom.o \ + $(BUILD_DIR)/math/fgeom2d.o \ No newline at end of file diff --git a/src/mi_memset.S b/src/mi_memset.S new file mode 100644 index 000000000..60876e958 --- /dev/null +++ b/src/mi_memset.S @@ -0,0 +1,219 @@ +######################################################################### +# __mi_memset - Fast memset using MI repeat mode + CPU fallback for iQue +# +# Authors: Giovanni Bajo +######################################################################### + +#include "regs.S" +#define MI_WMODE_SET_REPEAT_MODE 0x100 + + .section .text.__mi_memset + .p2align 5 + .globl __mi_memset + .globl __mi_memset16 + .globl __mi_memset32 + .globl __mi_memset64 + .set at + .set noreorder + .extern __boot_consoletype # 0=n64, 1=iQue + +# Force the same N opcodes to be in the same cacheline +.macro .same_cacheline num_opcodes + .p2align 5,,((\num_opcodes-1)*4) +.endm +.macro cache_wbi addr + cache HIT_WRITEBACK_INVALIDATE_D, \addr +.endm +.macro cache_i addr + cache HIT_INVALIDATE_D, \addr +.endm + +#define ptr a0 +#define val a1 +#define len a2 +#define end t8 +#define srsave t7 +#define srnoirq t6 +#define MI_MODE t5 +#define duffptr t4 +#define origptr v0 +#define uptr v1 + + + # __mi_memsetN(void *ptr, int value, size_t len) +__mi_memset: + dsll t0, val, 8 + or val, t0 +__mi_memset16: + dsll t0, val, 16 + or val, t0 +__mi_memset32: + dsll t0, val, 32 + or val, t0 +__mi_memset64: + move origptr, ptr + addiu t0, len, -7 + bltz t0, .Lsmall + addu end, ptr, len + + # Align to the next 8-byte boundary + andi t0, ptr, 7 + beqz t0, .Laligned8 + neg t0 + + # Write the first few bytes to reach 8-byte alignment + sdl val, 0(ptr) + andi t0, 7 + sll t1, t0, 3 + addu ptr, t0 + sub len, t0 + beqz len, .Lexit + + # Rotate the value after the partial write + dsllv t2, val, t1 + neg t1, t1 + dsrlv val, val, t1 + or val, t2 + +.Laligned8: + # We are now 8-byte aligned. + # Fallback to CPU memset if not supported on hardware (iQue) + lw t0, %gprel(__boot_consoletype)(gp) + bnez t0, .Lcpu_memset + nop + # From now on, we will be using + # the repeat mode. Setup registers + or uptr, ptr, 0x20000000 + la duffptr, .Lduffend + mfc0 srsave, C0_SR + and srnoirq, srsave, ~0x1 + beq uptr, ptr, .Lmirepeat_first # Check if the pointer is uncached + li MI_MODE, 0xA4300000 + + # Write the possibly-partial first/last cachelines to avoid + # invalidating false-shared data. We could do that always, + # but make them conditional to save a few cycles for aligned cases. + andi t0, ptr, 0xF + bnezl t0, 1f + cache_wbi (ptr) + # End pointer could be misaligned, we need to sweat a bit more +1: andi t0, end, 0xF + beqz t0, 1f + srl t0, end, 3 + sll t0, 3 + cache_wbi (t0) +1: + +.Lmirepeat_first: + # Now align to 128 bytes, by limiting the first repetition. + # Repeat modes does not require 128 bytes alignment, but it does require + # the whole write stay within a RDRAM row (0x800), and there is a maximum + # of 128 bytes per repeat, so we kill two birds with one stone. + neg t0, ptr + andi t0, 0x7F + beqzl t0, .Lmirepeat_len +.Lmirepeat: + li t0, 128 +.Lmirepeat_len: + # t0 is now the maximum requested length for this + # chunk (up to 128 bytes). Calculate t0 = min(t0, len) + bltl len, t0, 1f + move t0, len +1: + # Prepare repeat mode register value in t1 + ori t1, t0, MI_WMODE_SET_REPEAT_MODE + beq uptr, ptr, .Lduffend # If the pointer is uncached, do not invalidate cache + addiu t1, -1 + + # Calculate duff device pointer offset. We will jump to + # .Lduffend - ((len - 1) / 16 * 4) + addi t2, t0, -1 + srl t2, 4 + sll t2, 2 + sub t2, duffptr, t2 + jr t2 + cache_i 0(ptr) + # Duff device to clear the cache. Some of these cache + # instructions will be skipped if < 128 bytes will be written. + cache_i 112(ptr) + cache_i 96(ptr) + cache_i 80(ptr) + cache_i 64(ptr) + cache_i 48(ptr) + cache_i 32(ptr) + cache_i 16(ptr) +.Lduffend: + + # Now perform the repeated copy via MI repeat mode. + # We need to guarantee that the very next memory access after setting up MI repeat + # is the one to repeat. This means that: + # 1) interrupts must be disabled of course + # 2) we must be sure that both instructions are within the same i-cacheline, + # so that no cache fetch can happen in-between. + mtc0 srnoirq, C0_SR + .same_cacheline 2 + sw t1, (MI_MODE) + sd val, (uptr) + mtc0 srsave, C0_SR + + # Loop epilogue: update pointers and remaining length + sub len, t0 + add ptr, t0 + bgtz len, .Lmirepeat + add uptr, t0 + jr ra + nop + + # Fewer than 8 bytes in total: do it bytewise as speed is not critical + # Also ptr can be misaligned so it'd be hard to use larger writes +.Lsmall: + dsrl t0, val, 56 + sb t0, 0(ptr) + dsll val, 8 + addi len, -1 + bgtz len, .Lsmall + addi ptr, 1 +.Lexit: + jr ra + nop + + # CPU fallback memset, used when repeat mode is not supported (iQue) +.Lcpu_memset: + # Copy 64 bytes at a time to minimize loop overhead + b .Lcpu_memset64_check + li t0, 64 +.Lcpu_memset64: + sd val, 0(ptr) + sd val, 8(ptr) + sd val, 16(ptr) + sd val, 24(ptr) + sd val, 32(ptr) + sd val, 40(ptr) + sd val, 48(ptr) + sd val, 56(ptr) + sub len, 64 + addiu ptr, 64 +.Lcpu_memset64_check: + bge len, t0, .Lcpu_memset64 + nop + + b .Lcpu_memset8_check + li t1, 8 +.Lcpu_memset8: + sd val, 0(ptr) + sub len, 8 + addiu ptr, 8 +.Lcpu_memset8_check: + bge len, t1, .Lcpu_memset8 + nop + +.Lcpu_memset_epilogue: + # Write the remaining bytes with a single sdr, as we know + # the pointer is 8 byte aligned. We need to munge a bit + # address and value but it's for greater good. + add ptr, len + neg len + sll t0, len, 3 + dsrlv val, val, t0 + jr ra + sdr val, -1(ptr) diff --git a/src/model64.c b/src/model64.c new file mode 100644 index 000000000..2dd5c25ce --- /dev/null +++ b/src/model64.c @@ -0,0 +1,1158 @@ +/** + * @file model64.c + * @author Dennis Heinze + * @author gamemasterplc + */ +#include +#include +#include +#include +#include "fmath.h" +#include "n64sys.h" +#include "GL/gl.h" +#include "dragonfs.h" +#include "dma.h" +#include "model64.h" +#include "model64_internal.h" +#include "asset.h" +#include "debug.h" +#include "sprite.h" +#include "utils.h" +#include "rdpq_tex.h" + +#include "model64_catmull.h" + + +/** @brief Loading state of a texture_entry_t */ +typedef enum { + ENTRY_STATE_EMPTY = 0, + ENTRY_STATE_SPRITE_LOADED, + ENTRY_STATE_FULL +} texture_entry_state_t; + +/** @brief A single, possibly empty, shared texture */ +typedef struct texture_entry_s { + char *path; ///< Original file path of a texture, used as a key + texture_entry_state_t state; ///< Is only the sprite loaded or also a GL texture object + sprite_t *sprite; ///< A sprite with image data + GLuint obj; ///< Texture object created on first draw + int ref_count; ///< Reference count from all models +} texture_entry_t; + +/** @brief Contains shared textures and their metadata */ +typedef struct texture_table_s { + texture_entry_t *entries; ///< Path to entry mapping with empty slots + uint32_t size; ///< Number of elements allocated for the array above + int ref_count; ///< How many models have shared textures +} texture_table_t; + +/** @brief Global shared texture table */ +static texture_table_t* shared_textures; + +/** @brief Allocates the global texture table */ +void texture_table_allocate() +{ + shared_textures = calloc(1, sizeof(texture_table_t)); + assertf(shared_textures, "Out of memory"); + shared_textures->size = 2; + shared_textures->entries = calloc(shared_textures->size, sizeof(shared_textures->entries[0])); + assertf(shared_textures->entries, "Out of memory"); + shared_textures->ref_count = 1; +} + +/** @brief Frees a texture entry and its resources */ +void free_texture_entry(texture_entry_t *entry) { + assertf(entry->ref_count == 0, "Leaked a texture entry %p", entry); + free(entry->path); + sprite_free(entry->sprite); + if (entry->state == ENTRY_STATE_FULL) { + glDeleteTextures(1, &entry->obj); + } + entry->path = NULL; + entry->state = ENTRY_STATE_EMPTY; + entry->sprite = NULL; +} + +/** @brief Frees the global texture table */ +void texture_table_free() +{ + assertf(shared_textures->ref_count == 0, "Tried freeing texture table while still in use"); + + for (uint32_t i = 0; i < shared_textures->size; i++) { + texture_entry_t *entry = &shared_textures->entries[i]; + assertf(entry->state == ENTRY_STATE_EMPTY, "Shared texture %lu=%p was leaked", i, entry); + } + free(shared_textures->entries); + free(shared_textures); + shared_textures = NULL; +} + +/** @brief Gets texture index by path */ +uint32_t texture_table_get(const char* path) +{ + for (uint32_t i = 0; i < shared_textures->size; i++) { + texture_entry_t *entry = &shared_textures->entries[i]; + if (entry->state != ENTRY_STATE_EMPTY) { + if (strcmp(shared_textures->entries[i].path, path) == 0) { + return i; + } + } + } + + return TEXTURE_INDEX_MISSING; +} + +/** @brief Adds a texture to the shared table */ +uint32_t texture_table_add(const char* path, const char* prefix) +{ + uint32_t idx = TEXTURE_INDEX_MISSING; + + for (uint32_t i = 0; i < shared_textures->size; i++) { + texture_entry_t *entry = &shared_textures->entries[i]; + if (entry->state == ENTRY_STATE_EMPTY) { + idx = i; + break; + } + } + + if (idx == TEXTURE_INDEX_MISSING) { + // Table must be full because a free slot wasn't found. + uint32_t new_size = shared_textures->size * 2; + shared_textures->entries = realloc(shared_textures->entries, new_size * sizeof(shared_textures->entries[0])); + assertf(shared_textures->entries, "Out of memory"); + + for (uint32_t i = shared_textures->size; i < new_size; i++) { + memset(&shared_textures->entries[i], 0, sizeof(shared_textures->entries[i])); + } + + idx = shared_textures->size; + shared_textures->size = new_size; + } + + char prefixed[strlen(prefix) + strlen(path) + 1]; + prefixed[0] = '\0'; + strcat(prefixed, prefix); + strcat(prefixed, path); + + sprite_t *sprite = sprite_load(prefixed); + + if (!sprite) { + assertf(false, "Failed to load texture %s\n", prefixed); + return TEXTURE_INDEX_MISSING; + } + + size_t size = strlen(path) + 1; + char *new = malloc(size); + assertf(new, "Out of memory"); + strncpy(new, path, size); + + texture_entry_t* entry = &shared_textures->entries[idx]; + entry->path = new; + entry->sprite = sprite; + entry->ref_count = 0; // caller will increment ref_count if it stored a reference + entry->state = ENTRY_STATE_SPRITE_LOADED; // entry->obj gets initialized on first draw + + return idx; +} + +/** @brief Increments reference count for a texture */ +void texture_table_inc_ref_count(uint32_t idx) +{ + if (idx == TEXTURE_INDEX_MISSING) return; + assert(idx < shared_textures->size); + assert(shared_textures->entries[idx].ref_count >= 0); + shared_textures->entries[idx].ref_count++; +} + +/** @brief Decrements reference count for a texture */ +void texture_table_dec_ref_count(uint32_t idx) +{ + if (idx == TEXTURE_INDEX_MISSING) return; + + assert(idx < shared_textures->size); + + texture_entry_t* entry = &shared_textures->entries[idx]; + assert(entry->ref_count > 0); + + if (--entry->ref_count == 0) { + free_texture_entry(entry); + } +} + +/** @brief Decodes a pointer relative to model base address */ +#define PTR_DECODE(model, ptr) ((void*)(((uint8_t*)(model)) + (uint32_t)(ptr))) +/** @brief Encodes a pointer relative to model base address */ +#define PTR_ENCODE(model, ptr) ((void*)(((uint8_t*)(ptr)) - (uint32_t)(model))) + +static model64_data_t *load_model_data_buf(void *buf, int sz, const char* prefix) +{ + model64_data_t *model = buf; + assertf(sz >= sizeof(model64_data_t), "Model buffer too small (sz=%d)", sz); + if(model->magic == MODEL64_MAGIC_LOADED) { + assertf(0, "Trying to load already loaded model data (buf=%p, sz=%08x)", buf, sz); + } + assertf(model->magic == MODEL64_MAGIC, "invalid model data (magic: %08lx)", model->magic); + model->nodes = PTR_DECODE(model, model->nodes); + model->meshes = PTR_DECODE(model, model->meshes); + model->skins = PTR_DECODE(model, model->skins); + model->anims = PTR_DECODE(model, model->anims); + model->texture_paths = PTR_DECODE(model, model->texture_paths); + for(uint32_t i=0; inum_skins; i++) + { + model->skins[i].joints = PTR_DECODE(model, model->skins[i].joints); + } + for(uint32_t i=0; inum_nodes; i++) + { + if(model->nodes[i].name) + { + model->nodes[i].name = PTR_DECODE(model, model->nodes[i].name); + } + if(model->nodes[i].mesh) + { + model->nodes[i].mesh = PTR_DECODE(model, model->nodes[i].mesh); + } + model->nodes[i].children = PTR_DECODE(model, model->nodes[i].children); + if(model->nodes[i].skin) + { + model->nodes[i].skin = PTR_DECODE(model, model->nodes[i].skin); + } + } + if (model->num_textures > 0) { + assertf(prefix, "Trying to load a textured model from memory, only file system supported"); + if (shared_textures) { + shared_textures->ref_count++; + } else { + texture_table_allocate(); + } + } + for (uint32_t i = 0; i < model->num_textures; i++) + { + model->texture_paths[i] = PTR_DECODE(model, model->texture_paths[i]); + } + for (uint32_t i = 0; i < model->num_meshes; i++) + { + model->meshes[i].primitives = PTR_DECODE(model, model->meshes[i].primitives); + for (uint32_t j = 0; j < model->meshes[i].num_primitives; j++) + { + primitive_t *primitive = &model->meshes[i].primitives[j]; + primitive->position.pointer = PTR_DECODE(model, primitive->position.pointer); + primitive->color.pointer = PTR_DECODE(model, primitive->color.pointer); + primitive->texcoord.pointer = PTR_DECODE(model, primitive->texcoord.pointer); + primitive->normal.pointer = PTR_DECODE(model, primitive->normal.pointer); + primitive->mtx_index.pointer = PTR_DECODE(model, primitive->mtx_index.pointer); + primitive->indices = PTR_DECODE(model, primitive->indices); + + if (primitive->local_texture != TEXTURE_INDEX_MISSING) { + uint32_t idx = texture_table_get(model->texture_paths[primitive->local_texture]); + + if (idx == TEXTURE_INDEX_MISSING) { + idx = texture_table_add(model->texture_paths[primitive->local_texture], prefix); + } + + assert(idx != TEXTURE_INDEX_MISSING); + + primitive->shared_texture = idx; + texture_table_inc_ref_count(idx); + } + } + } + for (uint32_t i = 0; i < model->num_anims; i++) + { + if(model->anims[i].name) + { + model->anims[i].name = PTR_DECODE(model, model->anims[i].name); + } + model->anims[i].tracks = PTR_DECODE(model, model->anims[i].tracks); + if(!model->anim_data_handle) + { + model->anims[i].keyframes = PTR_DECODE(model, model->anims[i].keyframes); + } + } + + model->magic = MODEL64_MAGIC_LOADED; + model->ref_count = 1; + data_cache_hit_writeback(model, sz); + return model; +} + +static size_t get_model_instance_size(model64_data_t *model_data) +{ + return sizeof(model64_t)+(model_data->num_nodes*sizeof(node_transform_state_t)); +} + +static void multiply_node_mtx(float parent[16], float child[16]) +{ + + for (int i = 0; i < 4; ++i) + { + float l0 = child[i * 4 + 0]; + float l1 = child[i * 4 + 1]; + float l2 = child[i * 4 + 2]; + + float r0 = l0 * parent[0] + l1 * parent[4] + l2 * parent[8]; + float r1 = l0 * parent[1] + l1 * parent[5] + l2 * parent[9]; + float r2 = l0 * parent[2] + l1 * parent[6] + l2 * parent[10]; + + child[i * 4 + 0] = r0; + child[i * 4 + 1] = r1; + child[i * 4 + 2] = r2; + } + + child[12] += parent[12]; + child[13] += parent[13]; + child[14] += parent[14]; +} + +static void mtx_copy(float dst[16], float src[16]) +{ + memcpy(dst, src, 16*sizeof(float)); +} + +static void transform_calc_matrix(node_transform_t *transform) +{ + float tx = transform->pos[0]; + float ty = transform->pos[1]; + float tz = transform->pos[2]; + + float qx = transform->rot[0]; + float qy = transform->rot[1]; + float qz = transform->rot[2]; + float qw = transform->rot[3]; + + float sx = transform->scale[0]; + float sy = transform->scale[1]; + float sz = transform->scale[2]; + + transform->mtx[0] = (1 - 2 * qy*qy - 2 * qz*qz) * sx; + transform->mtx[1] = (2 * qx*qy + 2 * qz*qw) * sx; + transform->mtx[2] = (2 * qx*qz - 2 * qy*qw) * sx; + transform->mtx[3] = 0.f; + + transform->mtx[4] = (2 * qx*qy - 2 * qz*qw) * sy; + transform->mtx[5] = (1 - 2 * qx*qx - 2 * qz*qz) * sy; + transform->mtx[6] = (2 * qy*qz + 2 * qx*qw) * sy; + transform->mtx[7] = 0.f; + + transform->mtx[8] = (2 * qx*qz + 2 * qy*qw) * sz; + transform->mtx[9] = (2 * qy*qz - 2 * qx*qw) * sz; + transform->mtx[10] = (1 - 2 * qx*qx - 2 * qy*qy) * sz; + transform->mtx[11] = 0.f; + + transform->mtx[12] = tx; + transform->mtx[13] = ty; + transform->mtx[14] = tz; + transform->mtx[15] = 1.f; +} + +static void calc_node_local_matrix(model64_t *model, uint32_t node) +{ + transform_calc_matrix(&model->transforms[node].transform); +} + +static void calc_node_world_matrix(model64_t *model, uint32_t node) +{ + model64_node_t *node_ptr = model64_get_node(model, node); + node_transform_state_t *xform = &model->transforms[node]; + mtx_copy(xform->world_mtx, xform->transform.mtx); + uint32_t parent_node = node_ptr->parent; + + while(parent_node != model->data->num_nodes) { + model64_node_t *parent_ptr = model64_get_node(model, parent_node); + node_transform_state_t *parent_xform = &model->transforms[parent_node]; + multiply_node_mtx(parent_xform->transform.mtx, xform->world_mtx); + parent_node = parent_ptr->parent; + } +} +static void update_node_matrices(model64_t *model) +{ + for(uint32_t i=0; idata->num_nodes; i++) { + calc_node_world_matrix(model, i); + } +} + +static void init_model_transforms(model64_t *model) +{ + for(uint32_t i=0; idata->num_nodes; i++) { + model->transforms[i].transform = model->data->nodes[i].transform; + calc_node_local_matrix(model, i); + } + update_node_matrices(model); +} + +static model64_t *make_model_instance(model64_data_t *model_data) +{ + model64_t *instance = calloc(1, get_model_instance_size(model_data)); + assertf(instance, "Out of memory"); + instance->data = model_data; + instance->transforms = (node_transform_state_t *)&instance[1]; + init_model_transforms(instance); + return instance; +} + +static model64_data_t *load_model64_data_buf(void *buf, int sz) +{ + model64_data_t *data = load_model_data_buf(buf, sz, NULL); + assertf(!data->anim_data_handle, "Streaming animations not supported when loading model from buffer"); + return data; +} + +static model64_data_t *load_model64_data(const char *fn) +{ + int sz; + void *buf = asset_load(fn, &sz); + + // Path will have at least a file system prefix so a slash will be found. + char* slash = strrchr(fn, '/'); + char prefix[slash - fn + 2]; + memcpy(prefix, fn, slash - fn + 1); + prefix[sizeof(prefix)-1] = '\0'; + + model64_data_t *data = load_model_data_buf(buf, sz, prefix); + if(data->anim_data_handle) { + assertf(strncmp(fn, "rom:/", 5) == 0, "Cannot open %s: models with streamed animations must be stored in ROM (rom:/)", fn); + char anim_name[strlen(fn)+6]; + sprintf(anim_name, "%s.anim", fn); + data->anim_data_handle = (void *)dfs_rom_addr(anim_name+5); + } + return data; +} + +/** @brief Loads model from buffer + * @preview + */ +model64_t *model64_load_buf(void *buf, int sz) +{ + model64_data_t *data = load_model64_data_buf(buf, sz); + return make_model_instance(data); +} + +/** @brief Loads model from file + * @preview + */ +model64_t *model64_load(const char *fn) +{ + model64_data_t *data = load_model64_data(fn); + return make_model_instance(data); +} + +/** @brief Clones a model instance + * @preview + */ +model64_t *model64_clone(model64_t *model) +{ + model->data->ref_count++; + return make_model_instance(model->data); +} + +/** @brief Unloads model data and encodes pointers */ +static void unload_model_data(model64_data_t *model) +{ + for(uint32_t i=0; inum_nodes; i++) + { + model->nodes[i].children = PTR_ENCODE(model, model->nodes[i].children); + if(model->nodes[i].skin) + { + model->nodes[i].skin = PTR_ENCODE(model, model->nodes[i].skin); + } + if(model->nodes[i].mesh) + { + model->nodes[i].mesh = PTR_ENCODE(model, model->nodes[i].mesh); + } + if(model->nodes[i].name) + { + model->nodes[i].name = PTR_ENCODE(model, model->nodes[i].name); + } + } + for (uint32_t i = 0; i < model->num_meshes; i++) + { + for (uint32_t j = 0; j < model->meshes[i].num_primitives; j++) + { + primitive_t *primitive = &model->meshes[i].primitives[j]; + primitive->position.pointer = PTR_ENCODE(model, primitive->position.pointer); + primitive->color.pointer = PTR_ENCODE(model, primitive->color.pointer); + primitive->texcoord.pointer = PTR_ENCODE(model, primitive->texcoord.pointer); + primitive->normal.pointer = PTR_ENCODE(model, primitive->normal.pointer); + primitive->mtx_index.pointer = PTR_ENCODE(model, primitive->mtx_index.pointer); + primitive->indices = PTR_ENCODE(model, primitive->indices); + texture_table_dec_ref_count(primitive->shared_texture); + primitive->shared_texture = TEXTURE_INDEX_MISSING; + } + model->meshes[i].primitives = PTR_ENCODE(model, model->meshes[i].primitives); + } + for(uint32_t i=0; inum_skins; i++) + { + model->skins[i].joints = PTR_ENCODE(model, model->skins[i].joints); + } + for (uint32_t i = 0; i < model->num_anims; i++) + { + if(model->anims[i].name) + { + model->anims[i].name = PTR_ENCODE(model, model->anims[i].name); + } + model->anims[i].tracks = PTR_ENCODE(model, model->anims[i].tracks); + if(!model->anim_data_handle) + { + model->anims[i].keyframes = PTR_ENCODE(model, model->anims[i].keyframes); + } + } + model->nodes = PTR_ENCODE(model, model->nodes); + model->meshes = PTR_ENCODE(model, model->meshes); + model->skins = PTR_ENCODE(model, model->skins); + model->anims = PTR_ENCODE(model, model->anims); + if(model->magic == MODEL64_MAGIC_OWNED) { + #ifndef NDEBUG + // To help debugging, zero the model data structure + memset(model, 0, sizeof(model64_data_t)); + #endif + + free(model); + } +} + +static void free_model64_data(model64_data_t *data) +{ + if(--data->ref_count == 0) + { + bool had_textures = data->num_textures > 0; + unload_model_data(data); + if (had_textures) { + if (--shared_textures->ref_count == 0) { + texture_table_free(); + } + } + } +} + +/** @brief Frees a model instance and its data + * @preview + */ +void model64_free(model64_t *model) +{ + for(int i=0; iactive_anims[i]) { + free(model->active_anims[i]); + } + } + free_model64_data(model->data); + free(model); +} + +/** @brief Gets the number of meshes in a model */ +uint32_t model64_get_mesh_count(model64_t *model) +{ + return model->data->num_meshes; +} + +/** @brief Gets a mesh by index */ +mesh_t *model64_get_mesh(model64_t *model, uint32_t mesh_index) +{ + return &model->data->meshes[mesh_index]; +} + +/** @brief Gets the number of nodes in a model */ +uint32_t model64_get_node_count(model64_t *model) +{ + return model->data->num_nodes; +} + +/** @brief Gets a node by index */ +model64_node_t *model64_get_node(model64_t *model, uint32_t node_index) +{ + return &model->data->nodes[node_index]; +} + +/** @brief Searches for a node by name */ +model64_node_t *model64_search_node(model64_t *model, const char *name) +{ + for(uint32_t i=0; idata->num_nodes; i++) + { + char *node_name = model->data->nodes[i].name; + if(node_name && !strcmp(node_name, name)) + { + return &model->data->nodes[i]; + } + } + return NULL; +} + +/** @brief Gets the index of a node */ +static uint32_t get_node_idx(model64_t *model, model64_node_t *node) +{ + return node - model->data->nodes; +} + +/** @brief Sets the position of a node */ +void model64_set_node_pos(model64_t *model, model64_node_t *node, float x, float y, float z) +{ + uint32_t node_idx = get_node_idx(model, node); + assertf(node_idx < model->data->num_nodes, "Setting position of invalid node."); + float *dst = model->transforms[node_idx].transform.pos; + dst[0] = x; + dst[1] = y; + dst[2] = z; + calc_node_local_matrix(model, node_idx); + update_node_matrices(model); +} + +/** @brief Sets the rotation of a node using Euler angles */ +void model64_set_node_rot(model64_t *model, model64_node_t *node, float x, float y, float z) +{ + float cr, sr, cp, sp, cy, sy; + fm_sincosf(x*0.5f, &sr, &cr); + fm_sincosf(y*0.5f, &sp, &cp); + fm_sincosf(z*0.5f, &sy, &cy); + float w = cr * cp * cy + sr * sp * sy; + x = sr * cp * cy - cr * sp * sy; + y = cr * sp * cy + sr * cp * sy; + z = cr * cp * sy - sr * sp * cy; + model64_set_node_rot_quat(model, node, x, y, z, w); +} + +/** @brief Sets the rotation of a node using quaternion */ +void model64_set_node_rot_quat(model64_t *model, model64_node_t *node, float x, float y, float z, float w) +{ + uint32_t node_idx = get_node_idx(model, node); + assertf(node_idx < model->data->num_nodes, "Setting rotation of invalid node."); + float *dst = model->transforms[node_idx].transform.rot; + dst[0] = x; + dst[1] = y; + dst[2] = z; + dst[3] = w; + calc_node_local_matrix(model, node_idx); + update_node_matrices(model); +} + +/** @brief Sets the scale of a node */ +void model64_set_node_scale(model64_t *model, model64_node_t *node, float x, float y, float z) +{ + uint32_t node_idx = get_node_idx(model, node); + assertf(node_idx < model->data->num_nodes, "Setting scale of invalid node."); + float *dst = model->transforms[node_idx].transform.scale; + dst[0] = x; + dst[1] = y; + dst[2] = z; + calc_node_local_matrix(model, node_idx); + update_node_matrices(model); +} + +/** @brief Gets the world matrix of a node */ +void model64_get_node_world_mtx(model64_t *model, model64_node_t *node, float dst[16]) +{ + uint32_t node_idx = get_node_idx(model, node); + assertf(node_idx < model->data->num_nodes, "Grabbing world matrix of invalid node."); + mtx_copy(dst, model->transforms[node_idx].world_mtx); +} + +/** @brief Gets the number of primitives in a mesh */ +uint32_t model64_get_primitive_count(mesh_t *mesh) +{ + return mesh->num_primitives; +} + +/** @brief Gets a primitive by index */ +primitive_t *model64_get_primitive(mesh_t *mesh, uint32_t primitive_index) +{ + return &mesh->primitives[primitive_index]; +} + +/** @brief Draws a single primitive */ +void model64_draw_primitive(primitive_t *primitive) +{ + if (primitive->shared_texture != TEXTURE_INDEX_MISSING) { + texture_entry_t *entry = &shared_textures->entries[primitive->shared_texture]; + + if (entry->state == ENTRY_STATE_SPRITE_LOADED) { + glGenTextures(1, &entry->obj); + glBindTexture(GL_TEXTURE_2D, entry->obj); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + + // If a dimension is not a power of two then clamp and otherwise repeat. + float rs = (entry->sprite->width & (entry->sprite->width-1)) ? 1 : REPEAT_INFINITE; + float rt = (entry->sprite->height & (entry->sprite->height-1)) ? 1 : REPEAT_INFINITE; + glSpriteTextureN64(GL_TEXTURE_2D, entry->sprite, &(rdpq_texparms_t){.s.repeats = rs, .t.repeats = rt}); + + entry->state = ENTRY_STATE_FULL; + } + + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, entry->obj); + } + + if (primitive->position.size > 0) { + glEnableClientState(GL_VERTEX_ARRAY); + if (primitive->position.type == GL_HALF_FIXED_N64) { + glVertexHalfFixedPrecisionN64(primitive->vertex_precision); + } + glVertexPointer(primitive->position.size, primitive->position.type, primitive->position.stride, primitive->position.pointer); + } else { + glDisableClientState(GL_VERTEX_ARRAY); + } + + if (primitive->color.size > 0) { + glEnableClientState(GL_COLOR_ARRAY); + glColorPointer(primitive->color.size, primitive->color.type, primitive->color.stride, primitive->color.pointer); + } else { + glDisableClientState(GL_COLOR_ARRAY); + } + + if (primitive->texcoord.size > 0) { + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + if (primitive->texcoord.type == GL_HALF_FIXED_N64) { + glTexCoordHalfFixedPrecisionN64(primitive->texcoord_precision); + } + glTexCoordPointer(primitive->texcoord.size, primitive->texcoord.type, primitive->texcoord.stride, primitive->texcoord.pointer); + } else { + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + } + + if (primitive->normal.size > 0) { + glEnableClientState(GL_NORMAL_ARRAY); + glNormalPointer(primitive->normal.type, primitive->normal.stride, primitive->normal.pointer); + } else { + glDisableClientState(GL_NORMAL_ARRAY); + } + + if (primitive->mtx_index.size > 0) { + glEnableClientState(GL_MATRIX_INDEX_ARRAY_ARB); + glMatrixIndexPointerARB(primitive->mtx_index.size, primitive->mtx_index.type, primitive->mtx_index.stride, primitive->mtx_index.pointer); + } else { + glDisableClientState(GL_MATRIX_INDEX_ARRAY_ARB); + } + + if (primitive->num_indices > 0) { + glDrawElements(primitive->mode, primitive->num_indices, primitive->index_type, primitive->indices); + } else { + glDrawArrays(primitive->mode, 0, primitive->num_vertices); + } +} + +/** @brief Draws all primitives in a mesh */ +void model64_draw_mesh(mesh_t *mesh) +{ + for (uint32_t i = 0; i < model64_get_primitive_count(mesh); i++) + { + model64_draw_primitive(model64_get_primitive(mesh, i)); + } +} + +/** @brief Draws a node and its mesh */ +void model64_draw_node(model64_t *model, model64_node_t *node) +{ + uint32_t node_idx = get_node_idx(model, node); + assertf(node_idx < model->data->num_nodes, "Drawing invalid node."); + if(node->mesh) + { + if(node->skin) + { + glMatrixMode(GL_MATRIX_PALETTE_ARB); + for(uint32_t i=0; iskin->num_joints; i++) + { + glCurrentPaletteMatrixARB(i); + glCopyMatrixN64(GL_MODELVIEW); //Copy matrix at top of modelview stack to matrix palette + glMultMatrixf(model->transforms[node->skin->joints[i].node_idx].world_mtx); + glMultMatrixf(node->skin->joints[i].inverse_bind_mtx); + } + glEnable(GL_MATRIX_PALETTE_ARB); + model64_draw_mesh(node->mesh); + glDisable(GL_MATRIX_PALETTE_ARB); + glMatrixMode(GL_MODELVIEW); + } + else + { + glMatrixMode(GL_MODELVIEW); + glPushMatrix(); + glMultMatrixf(model->transforms[node_idx].world_mtx); + model64_draw_mesh(node->mesh); + glPopMatrix(); + } + } +} + +/** @brief Draws all nodes in a model */ +void model64_draw(model64_t *model) +{ + for (uint32_t i = 0; i < model64_get_node_count(model); i++) + { + model64_draw_node(model, model64_get_node(model, i)); + } +} + +static int32_t search_anim_index(model64_t *model, const char *name) +{ + if(!name) { + return -1; + } + for(uint32_t i=0; idata->num_anims; i++) { + if(model->data->anims[i].name && !strcmp(name, model->data->anims[i].name)) { + return i; + } + } + return -1; +} + +static void alloc_anim_slot(model64_t *model, model64_anim_slot_t slot) +{ + uint32_t state_size = sizeof(anim_state_t); + uint32_t num_tracks = model->data->max_tracks; + state_size += (num_tracks*4)*sizeof(decoded_keyframe_t); + state_size += sizeof(model64_keyframe_t); + anim_state_t *anim_state = calloc(1, state_size); + assertf(anim_state, "Out of memory"); + anim_state->frames = PTR_DECODE(anim_state, sizeof(anim_state_t)); + anim_state->curr_frame = PTR_DECODE(anim_state->frames, (num_tracks*4)*sizeof(decoded_keyframe_t)); + anim_state->index = -1; + anim_state->paused = true; + anim_state->speed = 1.0f; + model->active_anims[slot] = anim_state; +} + + +static bool is_anim_slot_valid(model64_anim_slot_t slot) +{ + return (slot >= 0) && (slot < MAX_ACTIVE_ANIMS); +} + +/** @brief Plays an animation in a slot + * @preview + */ +void model64_anim_play(model64_t *model, const char *anim, model64_anim_slot_t slot, bool paused, float start_time) +{ + int32_t anim_index = search_anim_index(model, anim); + assertf(anim_index != -1, "Animation %s was not found\n", anim); + assertf(start_time >= 0, "Start time must be strictly non-negative"); + if(!model->active_anims[slot]) { + alloc_anim_slot(model, slot); + } + model->active_anims[slot]->index = anim_index; + model->active_anims[slot]->paused = paused; + model->active_anims[slot]->time = start_time; + model->active_anims[slot]->loop = false; + model->active_anims[slot]->invalid_pose = true; + model->active_anims[slot]->prev_waiting_frame = false; + model->active_anims[slot]->speed = 1.0f; +} + +/** @brief Stops an animation in a slot + * @preview + */ +void model64_anim_stop(model64_t *model, model64_anim_slot_t slot) +{ + assertf(is_anim_slot_valid(slot), "Invalid animation ID"); + if(!model->active_anims[slot]) { + alloc_anim_slot(model, slot); + } + model->active_anims[slot]->index = -1; +} + +/** @brief Gets the length of an animation + * @preview + */ +float model64_anim_get_length(model64_t *model, const char *anim) +{ + int32_t anim_index = search_anim_index(model, anim); + assertf(anim_index != -1, "Animation %s was not found\n", anim); + return (float)model->data->anims[anim_index].duration; +} + +/** @brief Gets the current time of an animation slot + * @preview + */ +float model64_anim_get_time(model64_t *model, model64_anim_slot_t slot) +{ + assertf(is_anim_slot_valid(slot), "Invalid animation ID"); + if(!model->active_anims[slot]) { + alloc_anim_slot(model, slot); + } + return model->active_anims[slot]->time; +} + +/** @brief Sets the time of an animation slot + * @preview + */ +float model64_anim_set_time(model64_t *model, model64_anim_slot_t slot, float time) +{ + assertf(is_anim_slot_valid(slot), "Invalid animation ID"); + assertf(time >= 0, "Time must be strictly non-negative"); + if(!model->active_anims[slot]) { + alloc_anim_slot(model, slot); + } + float old_time = model->active_anims[slot]->time; + if(time < old_time) { + model->active_anims[slot]->invalid_pose = true; + } + model->active_anims[slot]->time = time; + return old_time; +} + +/** @brief Sets the speed of an animation slot + * @preview + */ +float model64_anim_set_speed(model64_t *model, model64_anim_slot_t slot, float speed) +{ + assertf(is_anim_slot_valid(slot), "Invalid animation ID"); + assertf(speed >= 0, "Speed cannot be negative"); + if(!model->active_anims[slot]) { + alloc_anim_slot(model, slot); + } + float old_speed = model->active_anims[slot]->speed; + model->active_anims[slot]->speed = speed; + return old_speed; +} + +/** @brief Sets the loop state of an animation slot + * @preview + */ +bool model64_anim_set_loop(model64_t *model, model64_anim_slot_t slot, bool loop) +{ + assertf(is_anim_slot_valid(slot), "Invalid animation ID"); + if(!model->active_anims[slot]) { + alloc_anim_slot(model, slot); + } + bool old_loop = model->active_anims[slot]->loop; + model->active_anims[slot]->loop = loop; + return old_loop; +} + +/** @brief Sets the pause state of an animation slot + * @preview + */ +bool model64_anim_set_pause(model64_t *model, model64_anim_slot_t slot, bool paused) +{ + assertf(is_anim_slot_valid(slot), "Invalid animation ID"); + if(!model->active_anims[slot]) { + alloc_anim_slot(model, slot); + } + bool old_pause = model->active_anims[slot]->paused; + model->active_anims[slot]->paused = paused; + return old_pause; +} + +static void vec_normalize(float *vec, size_t count) +{ + float mag2 = 0.0f; + for(size_t i=0; i= 0) ? 1.0f : -1.0f; + for(size_t i=0; i<4; i++) { + out[i] = ((1-time)*in1[i])+(out_scale*time*in2[i]); + } + vec_normalize(out, 4); +} + +static void decode_normalized_u16(uint16_t *in, float *out, size_t count, float f1, float f2) +{ + for(size_t i=0; i>15) | (in[1]>>15<<1); + float decoded_values[3]; + for(size_t i=0; i<3; i++) { + decoded_values[i] = (values[i]*0.000043159689f)-0.70710678f; + } + int16_t sign = (int16_t)in[2]; + float mag2_decoded = (decoded_values[0]*decoded_values[0])+(decoded_values[1]*decoded_values[1])+(decoded_values[2]*decoded_values[2]); + float derived_component = copysignf(sqrtf(1.0f - mag2_decoded), (float)sign); + int src_component = 0; + for(size_t i=0; i<4; i++) { + if(i == max_component) { + out[i] = derived_component; + } else { + out[i] = decoded_values[src_component]; + src_component++; + } + } +} + +static void decode_cur_keyframe(model64_t *model, model64_anim_slot_t anim_slot) +{ + anim_state_t *anim_state = model->active_anims[anim_slot]; + model64_anim_t *curr_anim = &model->data->anims[anim_state->index]; + uint16_t track = anim_state->curr_frame->track; + decoded_keyframe_t keyframe; + keyframe.time = anim_state->curr_frame->time; + switch(curr_anim->tracks[track] >> 14) { + case ANIM_COMPONENT_POS: + decode_normalized_u16(anim_state->curr_frame->data, keyframe.data, 3, curr_anim->pos_f1, curr_anim->pos_f2); + keyframe.data[3] = 0.0f; + break; + + case ANIM_COMPONENT_ROT: + decode_quaternion(anim_state->curr_frame->data, keyframe.data); + break; + + case ANIM_COMPONENT_SCALE: + decode_normalized_u16(anim_state->curr_frame->data, keyframe.data, 3, curr_anim->scale_f1, curr_anim->scale_f2); + keyframe.data[3] = 0.0f; + break; + + default: + keyframe.data[0] = keyframe.data[1] = keyframe.data[2] = keyframe.data[3] = 0.0f; + break; + } + anim_state->frames[track*4] = anim_state->frames[(track*4)+1]; + anim_state->frames[(track*4)+1] = anim_state->frames[(track*4)+2]; + anim_state->frames[(track*4)+2] = anim_state->frames[(track*4)+3]; + anim_state->frames[(track*4)+3] = keyframe; +} + +static bool read_keyframe(model64_t *model, model64_anim_slot_t anim_slot) +{ + anim_state_t *anim_state = model->active_anims[anim_slot]; + model64_anim_t *curr_anim = &model->data->anims[anim_state->index]; + if(anim_state->prev_waiting_frame) { + if(!anim_state->done_decoding) { + decode_cur_keyframe(model, anim_slot); + } + } else { + anim_state->prev_waiting_frame = true; + } + if(anim_state->frame_idx >= curr_anim->num_keyframes) { + anim_state->done_decoding = true; + return false; + } + if(model->data->anim_data_handle) { + uint32_t rom_addr = (uint32_t)model->data->anim_data_handle; + rom_addr += (uint32_t)curr_anim->keyframes; + rom_addr += anim_state->frame_idx*sizeof(model64_keyframe_t); + data_cache_hit_writeback_invalidate(anim_state->curr_frame, sizeof(model64_keyframe_t)); + dma_read(anim_state->curr_frame, rom_addr, sizeof(model64_keyframe_t)); + } else { + memcpy(anim_state->curr_frame, &curr_anim->keyframes[anim_state->frame_idx], sizeof(model64_keyframe_t)); + } + anim_state->frame_idx++; + return true; +} + +static void init_keyframes(model64_t *model, model64_anim_slot_t anim_slot) +{ + anim_state_t *anim_state = model->active_anims[anim_slot]; + model64_anim_t *curr_anim = &model->data->anims[anim_state->index]; + uint32_t num_tracks = curr_anim->num_tracks; + anim_state->frame_idx = 0; + anim_state->prev_waiting_frame = false; + anim_state->done_decoding = false; + for(size_t i=0; i<(num_tracks*4)+1; i++) { + read_keyframe(model, anim_slot); + } +} + +static void fetch_needed_keyframes(model64_t *model, model64_anim_slot_t anim_slot) +{ + anim_state_t *anim_state = model->active_anims[anim_slot]; + while(anim_state->curr_frame->time_req < anim_state->time) { + if(!read_keyframe(model, anim_slot)) { + return; + } + } +} + +static void calc_anim_pose(model64_t *model, model64_anim_slot_t anim_slot) +{ + anim_state_t *anim_state = model->active_anims[anim_slot]; + model64_anim_t *curr_anim = &model->data->anims[anim_state->index]; + for(uint32_t i=0; inum_tracks; i++) { + decoded_keyframe_t *curr_frame = &anim_state->frames[i*4]; + uint32_t component = curr_anim->tracks[i] >> 14; + uint16_t node = curr_anim->tracks[i] & 0x3FFF; + float time = curr_frame[1].time; + float time_next = curr_frame[2].time; + float weight = (time == time_next) ? 0 : (anim_state->time-time)/(time_next-time); + assert(weight <= 1.0f && weight >= 0.0f); + float *out; + size_t out_count; + switch(component) { + case ANIM_COMPONENT_POS: + out = model->transforms[node].transform.pos; + out_count = 3; + break; + + case ANIM_COMPONENT_ROT: + out = model->transforms[node].transform.rot; + out_count = 4; + break; + + case ANIM_COMPONENT_SCALE: + out = model->transforms[node].transform.scale; + out_count = 3; + break; + + default: + out = NULL; + break; + } + if(out == NULL) { + continue; + } + if(component == ANIM_COMPONENT_ROT) { + quat_lerp(out, curr_frame[1].data, curr_frame[2].data, weight); + } else { + catmull_calc_vec(curr_frame[0].data, curr_frame[1].data, curr_frame[2].data, curr_frame[3].data, out, weight, out_count); + } + if(i == curr_anim->num_tracks-1 || (curr_anim->tracks[i+1] & 0x3FFF) != node) { + calc_node_local_matrix(model, node); + } + } +} + +/** @brief Updates model animations and transforms */ +void model64_update(model64_t *model, float deltatime) +{ + assertf(deltatime >= 0, "Delta time must not be negative"); + for(int i=0; iactive_anims[i]) { + continue; + } + if(model->active_anims[i]->index == -1 || model->active_anims[i]->paused || model->active_anims[i]->speed == 0) { + if(model->active_anims[i]->index != -1 && model->active_anims[i]->invalid_pose) { + model64_anim_t *curr_anim = &model->data->anims[model->active_anims[i]->index]; + if(model->active_anims[i]->time >= curr_anim->duration) { + model->active_anims[i]->time = curr_anim->duration; + } + init_keyframes(model, i); + fetch_needed_keyframes(model, i); + calc_anim_pose(model, i); + model->active_anims[i]->invalid_pose = false; + } + continue; + } + model64_anim_t *curr_anim = &model->data->anims[model->active_anims[i]->index]; + model->active_anims[i]->time += model->active_anims[i]->speed*deltatime; + if(model->active_anims[i]->time >= curr_anim->duration) { + if(model->active_anims[i]->loop) { + while(model->active_anims[i]->time >= curr_anim->duration) { + model->active_anims[i]->time -= curr_anim->duration; + } + model->active_anims[i]->invalid_pose = true; + } else { + model->active_anims[i]->time = curr_anim->duration; + model->active_anims[i]->paused = true; + } + } + if(model->active_anims[i]->invalid_pose) { + init_keyframes(model, i); + model->active_anims[i]->invalid_pose = false; + } + fetch_needed_keyframes(model, i); + calc_anim_pose(model, i); + } + update_node_matrices(model); +} \ No newline at end of file diff --git a/src/model64_catmull.h b/src/model64_catmull.h new file mode 100644 index 000000000..a883f8423 --- /dev/null +++ b/src/model64_catmull.h @@ -0,0 +1,24 @@ +/** + * @file model64_catmull.h + * @author gamemasterplc + */ +#ifndef __LIBDRAGON_MODEL64_CATMULL_H +#define __LIBDRAGON_MODEL64_CATMULL_H + +static float catmull_calc(float p1, float p2, float p3, float p4, float t) +{ + float a = (-1*p1 +3*p2 -3*p3 + 1*p4) * t*t*t; + float b = ( 2*p1 -5*p2 +4*p3 - 1*p4) * t*t; + float c = ( -p1 + p3) * t; + float d = 2*p2; + return 0.5f * (a+b+c+d); +} + +static void catmull_calc_vec(float *p1, float *p2, float *p3, float *p4, float *out, float t, int num_values) +{ + for(int i=0; i + * @author gamemasterplc + */ +#ifndef __LIBDRAGON_MODEL64_INTERNAL_H +#define __LIBDRAGON_MODEL64_INTERNAL_H + +#include +#include + +/** @brief model64 file magic header */ +#define MODEL64_MAGIC 0x4D444C48 // "MDLH" +/** @brief model64 loaded model buffer magic */ +#define MODEL64_MAGIC_LOADED 0x4D444C4C // "MDLL" +/** @brief model64 owned model buffer magic */ +#define MODEL64_MAGIC_OWNED 0x4D444C4F // "MDLO" +/** @brief Current version of model64 */ +#define MODEL64_VERSION 2 + +/** @brief Animation component index for position. */ +#define ANIM_COMPONENT_POS 0 +/** @brief Animation component index for rotation. */ +#define ANIM_COMPONENT_ROT 1 +/** @brief Animation component index for scale. */ +#define ANIM_COMPONENT_SCALE 2 + +/** @brief Maximum number of active animations per model instance. */ +#define MAX_ACTIVE_ANIMS 4 + +/** @brief A special empty value for both local_texture and shared_texture fields */ +#define TEXTURE_INDEX_MISSING 0xFFFFFFLU + +/** @brief Parameters for a single vertex attribute (part of #primitive_t) */ +typedef struct attribute_s { + uint32_t size; ///< Number of components per vertex. If 0, this attribute is not defined + uint32_t type; ///< The data type of each component (for example GL_FLOAT) + uint32_t stride; ///< The byte offset between consecutive vertices. If 0, the values are tightly packed + void *pointer; ///< Pointer to the first value +} attribute_t; + +/** @brief A single draw call that makes up part of a mesh (part of #mesh_t) */ +typedef struct primitive_s { + uint32_t mode; ///< Primitive assembly mode (for example GL_TRIANGLES) + attribute_t position; ///< Vertex position attribute, if defined + attribute_t color; ///< Vertex color attribyte, if defined + attribute_t texcoord; ///< Texture coordinate attribute, if defined + attribute_t normal; ///< Vertex normals, if defined + attribute_t mtx_index; ///< Matrix indices (aka bones), if defined + uint32_t vertex_precision; ///< If the vertex positions use fixed point values, this defines the precision + uint32_t texcoord_precision; ///< If the texture coordinates use fixed point values, this defines the precision + uint32_t index_type; ///< Data type of indices (for example GL_UNSIGNED_SHORT) + uint32_t num_vertices; ///< Number of vertices + uint32_t num_indices; ///< Number of indices + uint32_t local_texture; ///< Texture index in this model + uint32_t shared_texture; ///< A shared texture index between other models + void *indices; ///< Pointer to the first index value. If NULL, indices are not used +} primitive_t; + +/** @brief Transform of a node of a model */ +typedef struct node_transform_s { + float pos[3]; ///< Position of a node + float rot[4]; ///< Rotation of a node (quaternion) + float scale[3]; ///< Scale of a node + float mtx[16]; ///< Matrix of a node +} node_transform_t; + +/** @brief Transform state of a node of a model */ + +typedef struct node_transform_state_s { + node_transform_t transform; ///< Current transform state for a node + float world_mtx[16]; ///< World matrix for a node +} node_transform_state_t; + +/** @brief A mesh of the model */ +typedef struct mesh_s { + uint32_t num_primitives; ///< Number of primitives + primitive_t *primitives; ///< Pointer to the first primitive +} mesh_t; + +/** @brief A joint of the model */ +typedef struct model64_joint_s { + uint32_t node_idx; ///< Index of node joint is attached to + float inverse_bind_mtx[16]; ///< Inverse bind matrix of joint +} model64_joint_t; + +/** @brief A skin of the model */ +typedef struct model64_skin_s { + uint32_t num_joints; ///< Number of joints + model64_joint_t *joints; ///< Pointer to the first joint +} model64_skin_t; + +/** @brief A node of the model */ +typedef struct model64_node_s { + char *name; ///< Name of a node + mesh_t *mesh; ///< Mesh a node refers to + model64_skin_t *skin; ///< Skin a node refers to + node_transform_t transform; ///< Initial transform of a node + uint32_t parent; ///< Index of parent node + uint32_t num_children; ///< Number of children nodes + uint32_t *children; ///< List of children node indices +} model64_node_t; + +/** @brief A keyframe of an animation */ +typedef struct model64_keyframe_s { + float time; ///< Time of keyframe + float time_req; ///< Time keyframe was requested + uint16_t track; ///< Track keyframe applies to + uint16_t data[3]; ///< Data for keyframe +} model64_keyframe_t; + +/** @brief An animation of a model */ +typedef struct model64_anim_s { + char *name; ///< Name of the animation + float pos_f1; ///< Scale of position components of animation + float pos_f2; ///< Minimum position of animation + float scale_f1; ///< Scale of scale components of animation + float scale_f2; ///< Minimum scale of animation + float duration; ///< Duration of animation + uint32_t num_keyframes; ///< Number of keyframes in animation + model64_keyframe_t *keyframes; ///< Pointer to animation keyframes + uint32_t num_tracks; ///< Number of tracks targeted by animation + uint16_t *tracks; ///< Top 2 bits: target component; lowest 14 bits: target node +} model64_anim_t; + +/** @brief A model64 file containing a model */ +typedef struct model64_data_s { + uint32_t magic; ///< Magic header (MODEL64_MAGIC) + uint32_t ref_count; ///< Number of times model data is used + uint32_t version; ///< Version of this file + uint32_t header_size; ///< Size of the header in bytes + uint32_t mesh_size; ///< Size of a mesh header in bytes + uint32_t primitive_size; ///< Size of a primitive header in bytes + uint32_t node_size; ///< Size of a node in bytes + uint32_t skin_size; ///< Size of a skin in bytes + uint32_t anim_size; ///< Size of an animation in bytes + uint32_t num_nodes; ///< Number of nodes + model64_node_t *nodes; ///< Pointer to the first node + uint32_t root_node; ///< Root node of the model + uint32_t num_skins; ///< Number of skins + model64_skin_t *skins; ///< Pointer to the first skin + uint32_t num_meshes; ///< Number of meshes + mesh_t *meshes; ///< Pointer to the first mesh + uint32_t num_anims; ///< Number of animations + model64_anim_t *anims; ///< Pointer to first animation + uint32_t max_tracks; ///< Maximum number of tracks for animation + void *anim_data_handle; ///< Handle for animation data (0 means animations are not streamed) + uint32_t num_textures; ///< Number of texture paths + char **texture_paths; ///< Pointer to first texture path +} model64_data_t; + +/** @brief Decoded data for a keyframe */ +typedef struct decoded_keyframe_s { + float time; ///< Time of keyframe + float data[4]; ///< Data for keyframe +} decoded_keyframe_t; + +/** @brief State of an active animation */ +typedef struct anim_state_s { + int32_t index; ///< Index of animation playing + float time; ///< Current time of animation + bool invalid_pose; ///< Whether this animation needs to recalculate a pose + bool loop; ///< Whether this animation loops + bool paused; ///< Whether this animation is active + bool prev_waiting_frame; ///< Whether there is a previous waiting frame + bool done_decoding; ///< Whether there are more keyframes to read + float speed; ///< The speed of an animation + uint32_t frame_idx; ///< Index of next keyframe to read + decoded_keyframe_t *frames; ///< Buffer for decoded keyframes + model64_keyframe_t *curr_frame; ///< Buffer for keyframe waiting to be copied +} anim_state_t; + +/** @brief A model64 instance */ +typedef struct model64_s { + model64_data_t *data; ///< Pointer to the model data this instance refers to + node_transform_state_t *transforms; ///< List of transforms for each bone in a model instance + anim_state_t *active_anims[MAX_ACTIVE_ANIMS]; ///< List of active animations +} model64_t; + +#endif diff --git a/src/n64sys.c b/src/n64sys.c index 235795c60..5e712ec88 100644 --- a/src/n64sys.c +++ b/src/n64sys.c @@ -8,20 +8,25 @@ #include #include +#include #include #include +#include #include "n64sys.h" #include "regsinternal.h" #include "interrupt.h" -#include "vi.h" #include "rsp.h" #include "rdp.h" #include "utils.h" +#include "rompak_internal.h" +#include "accounting_internal.h" +#include "interrupt_internal.h" int __boot_memsize; ///< Memory size as detected by IPL3 int __boot_tvtype; ///< TV type as detected by IPL3 int __boot_resettype; ///< Reset type as detected by IPL3 int __boot_consoletype; ///< Console type as detected by IPL3 +int __boot_address_page; ///< Address in the ROM of the ELF /** @brief Records whether the user called a function to check for the presence of expanded memory */ bool __expanded_memory_asserted = false; @@ -29,6 +34,9 @@ bool __expanded_memory_asserted = false; /** @brief Last value of the 64-bit counter */ static uint64_t ticks64_base; +/** @brief Set if it's a broken emulator not support hw_memset */ +static bool hw_memset_broken = false; + /// @cond // Instruction cache defines. @@ -46,6 +54,7 @@ static uint64_t ticks64_base; #define INDEX_CREATE_DIRTY (3) #define HIT_INVALIDATE (4) #define HIT_WRITEBACK_INVALIDATE (5) +#define HIT_FILL (5) #define HIT_WRITEBACK (6) /// @endcond @@ -115,6 +124,11 @@ void inst_cache_hit_invalidate(volatile void * addr, unsigned long length) cache_op(build_opcode(HIT_INVALIDATE, CACHE_INST_FLAG), CACHE_INST_LINESIZE); } +void inst_cache_hit_fill(volatile void * addr, unsigned long length) +{ + cache_op(build_opcode(HIT_FILL, CACHE_INST_FLAG), CACHE_INST_LINESIZE); +} + void inst_cache_index_invalidate(volatile void * addr, unsigned long length) { cache_op(build_opcode(INDEX_INVALIDATE, CACHE_INST_FLAG), CACHE_INST_LINESIZE); @@ -157,6 +171,30 @@ void free_uncached(void *buf) free(CachedAddr(buf)); } +void* realloc_uncached(void *buf, size_t size) +{ + size_t old_size = malloc_usable_size(CachedAddr(buf)); + old_size = ROUND_DOWN(old_size, 16); + size = ROUND_UP(size, 16); + + if (old_size == size) + return buf; + + if (old_size > size) { + void *new_buf = realloc(CachedAddr(buf), size); + assertf(new_buf == CachedAddr(buf), "shrinking a buffer should not move it"); + return UncachedAddr(new_buf); + } + + // Growing the buffer: allocate a new one and copy the contents. + void *new_buf = malloc_uncached(size); + if (!new_buf) + return NULL; + memcpy(new_buf, buf, old_size); + free_uncached(buf); + return new_buf; +} + int get_memory_size(void) { // If the application checked the size of the memory, we can assume that @@ -187,14 +225,19 @@ reset_type_t sys_reset_type(void) return __boot_resettype; } +pi_addr_t sys_elf_address(void) +{ + return 0x10000000 | (__boot_address_page << 8); +} + uint64_t get_ticks(void) { - disable_interrupts(); + uint32_t sr = __disable_interrupts(); uint32_t now = TICKS_READ(); uint32_t prev = (uint32_t)ticks64_base; ticks64_base += (uint32_t)(now - prev); uint64_t ret = ticks64_base; - enable_interrupts(); + __enable_interrupts(sr); return ret; } @@ -208,6 +251,11 @@ uint64_t get_ticks_ms(void) return TICKS_TO_MS(get_ticks()); } +uint64_t get_user_ticks(void) +{ + return get_ticks() - get_system_ticks(); +} + void wait_ticks( unsigned long wait ) { unsigned int initial_tick = TICKS_READ(); @@ -246,18 +294,47 @@ void die(void) void sys_get_heap_stats(heap_stats_t *stats) { - extern int __heap_total_size; + extern int __heap_total_size, __heap_top_allocated_size; struct mallinfo m = mallinfo(); stats->total = __heap_total_size; - stats->used = m.uordblks; + stats->used = m.uordblks + __heap_top_allocated_size; + stats->free = stats->total - stats->used; + stats->fragmented = m.fordblks > m.keepcost ? m.fordblks - m.keepcost : 0; + stats->fragmentation = m.fordblks ? (float)stats->fragmented / (float)m.fordblks : 0.0f; +} + +static void version_callback(void *ctx, char *key, char *value) +{ + sys_version_t* version = (sys_version_t *)ctx; + if (strcmp(key, "branch") == 0) { + strlcpy(version->branch, value, sizeof(version->branch)); + } else if (strcmp(key, "hash") == 0) { + strlcpy(version->hash, value, sizeof(version->hash)); + } else if (strcmp(key, "commit-date") == 0) { + strlcpy(version->commit_date, value, sizeof(version->commit_date)); + } else if (strcmp(key, "dirty") == 0) { + version->dirty = strcmp(value, "true") == 0; + } +} + +bool sys_get_version(sys_version_t *version) +{ + size_t size; + pi_addr_t version_addr = rompak_search_ext("libdragon.version", &size); + if (!version_addr) { + return false; + } + + rompak_version_parse(version_addr, size, version_callback, version); + return true; } /** * @brief Initialize COP1 with default settings that prevent undesirable exceptions. * */ -__attribute__((constructor)) void __init_cop1(void) +__attribute__((constructor(110))) void __init_cop1(void) { /* Read initialized value from cop1 control register */ uint32_t fcr31 = C1_FCR31(); @@ -298,3 +375,176 @@ __attribute__((constructor)) void __init_cop1(void) /* Write back updated cop1 control register */ C1_WRITE_FCR31(fcr31); } + +///@cond +// Assembly implementations of memset functions (see mi_memset.S) +extern void* __mi_memset(void *ptr, uint8_t value, size_t len); +extern void* __mi_memset16(void *ptr, uint16_t value, size_t len); +extern void* __mi_memset32(void *ptr, uint32_t value, size_t len); +extern void* __mi_memset64(void *ptr, uint64_t value, size_t len); +///@endcond + +void* sys_hw_memset(void *ptr, uint8_t value, size_t len) +{ + assertf(!hw_memset_broken, "Your emulator is not accurate enough to run this ROM.\nSpecifically, it doesn't support MI repeat mode"); + return __mi_memset(ptr, value, len); +} + +void* sys_hw_memset16(void *ptr, uint16_t value, size_t len) +{ + assertf(!hw_memset_broken, "Your emulator is not accurate enough to run this ROM.\nSpecifically, it doesn't support MI repeat mode"); + return __mi_memset16(ptr, value, len); +} + +void* sys_hw_memset32(void *ptr, uint32_t value, size_t len) +{ + assertf(!hw_memset_broken, "Your emulator is not accurate enough to run this ROM.\nSpecifically, it doesn't support MI repeat mode"); + return __mi_memset32(ptr, value, len); +} + +void* sys_hw_memset64(void *ptr, uint64_t value, size_t len) +{ + assertf(!hw_memset_broken, "Your emulator is not accurate enough to run this ROM.\nSpecifically, it doesn't support MI repeat mode"); + return __mi_memset64(ptr, value, len); +} + +/** @brief Constructor that verifies if MI repeat mode is correctly emulated */ +__attribute__((constructor)) +void __n64sys_hwmemset_emucheck(void) +{ + uint32_t test[4] = {0}; + __mi_memset32(&test, 0x12345678, sizeof(test)); + if (test[0] != 0x12345678 || test[1] != 0x12345678 || test[2] != 0x12345678 || test[3] != 0x12345678) + hw_memset_broken = true; +} + + +/************* BSS CHECK **************/ +// This code is useful only while debugging IPL3 changes. It is not run by default +// and requires manually changing entrypoint.S to be activated. It is left in the +// preview branch for reference, but it's not meant to be merged into stable. + +/// @cond +static uint32_t io_read32(uint32_t vaddrx) +{ + vaddrx |= 0xA0000000; + volatile uint32_t *vaddr = (uint32_t *)vaddrx; + return *vaddr; +} + +static uint8_t io_read8(uint32_t vaddrx) +{ + uint32_t value = io_read32(vaddrx & ~3); + return value >> ((~vaddrx & 3)*8); +} + +static const unsigned char font[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7e, 0xa1, 0x99, 0x85, 0x7e, 0x84, 0x82, 0xff, 0x80, 0x80, 0xc1, 0xa1, + 0x91, 0x89, 0x86, 0x89, 0x89, 0x89, 0x89, 0x76, 0x18, 0x14, 0x12, 0xff, + 0x10, 0x8f, 0x89, 0x89, 0x89, 0x71, 0x7e, 0x89, 0x89, 0x89, 0x72, 0x01, + 0x81, 0x61, 0x19, 0x07, 0x62, 0x95, 0x89, 0x95, 0x62, 0x4e, 0x91, 0x91, + 0x91, 0x7e, 0xfe, 0x11, 0x11, 0x11, 0xfe, 0xff, 0x89, 0x89, 0x89, 0x76, + 0x7e, 0x81, 0x81, 0x81, 0x81, 0xff, 0x81, 0x81, 0x81, 0x7e, 0xff, 0x89, + 0x89, 0x89, 0x89, 0xff, 0x09, 0x09, 0x09, 0x09, 0x7e, 0x81, 0x91, 0x51, + 0xf1, 0xff, 0x08, 0x08, 0x08, 0xff, 0x00, 0x81, 0xff, 0x81, 0x00, 0x40, + 0x80, 0x80, 0x80, 0x7f, 0xff, 0x08, 0x14, 0x22, 0xc1, 0xff, 0x80, 0x80, + 0x80, 0x80, 0xff, 0x02, 0x04, 0x02, 0xff, 0xff, 0x06, 0x18, 0x60, 0xff, + 0x7e, 0x81, 0x81, 0x81, 0x7e, 0xff, 0x11, 0x11, 0x11, 0x0e, 0x7e, 0x81, + 0xa1, 0xc1, 0xfe, 0xff, 0x11, 0x11, 0x11, 0xee, 0x86, 0x89, 0x89, 0x89, + 0x71, 0x01, 0x01, 0xff, 0x01, 0x01, 0x7f, 0x80, 0x80, 0x80, 0x7f, 0x1f, + 0x60, 0x80, 0x60, 0x1f, 0xff, 0x40, 0x20, 0x40, 0xff, 0xc7, 0x28, 0x10, + 0x28, 0xc7, 0x07, 0x08, 0xf0, 0x08, 0x07, 0xc1, 0xa1, 0x99, 0x85, 0x83, +}; + + +#define _(x) ((x) >= '0' && (x) <= '9' ? (x) - '0' + 2 : \ + (x) >= 'A' && (x) <= 'Z' ? (x) - 'A' + 12 : \ + (x) == ' ' ? 1 : 0) + +// "BSS CHECK ERROR" +static const char MSG_BSS_CHECK_ERROR[] = { + _('B'), _('S'), _('S'), _(' '), _('C'), _('H'), _('E'), _('C'), _('K'), _(' '), _('E'), _('R'), _('R'), _('O'), _('R'), 0 +}; + +__attribute__((noreturn)) +static void fatal(const char *str) +{ +#if 1 +#define FATAL_TEXT 1 +#if FATAL_TEXT +#endif + static const uint32_t vi_regs_p[3][7] = { + { /* PAL */ 0x0404233a, 0x00000271, 0x00150c69, + 0x0c6f0c6e, 0x00800300, 0x005f0239, 0x0009026b }, + { /* NTSC */ 0x03e52239, 0x0000020d, 0x00000c15, + 0x0c150c15, 0x006c02ec, 0x002501ff, 0x000e0204 }, + { /* MPAL */ 0x04651e39, 0x0000020d, 0x00040c11, + 0x0c190c1a, 0x006c02ec, 0x002501ff, 0x000e0204 }, + }; + + #undef RGBA32 + #define RGBA(r,g,b,a) (((r)<<11) | ((g)<<6) | ((b)<<1) | (a)) + #define RGBA32(rgba32) RGBA((((rgba32)>>19) & 0x1F), (((rgba32)>>11) & 0x1F), ((rgba32>>3) & 0x1F), (((rgba32)>>31) & 1)) + + uint16_t *fb_base = (uint16_t*)0xA0100000; + volatile uint32_t* regs = (uint32_t*)0xA4400000; + regs[1] = (uint32_t)fb_base; + for (int i=0; i<320*240; i++) + fb_base[i] = RGBA32(0xDF8A7B); + + regs[2] = 320; + regs[12] = 0x200; + regs[13] = 0x400; + +#if FATAL_TEXT + const int RES_WIDTH = 320; + const int X = 40; + const int Y = 40; + const uint16_t COLOR = RGBA32(0xF3F9D2); + + fb_base += Y*RES_WIDTH + X; + uint16_t *fb = fb_base; + while (1) { + char ch = io_read8((uint32_t)str); str++; + if (!ch) break; + const uint8_t *glyph = font + (ch-1)*5; + for (int x=0; x<5; x++) { + uint8_t g = io_read8((uint32_t)glyph); + for (int y=0; y<8; y++) { + if (g & (1 << y)) + fb[RES_WIDTH*y] = COLOR; + } + fb++; + glyph++; + } + + fb += 2; // spacing + } + +#endif + int tv_type = io_read8(0xA4000009); + bool ique = io_read8(0xA400000B); + #pragma GCC unroll 0 + for (int reg=0; reg<7; reg++) + regs[reg+5] = vi_regs_p[tv_type][reg]; + regs[0] = ique ? 0x1202 : 0x3202; +#endif + abort(); +} + +void __bss_check(void) +{ + extern char __bss_start[]; + void *bss_start = (void*)__bss_start; + void *bss_end = (void*)__bss_end; + for (uint32_t *p = bss_start; p < (uint32_t*)bss_end; p++) + if (*p != 0) + fatal(MSG_BSS_CHECK_ERROR); +} +/// @endcond + +/* Inline instantiations */ +extern inline bool sys_bbplayer(void); +extern inline tv_type_t get_tv_type(void); +extern inline uint64_t get_system_ticks(void); diff --git a/src/pifile.c b/src/pifile.c new file mode 100644 index 000000000..a792db848 --- /dev/null +++ b/src/pifile.c @@ -0,0 +1,151 @@ +/** + * @file pifile.c + * @author Giovanni Bajo + * @brief Load a PI-mapped file + * @ingroup asset + */ +#include +#include +#include +#include +#include +#include +#include +#include "pifile.h" +#include "n64sys.h" +#include "system.h" +#include "dma.h" + +/** @brief A PI-mapped open file */ +typedef struct { + uint32_t base; ///< Base address in the PI bus + int ptr; ///< Current pointer + int size; ///< File size +} pifile_t; + +static void *__pifile_open(char *name, int flags) +{ + if (flags != O_RDONLY) { + errno = EACCES; + return NULL; + } + + // Parse the name in the foramt "ADDR:SIZE" as hexdecimal numbers + char *end; + uint32_t base = strtoul(name, &end, 16); + if (*end != ':') { + errno = EINVAL; + return NULL; + } + int size = strtoul(end+1, &end, 16); + if (*end != '\0') { + errno = EINVAL; + return NULL; + } + + pifile_t *file = malloc(sizeof(pifile_t)); + if (!file) { + errno = ENOMEM; + return NULL; + } + + file->base = base; + file->size = size; + file->ptr = 0; + return file; +} + +static int __pifile_fstat(void *file, struct stat *st) +{ + pifile_t *f = file; + memset(st, 0, sizeof(struct stat)); + st->st_mode = S_IFREG; + st->st_size = f->size; + st->st_nlink = 1; + return 0; +} + +static int __pifile_lseek(void *file, int offset, int whence) +{ + pifile_t *f = file; + switch (whence) { + case SEEK_SET: + f->ptr = offset; + break; + case SEEK_CUR: + f->ptr += offset; + break; + case SEEK_END: + f->ptr = f->size + offset; + break; + default: + errno = EINVAL; + return -1; + } + + if (f->ptr < 0) + f->ptr = 0; + if (f->ptr > f->size) + f->ptr = f->size; + return f->ptr; +} + +static int __pifile_read(void *file, uint8_t *buf, int len) +{ + pifile_t *f = file; + if (f->ptr + len > f->size) + len = f->size - f->ptr; + if (len <= 0) + return 0; + + // Check if we can DMA directly to the output buffer + if ((((f->base + f->ptr) ^ (uint32_t)buf) & 1) == 0) { + data_cache_hit_writeback_invalidate(buf, len); + dma_read_async(buf, f->base + f->ptr, len); + dma_wait(); + f->ptr += len; + } else { + // Go through a temporary buffer + uint8_t *tmp = alloca(512+1); + if ((f->base + f->ptr) & 1) tmp++; + + while (len > 0) { + int n = len > 512 ? 512 : len; + data_cache_hit_writeback_invalidate(tmp, n); + dma_read_async(tmp, f->base + f->ptr, n); + dma_wait(); + memcpy(buf, tmp, n); + buf += n; + f->ptr += n; + len -= n; + } + } + + return len; +} + +static int __pifile_close(void *file) +{ + free(file); + return 0; +} + +static filesystem_t pifile_fs = { + .open = __pifile_open, + .fstat = __pifile_fstat, + .lseek = __pifile_lseek, + .read = __pifile_read, + .close = __pifile_close, +}; + +void pifile_init(void) +{ + attach_filesystem("pi:/", &pifile_fs); +} + +FILE *pifile_open(pi_addr_t base, size_t size, uint32_t flags) +{ + char name[32]; + snprintf(name, sizeof(name), "pi:/%08lX:%08X", base, size); + return fopen(name, "r"); +} diff --git a/src/profile.c b/src/profile.c new file mode 100644 index 000000000..a8d13a98d --- /dev/null +++ b/src/profile.c @@ -0,0 +1,215 @@ +/** + * @file profile.c + * @author Giovanni Bajo + * @brief CPU profiler implementation. + */ +#include "profile.h" +#include "debug.h" +#include "n64sys.h" +#include "timer.h" +#include "accounting_internal.h" +#include +#include + +/** @brief Internal profile slot data */ +typedef struct { + const char *name; ///< Name of the slot + int nest_level; ///< Number of nesting levels for this slot + uint32_t ticks; ///< Total number of ticks spent in the slot + uint32_t cache_misses; ///< Total number of cache misses in the slot + uint32_t cache_hits; ///< Total number of cache hits in the slot + int count; ///< Number of times the slot was hit +} profile_slot_t; + +/** @brief Profile counters (actual accumulators) */ +uint64_t *__profile_counters; +/** @brief Porfile slots descriptions */ +profile_slot_t *slots; +/** @brief Number of profile slots */ +static int num_slots; + +static uint64_t total_time_wall; +static uint64_t total_time_user; +static uint64_t last_frame_wall; +static uint64_t last_frame_user; +static uint64_t target_frame_ticks; +static uint64_t sys_total[ACCT_CAT_MAX]; +static uint64_t sys_frame_last[ACCT_CAT_MAX]; +static int frames; +static uint64_t dump_interval; +static uint64_t last_dump_time; + +void profile_init(profile_parms_t *parms) { + num_slots = (parms && parms->num_slots > 0) ? parms->num_slots : 128; + slots = calloc(num_slots, sizeof(profile_slot_t)); + __profile_counters = calloc(num_slots, sizeof(uint64_t)); + assertf(slots && __profile_counters, "Out of memory"); + + float dump_time = parms ? parms->dump_stderr_interval : 5.0f; + if (dump_time >= 0.0f) { + dump_interval = (uint64_t)(dump_time * TICKS_PER_SECOND); + } else { + dump_interval = 0; + } + last_dump_time = get_ticks(); + + profile_reset(); +} + +void profile_close(void) +{ + if (__profile_counters) { + free(__profile_counters); + __profile_counters = NULL; + } + if (slots) { + free(slots); + slots = NULL; + } + num_slots = 0; +} + +void profile_reset(void) { + sys_hw_memset(__profile_counters, 0, num_slots * sizeof(uint64_t)); + for (int i=0; i= 0 && slot < num_slots, "Invalid profile slot %d", slot); + slots[slot].name = name; + slots[slot].nest_level = nest_level; +} + +void profile_set_target_fps(float fps) { + if (fps <= 0.0f) { + target_frame_ticks = 0; + } else { + target_frame_ticks = (uint64_t)(TICKS_PER_SECOND / fps); + } +} + +void profile_next_frame(void) { + bool emux_profile_available = (emux_detect(1) & EMUX_FEAT1_PROFILER) != 0; + for (int i=0; iticks += __profile_counters[i] >> 32; + slot->count += __profile_counters[i] & 0xFFFFFFFF; + if (emux_profile_available) { + slot->cache_misses += emux_prof_read(i, EMUX_PROF_ICACHE_MISSES) + emux_prof_read(i, EMUX_PROF_DCACHE_MISSES); + slot->cache_hits += emux_prof_read(i, EMUX_PROF_ICACHE_HITS) + emux_prof_read(i, EMUX_PROF_DCACHE_HITS); + } + __profile_counters[i] = 0; + emux_prof_clear(i); + } + for (int i=0;i 0) { + cache_miss_rate = (float)cache_misses * 100.0f / + (float)(slots[i].cache_hits + slots[i].cache_misses); + } + + int avg_us = TIMER_MICROS(mean); + debugf("%-35.35s %4d %6d (%5.1f%%) %6d (%5.2f%%)\n", + name, slots[i].count / frames, avg_us, partial_avg, cache_misses, cache_miss_rate); + } + +///@cond +#define DUMP_SYS(cat, name) ({ \ + uint64_t ticks = sys_total[cat] / frames; \ + partial_sys = (float)ticks * 100.0f / (float)frame_avg_wall; \ + if (ticks) debugf("%-35.35s %4s %6d (%5.1f%%)\n", name, "-", \ + TIMER_MICROS(ticks), partial_sys); \ +}) +///@endcond + + DUMP_SYS(ACCT_CAT_IRQ, "[sys] IRQ time"); + DUMP_SYS(ACCT_CAT_RSP, "[sys] RSP wait"); + DUMP_SYS(ACCT_CAT_DISPLAY, "[sys] Display wait"); + DUMP_SYS(ACCT_CAT_RSPQ, "[sys] RSPQ wait"); + DUMP_SYS(ACCT_CAT_VI, "[sys] VI wait"); + DUMP_SYS(ACCT_CAT_JOYBUS, "[sys] Joybus wait"); + DUMP_SYS(ACCT_CAT_PI, "[sys] PI wait"); + + debugf("------------------------------------------------------------------------\n"); + debugf("Profiled frames: %4d\n", frames); + debugf("Frames per second: %4.1f\n", (float)TICKS_PER_SECOND/(float)frame_avg_wall); + debugf("Target frame time: %4d us\n", TIMER_MICROS(target_frame_ticks)); + debugf("Average frame (wall): %4d us\n", TIMER_MICROS(frame_avg_wall)); + debugf("Average frame (user): %4d us (%2.1f%%)\n", TIMER_MICROS(frame_avg_user), + (float)frame_avg_user * 100.0f / (float)frame_avg_wall); + debugf("Average frame (sys): %4d us (%2.1f%%)\n", TIMER_MICROS(frame_avg_wall - frame_avg_user), + (float)(frame_avg_wall - frame_avg_user) * 100.0f / (float)frame_avg_wall); +} diff --git a/src/rand.c b/src/rand.c new file mode 100644 index 000000000..b3f9aca97 --- /dev/null +++ b/src/rand.c @@ -0,0 +1,73 @@ +/** + * @file rand.c + * @author Giovanni Bajo + * @brief Internal random number generation functions. + */ + +#include "rand_internal.h" +#include "n64types.h" +#include "entropy.h" +#include +#include +#include + +/** @brief Constant used in the xorshift64 algorithm */ +uint64_t __xorshift64_mul_K = 0x2545F4914F6CDD1Dull; + +__attribute__((noinline)) +uint64_t __xorshift64(_Atomic uint64_t *state) { + uint64_t old = atomic_load_explicit(state, memory_order_relaxed); + while (1) { + uint64_t x = old; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + if (atomic_compare_exchange_weak_explicit( + state, &old, x, + memory_order_relaxed, memory_order_relaxed)) + return x * __xorshift64_mul_K; + } +} + +__attribute__((noinline)) +void __xorshift64_buffer(void *buf, size_t len, _Atomic uint64_t *state) +{ + while (len >= 8) { + uint64_t x = __xorshift64(state); + memcpy(buf, &x, 8); + buf += 8; + len -= 8; + } + + if (len > 0) { + uint64_t x = __xorshift64(state); + memcpy(buf, &x, len); + } +} + +// Seeded at construction time, but anyway avoid 0 which is invalid +static _Atomic uint64_t rand_state = 1; + +__attribute__((noinline)) +uint64_t __rand64(void) +{ + return __xorshift64(&rand_state); +} + +void __rand(void *buf, size_t n) +{ + __xorshift64_buffer(buf, n, &rand_state); +} + +/** @brief Initialize the random number generator (constructor) */ +__attribute__((constructor)) +void __rand_init(void) +{ + // Initialize random state + rand_state = getentropy64(); +} + +extern inline uint8_t __rand8(void); +extern inline uint16_t __rand16(void); +extern inline uint32_t __rand32(void); +extern inline float __randf32(void); diff --git a/src/rand_internal.h b/src/rand_internal.h new file mode 100644 index 000000000..54ffe1a96 --- /dev/null +++ b/src/rand_internal.h @@ -0,0 +1,55 @@ +/** + * @file rand_internal.h + * @author Giovanni Bajo + * @brief Internal random number generation functions. + * + * This module provides a simple and fast PRNG, only for internal libdragon use. + * + * Whenever libdragon needs some random number, it should use this module instead + * of the standard library functions. This is important because C rand() is + * deterministic and if determinism is needed, users will expect all calls to + * rand() to happen under their control. If libdragon itself started calling + * rand() without the user knowing, it would break determinism. + */ +#ifndef LIBDRAGON_RAND_INTERNAL_H +#define LIBDRAGON_RAND_INTERNAL_H + +#include +#include +#include + +/** @brief Main PRNG (xorshift64*) */ +uint64_t __xorshift64(_Atomic uint64_t *state); + +/** @brief Fill a buffer with xorshift64 */ +void __xorshift64_buffer(void *buf, size_t n, _Atomic uint64_t *state); + +/** + * @brief Generate a random 64-bit unsigned integer. + */ +uint64_t __rand64(void); + +/** + * @brief Fill a buffer with random data. + * + * @param buf Pointer to the buffer to fill. + * @param len Length of the buffer in bytes. + */ +void __rand(void *buf, size_t len); + +/** @brief Generate a random 8-bit unsigned integer. */ +inline uint8_t __rand8(void) { return __rand64() & 0xFF; } +/** @brief Generate a random 16-bit unsigned integer. */ +inline uint16_t __rand16(void) { return __rand64() & 0xFFFF; } +/** @brief Generate a random 32-bit unsigned integer. */ +inline uint32_t __rand32(void) { return __rand64() & 0xFFFFFFFF; } +/** @brief Generate a random 32-bit floating point number in range [0, 1). */ +inline float __randf32(void) { return (float)__rand32() * (1.0f / 4294967296.0f); } + +/** @brief Generate a random number in the range [0, n-1]. */ +#define __randn(n) (\ + __builtin_constant_p(n) ? \ + __rand32() % (n) : \ + ((uint64_t)__rand32() * (n)) >> 32) + +#endif // LIBDRAGON_RAND_INTERNAL_H diff --git a/src/rdpq/libdragon.mk b/src/rdpq/libdragon.mk index bf1497133..b18bb3dd3 100644 --- a/src/rdpq/libdragon.mk +++ b/src/rdpq/libdragon.mk @@ -5,9 +5,11 @@ LIBDRAGON_OBJS += \ $(BUILD_DIR)/rdpq/rdpq_tri.o \ $(BUILD_DIR)/rdpq/rdpq_rect.o \ $(BUILD_DIR)/rdpq/rdpq_mode.o \ + $(BUILD_DIR)/rdpq/rdpq_mat.o \ $(BUILD_DIR)/rdpq/rdpq_sprite.o \ $(BUILD_DIR)/rdpq/rdpq_tex.o \ $(BUILD_DIR)/rdpq/rdpq_attach.o \ $(BUILD_DIR)/rdpq/rdpq_font.o \ $(BUILD_DIR)/rdpq/rdpq_text.o \ - $(BUILD_DIR)/rdpq/rdpq_paragraph.o \ No newline at end of file + $(BUILD_DIR)/rdpq/rdpq_paragraph.o \ + $(BUILD_DIR)/rdpq/rdpq_xform.o \ No newline at end of file diff --git a/src/rdpq/rdpq.c b/src/rdpq/rdpq.c index ea0e2b6be..4ec98fedf 100644 --- a/src/rdpq/rdpq.c +++ b/src/rdpq/rdpq.c @@ -363,6 +363,7 @@ #include "rdpq_internal.h" #include "rdpq_constants.h" #include "rdpq_debug_internal.h" +#include "rdpq_xform_internal.h" #include "rspq.h" #include "rspq/rspq_internal.h" #include "rspq_constants.h" @@ -409,6 +410,9 @@ rdpq_block_state_t rdpq_block_state; /** @brief Tracking state of RDP */ rdpq_tracking_t rdpq_tracking; +/** @brief Syncpoint ID at the moment of last SYNC_FULL. Used to implement #rdpq_call_deferred. */ +volatile int __rdpq_syncpoint_at_syncfull; + /** * @brief RDP interrupt handler * @@ -424,6 +428,9 @@ static void __rdpq_interrupt(void) { // Fetch the current RDP buffer for tracing if (rdpq_trace_fetch) rdpq_trace_fetch(false); + // Store the current syncpoint ID. This is used to implement #rdpq_call_deferred. + __rdpq_syncpoint_at_syncfull = rdpq_state->rspq_syncpoint_id; + // The state has been updated to contain a copy of the last SYNC_FULL command // that was sent to RDP. The command might contain a callback to invoke. // Extract it to local variables. @@ -451,7 +458,8 @@ void rdpq_init() return; rspq_init(); - + __rdpq_xform_init(); + // Get a pointer to the RDRAM copy of the rdpq ucode state. rdpq_state = UncachedAddr(rspq_overlay_get_state(&rsp_rdpq)); @@ -492,7 +500,9 @@ void rdpq_close() return; rspq_overlay_unregister(RDPQ_OVL_ID); - + __rdpq_attach_close(); + __rdpq_xform_close(); + set_DP_interrupt( 0 ); unregister_DP_handler(__rdpq_interrupt); @@ -523,7 +533,7 @@ void rdpq_fence(void) // then we send the internal rspq command that make the RSP spin-wait // until the RDP is idle. The RDP becomes idle only after SYNC_FULL is done. rdpq_sync_full(NULL, NULL); - rspq_int_write(RSPQ_CMD_RDP_WAIT_IDLE); + rspq_write(RDPQ_OVL_ID, RDPQ_CMD_WAIT_IDLE); } void rdpq_exec(void *buffer, int size) @@ -534,7 +544,7 @@ void rdpq_exec(void *buffer, int size) // TODO: to implement support in blocks, we need a way to notify the block state machine that // after this command, a new RSPQ_CMD_RDP_SET_BUFFER is required to be sent, to resume playing // the static buffer. - assertf(!rspq_in_block(), "cannot call rdpq_exec() inside a block"); + assertf(!rspq_block_is_recording(), "cannot call rdpq_exec() inside a block"); void *end = buffer + size; rspq_int_write(RSPQ_CMD_RDP_SET_BUFFER, PhysicalAddr(end), PhysicalAddr(buffer), PhysicalAddr(end)); @@ -569,12 +579,19 @@ static void rdpq_assert_handler(rsp_snapshot_t *state, uint16_t assert_code) printf("incorrect usage of auto-TMEM: unpaired begin/end\n"); break; + case RDPQ_ASSERT_ZCLEAR_INVALID_BUFFER: + printf("Z-clear command issued with a too small temporary buffer\n"); + break; + default: printf("Unknown assert\n"); break; } } +/** @brief Resets a tracking state into an unknown state */ +extern inline void __rdpq_tracking_state_reset(rdpq_tracking_t *state); + /** @brief Autosync engine: mark certain resources as in use */ extern inline void __rdpq_autosync_use(uint32_t res); @@ -632,7 +649,44 @@ void __rdpq_block_begin() rdpq_block_state.previous_tracking = rdpq_tracking; // Set for unknown state (like if we just run another unknown block: we lost track of the RDP state) - __rdpq_block_run(NULL); + __rdpq_block_run_no_rdp(); +} + +/** + * @brief Reuse an existing RDP block chain for a new rspq block recording session. + * + * Like #__rdpq_block_begin followed immediately by the state left after the first + * #__rdpq_block_next_buffer on the first node: RSP #RSPQ_CMD_RDP_SET_BUFFER is emitted + * and write pointers point at @p head. + * + * The first link is always sized #RDPQ_BLOCK_MIN_SIZE (see #__rdpq_block_next_buffer). + */ +void __rdpq_block_recycle(rdpq_block_t *head) +{ + struct rdpq_block_state_s *st = &rdpq_block_state; + assertf(head, "__rdpq_block_recycle: NULL head"); + + memset(st, 0, sizeof(*st)); + st->previous_tracking = rdpq_tracking; + __rdpq_block_run_no_rdp(); + + st->first_node = head; + st->last_node = head; + int c = RDPQ_BLOCK_MIN_SIZE; + st->wptr = head->cmds; + st->wend = head->cmds + c; + st->bufsize = (c < RDPQ_BLOCK_MAX_SIZE) ? (c * 2) : c; + + extern volatile uint32_t *rspq_cur_pointer; + st->last_rdp_append_buffer = rspq_cur_pointer; + + assertf((PhysicalAddr(st->wptr) & 0x7) == 0, + "start not aligned to 8 bytes: %lx", PhysicalAddr(st->wptr)); + assertf((PhysicalAddr(st->wend) & 0x7) == 0, + "end not aligned to 8 bytes: %lx", PhysicalAddr(st->wend)); + + rspq_int_write(RSPQ_CMD_RDP_SET_BUFFER, + PhysicalAddr(st->wptr), PhysicalAddr(st->wptr), PhysicalAddr(st->wend)); } /** @@ -663,17 +717,26 @@ void __rdpq_block_next_buffer(void) assert(RDPQ_BLOCK_MIN_SIZE >= RDPQ_MAX_COMMAND_SIZE); } - // Allocate RDP static buffer. - int memsz = sizeof(rdpq_block_t) + st->bufsize*sizeof(uint32_t); - rdpq_block_t *b = malloc_uncached(memsz); - - // Chain the block to the current one (if any) - b->next = NULL; - if (st->last_node) { - st->last_node->next = b; + /* Reuse a pre-linked node when recycling: #next was allocated by this same + * function with the current #bufsize (doubling sequence invariant). */ + rdpq_block_t *b; + if (st->last_node && st->last_node->next) { + b = st->last_node->next; + st->last_node = b; + } else { + // Allocate RDP static buffer. + int memsz = sizeof(rdpq_block_t) + st->bufsize*sizeof(uint32_t); + b = malloc_uncached(memsz); + assertf(b, "Out of memory"); + + // Chain the block to the current one (if any) + b->next = NULL; + if (st->last_node) { + st->last_node->next = b; + } + st->last_node = b; + if (!st->first_node) st->first_node = b; } - st->last_node = b; - if (!st->first_node) st->first_node = b; // Set write pointer and sentinel for the new buffer st->wptr = b->cmds; @@ -707,12 +770,12 @@ void __rdpq_block_next_buffer(void) * This is called by #rspq_block_end. It finalizes block creation * and return a pointer to the first node of the block, which will * be put within the #rspq_block_t structure, so to be able to - * reference it in #__rdpq_block_run and #__rdpq_block_free. + * reference it in #__rdpq_block_run_with_rdp and #__rdpq_block_free. * - * @return rdpq_block_t* The created block (first node) + * @return The created block (first node) * * @see #rspq_block_end - * @see #__rdpq_block_run + * @see #__rdpq_block_run_with_rdp * @see #__rdpq_block_free */ rdpq_block_t* __rdpq_block_end() @@ -739,53 +802,69 @@ rdpq_block_t* __rdpq_block_end() return ret; } -/** @brief Notify that a rspq block was run (called by #rspq_block_run). */ -void __rdpq_block_run(rdpq_block_t *block) +/** + * @brief Notify that a rspq block was run. The block did contain RDP commands. + * + * @param block containing the state + */ +void __rdpq_block_run_with_rdp(rdpq_block_t *block) { - if (block) { - // We have run a block that contains rdpq commands. - // During creation, we tracked some state for the block - // and saved it into the block structure; set it as current, - // because from now on we can assume the block would and the - // state of the engine must match the state at the end of the block. - rdpq_tracking_t prev = rdpq_tracking; - rdpq_tracking = block->tracking; - - // If the data coming out of the block is "unknown", we can - // restore the previous value, because it means that the block didn't - // change it. - if (rdpq_tracking.cycle_type_known == 0) - rdpq_tracking.cycle_type_known = prev.cycle_type_known; - if (rdpq_tracking.cycle_type_frozen == 0) - rdpq_tracking.cycle_type_frozen = prev.cycle_type_frozen; - - // The called block has switched static buffer. Adjust our state to set - // our buffer as pending; if a new RDP command is issued, we will switch - // back to it. - struct rdpq_block_state_s *st = &rdpq_block_state; - st->pending_wptr = st->wptr; - st->pending_wend = st->wend; - st->wptr = NULL; - st->wend = NULL; - } else { - // Initialize tracking state for unknown state - rdpq_tracking = (rdpq_tracking_t){ - // current autosync status is unknown because blocks can be - // played in any context. So assume the worst: all resources - // are being used. This will cause all SYNCs to be generated, - // which is the safest option. - .autosync = ~0, - // we don't know whether mode changes will be frozen or not - // when the block will play. Assume the worst (and thus - // do not optimize out mode changes). - .mode_freeze = false, - // we don't know the cycle type after we run the block - .cycle_type_known = 0, - .cycle_type_frozen = 0, - }; - } + // We have run a block that contains rdpq commands. + // During creation, we tracked some state for the block + // and saved it into the block structure; set it as current, + // because from now on we can assume the block would and the + // state of the engine must match the state at the end of the block. + rdpq_tracking_t prev = rdpq_tracking; + rdpq_tracking = block->tracking; + + // If the data coming out of the block is "unknown", we can + // restore the previous value, because it means that the block didn't + // change it. + if (rdpq_tracking.cycle_type_known == 0) + rdpq_tracking.cycle_type_known = prev.cycle_type_known; + if (rdpq_tracking.cycle_type_frozen == 0) + rdpq_tracking.cycle_type_frozen = prev.cycle_type_frozen; + + // The called block has switched static buffer. Adjust our state to set + // our buffer as pending; if a new RDP command is issued, we will switch + // back to it. + struct rdpq_block_state_s *st = &rdpq_block_state; + st->pending_wptr = st->wptr; + st->pending_wend = st->wend; + st->wptr = NULL; + st->wend = NULL; } +/** + * @brief Notify that a rspq block was run. The block did NOT contain RDP commands. + */ +void __rdpq_block_run_no_rdp(void) +{ + __rdpq_tracking_state_reset(&rdpq_tracking); +} + +/** + * @brief Notify that a rspq block was run. + * + * The block might or might not contain RDP commands. + * This is the case for a block placeholder. + */ +void __rdpq_block_run_maybe_rdp(void) +{ + rdpq_tracking_t prev = rdpq_tracking; + __rdpq_block_run_no_rdp(); + + // The called block has switched static buffer. Adjust our state to set + // our buffer as pending; if a new RDP command is issued, we will switch + // back to it. + struct rdpq_block_state_s *st = &rdpq_block_state; + st->pending_wptr = st->wptr; + st->pending_wend = st->wend; + st->wptr = NULL; + st->wend = NULL; +} + + /** * @brief Free a block * @@ -1093,6 +1172,12 @@ uint64_t rdpq_get_other_modes_raw(void) return state->rdp_mode.other_modes; } +uint64_t rdpq_get_combiner_raw(void) +{ + rsp_queue_t *state = __rspq_get_state(); + return state->rdp_mode.combiner; +} + void rdpq_set_tile_autotmem(int16_t tmem_bytes) { if (tmem_bytes >= 0) { @@ -1133,6 +1218,12 @@ void rdpq_sync_load(void) rdpq_tracking.autosync &= ~AUTOSYNC_TMEMS; } +void rdpq_call_deferred(void (*func)(void *), void *arg) +{ + __rspq_call_deferred(func, arg, true); + rspq_flush(); +} + /** @} */ ///@cond @@ -1174,3 +1265,5 @@ extern inline void rdpq_set_z_image_raw(uint8_t index, uint32_t offset); extern inline void rdpq_set_texture_image_raw(uint8_t index, uint32_t offset, tex_format_t format, uint16_t width, uint16_t height); extern inline void rdpq_set_lookup_address(uint8_t index, void* rdram_addr); extern inline void rdpq_set_tile(rdpq_tile_t tile, tex_format_t format, int32_t tmem_addr, uint16_t tmem_pitch, const rdpq_tileparms_t *parms); +extern inline void rdpq_call_deferred(void (*func)(void *), void *arg); +extern inline void rdpq_set_yuv_parms(uint16_t k0, uint16_t k1, uint16_t k2, uint16_t k3, uint16_t k4, uint16_t k5); diff --git a/src/rdpq/rdpq_attach.c b/src/rdpq/rdpq_attach.c index e85fc9a3d..e0eaeb475 100644 --- a/src/rdpq/rdpq_attach.c +++ b/src/rdpq/rdpq_attach.c @@ -20,6 +20,49 @@ static const surface_t* attach_stack[ATTACH_STACK_SIZE][2] = { { NULL, NULL } }; static int attach_stack_ptr = 0; +// Compute the RSP DMA size (in the RSP hardware register format) +// for a given number of bytes. +static uint32_t calc_rspdma_size(int nbytes) +{ + int len, count; + for (len = 0x1000; len >= 8; len -= 8) { + if (nbytes % len == 0) { + count = nbytes / len; + if (count <= 0x100) + return ((count-1) << 12) | (len - 1); + } + } + return 0; +} + +static bool __rdpq_clear_z_with_rsp(const surface_t *surf_z, uint16_t zvalue) +{ + static uint8_t temp_buffer[1280] __attribute__((aligned(16))); + static int last_buffer_size = 0; + static uint32_t last_rspsize = 0; + + int nbytes = surf_z->height * surf_z->stride; + + // Check if we need to recalculate the RSP DMA size for a buffer of this size. + // Since the calculation is marginally expensive, we cache it across frames. + if (nbytes != last_buffer_size) { + last_buffer_size = nbytes; + last_rspsize = calc_rspdma_size(nbytes); + } + + // If RSP size is 0, it means that we cannot represent the current size with + // a single RSP DMA command. In this case, we fall back to the normal RDP clear + if (last_rspsize == 0) + return false; + + // We need a RDP fence here because the RDP might be drawing to the Z buffer + // at this point. So first force the RSP to wait for the RDP to finish. + rdpq_fence(); + rspq_write(RDPQ_OVL_ID, RDPQ_CMD_CLEAR_ZBUFFER, + PhysicalAddr(surf_z->buffer), last_rspsize, PhysicalAddr(temp_buffer), zvalue); + return true; +} + bool rdpq_is_attached(void) { return attach_stack_ptr > 0; @@ -41,19 +84,21 @@ static void attach(const surface_t *surf_color, const surface_t *surf_z, bool cl "Color and Z buffers must have the same size"); if (clear_z) { - rdpq_set_color_image(surf_z); - rdpq_set_mode_fill(color_from_packed16(0xFFFC)); - rdpq_fill_rectangle(0, 0, surf_z->width, surf_z->height); + if (!__rdpq_clear_z_with_rsp(surf_z, ZBUF_MAX)) { + rdpq_set_color_image(surf_z); + rdpq_set_mode_fill(color_from_packed16(ZBUF_MAX)); + rdpq_fill_rectangle(0, 0, surf_z->width, surf_z->height); + } } } + rdpq_set_z_image(surf_z); + rdpq_set_color_image(surf_color); if (clear_clr) { - rdpq_set_color_image(surf_color); rdpq_set_mode_fill(color_from_packed32(0x000000FF)); rdpq_fill_rectangle(0, 0, surf_color->width, surf_color->height); } - rdpq_set_color_image(surf_color); if (clear_clr || clear_z) rdpq_mode_pop(); @@ -107,6 +152,11 @@ void __rdpq_clear_z(const uint16_t *z) const surface_t *surf_z = attach_stack[attach_stack_ptr-1][1]; assertf(surf_z, "No Z buffer is currently attached"); + // Try first to clear the Z buffer using the RSP. It should always be + // possible unless the surface has some very unexpected size. + if (z && __rdpq_clear_z_with_rsp(surf_z, *z)) + return; + // Disable autoscissor, so that when we attach to the Z buffer, we // keep the previous scissor rect. This is probably expected by the user // for symmetry with rdpq_clear that does respect the scissor rect. @@ -135,6 +185,21 @@ void rdpq_detach_show(void) rdpq_detach_cb((void (*)(void*))display_show, (void*)attach_stack[attach_stack_ptr-1][0]); } +void __rdpq_attach_close(void) +{ + assertf(attach_stack_ptr == 0, "rdpq_close called but attach stack is not empty"); + attach_stack_ptr = 0; +} + +const surface_t* rdpq_get_attached(void) +{ + if (rdpq_is_attached()) { + return attach_stack[attach_stack_ptr-1][0]; + } else { + return NULL; + } +} + /* Extern inline instantiations. */ extern inline void rdpq_clear(color_t color); extern inline void rdpq_clear_z(uint16_t z); diff --git a/src/rdpq/rdpq_debug.c b/src/rdpq/rdpq_debug.c index 19016ce60..ea502aab1 100644 --- a/src/rdpq/rdpq_debug.c +++ b/src/rdpq/rdpq_debug.c @@ -98,7 +98,8 @@ typedef struct { struct { bool persp, detail, sharpen, lod; } tex; struct { bool enable; uint8_t type; } tlut; uint8_t sample_type; - uint8_t tf_mode; + uint8_t tf_mode0; + uint8_t tf_mode1; bool chromakey; struct { uint8_t rgb, alpha; } dither; struct blender_s { uint8_t p, a, q, b; } blender[2]; @@ -405,6 +406,7 @@ void rdpq_debug_stop(void) rspq_write(RDPQ_OVL_ID, RDPQ_CMD_SET_DEBUG_MODE, 0); } +/** @brief Install a hook to be called for each RDP command */ void rdpq_debug_install_hook(void (*hook)(void*, uint64_t*, int), void* ctx) { for (int i=0;i]\n"); else fprintf(out, "cyc1=[(%s-%s)*%s+%s, (%s-%s)*%s+%s]\n", - rgb_suba[cc.cyc[1].rgb.suba], rgb_subb[cc.cyc[1].rgb.subb], rgb_mul[cc.cyc[1].rgb.mul], rgb_add[cc.cyc[1].rgb.add], - alpha_addsub[cc.cyc[1].alpha.suba], alpha_addsub[cc.cyc[1].alpha.subb], alpha_mul[cc.cyc[1].alpha.mul], alpha_addsub[cc.cyc[1].alpha.add]); + cyc1_rgb_suba[cc.cyc[1].rgb.suba], cyc1_rgb_subb[cc.cyc[1].rgb.subb], cyc1_rgb_mul[cc.cyc[1].rgb.mul], cyc1_rgb_add[cc.cyc[1].rgb.add], + cyc1_alpha_addsub[cc.cyc[1].alpha.suba], cyc1_alpha_addsub[cc.cyc[1].alpha.subb], cyc1_alpha_mul[cc.cyc[1].alpha.mul], cyc1_alpha_addsub[cc.cyc[1].alpha.add]); } return; case 0x35: { fprintf(out, "SET_TILE "); uint8_t f = BITS(buf[0], 53, 55); @@ -1044,8 +1055,6 @@ static int cc_2cyc_use_tex0(void) { /** @brief Non-zoer if the current CC uses the TEX1 slot aka the second texture, in 2cycle mode */ static int cc_2cyc_use_tex1(void) { struct cc_cycle_s *cc = rdp.cc.cyc; - if ((rdp.som.tf_mode & 3) == 1) // TEX1 is the color-conversion of TEX0, so TEX1 is not used - return false; int ret = 0; // Cycle0: reference to TEX1/TEX1_ALPHA slot if (cc[0].rgb.suba == 2 || cc[0].rgb.subb == 2 || cc[0].rgb.mul == 2 || cc[0].rgb.mul == 9 || cc[0].rgb.add == 2) @@ -1151,25 +1160,25 @@ static void lazy_validate_rendermode(void) { if (rdp.som.cycle_type == 0) { // 1cyc VALIDATE_ERR_CC(ccs[1].rgb.suba != 0 && ccs[1].rgb.subb != 0 && ccs[1].rgb.mul != 0 && ccs[1].rgb.add != 0 && ccs[1].alpha.suba != 0 && ccs[1].alpha.subb != 0 && ccs[1].alpha.add != 0, - "in 1cycle mode, the color combiner cannot access the COMBINED slot"); + "in 1cycle mode, the color combiner cannot access the COMBINED slot (RH#003)"); VALIDATE_ERR_CC(ccs[1].rgb.suba != 2 && ccs[1].rgb.subb != 2 && ccs[1].rgb.mul != 2 && ccs[1].rgb.add != 2 && ccs[1].alpha.suba != 2 && ccs[1].alpha.subb != 2 && ccs[1].alpha.mul != 2 && ccs[1].alpha.add != 2, - "in 1cycle mode, the color combiner cannot access the TEX1 slot"); + "in 1cycle mode, the color combiner cannot access the TEX1 slot (RH#001)"); VALIDATE_ERR_CC(ccs[1].rgb.mul != 7, - "in 1cycle mode, the color combiner cannot access the COMBINED_ALPHA slot"); + "in 1cycle mode, the color combiner cannot access the COMBINED_ALPHA slot (RH#003)"); VALIDATE_ERR_CC(ccs[1].rgb.mul != 9, - "in 1cycle mode, the color combiner cannot access the TEX1_ALPHA slot"); + "in 1cycle mode, the color combiner cannot access the TEX1_ALPHA slot (RH#001)"); } else { // 2 cyc VALIDATE_ERR_CC(ccs[0].rgb.suba != 0 && ccs[0].rgb.subb != 0 && ccs[0].rgb.mul != 0 && ccs[0].rgb.add != 0 && ccs[0].alpha.suba != 0 && ccs[0].alpha.subb != 0 && ccs[0].alpha.add != 0, - "in 2cycle mode, the color combiner cannot access the COMBINED slot in the first cycle"); + "in 2cycle mode, the color combiner cannot access the COMBINED slot in the first cycle (RH#003)"); VALIDATE_ERR_CC(ccs[1].rgb.suba != 2 && ccs[1].rgb.subb != 2 && ccs[1].rgb.mul != 2 && ccs[1].rgb.add != 2 && ccs[1].alpha.suba != 2 && ccs[1].alpha.subb != 2 && ccs[1].alpha.mul != 2 && ccs[1].alpha.add != 2, - "in 2cycle mode, the color combiner cannot access the TEX1 slot in the second cycle (but TEX0 contains the second texture)"); + "in 2cycle mode, TEX0 in the second cycle accesses the next pixel in the scanline (and random values on the last scanline pixel) (RH#002)"); VALIDATE_ERR_CC(ccs[0].rgb.mul != 7, - "in 2cycle mode, the color combiner cannot access the COMBINED_ALPHA slot in the first cycle"); + "in 2cycle mode, the color combiner cannot access the COMBINED_ALPHA slot in the first cycle (RH#003)"); VALIDATE_ERR_CC(ccs[1].rgb.mul != 9, - "in 2cycle mode, the color combiner cannot access the TEX1_ALPHA slot in the second cycle (but TEX0_ALPHA contains the second texture)"); + "in 2cycle mode, TEX0_ALPHA in the second cycle accesses the next pixel in the scanline (and random values on the last scanline pixel)"); if (rdp.som.alphacmp.enable && !rdp.som.alphacmp.noise) { bool cc1_passthrough = (ccs[1].alpha.mul == 7 && ccs[1].alpha.add == 0); // (any-any)*0+combined cc1_passthrough |= (ccs[1].alpha.suba == ccs[1].alpha.subb && ccs[1].alpha.add == 0); // (same-same)*any+combine @@ -1230,7 +1239,7 @@ static void validate_draw_cmd(bool use_colors, bool use_tex, bool use_z, bool us VALIDATE_ERR_CC(!cc_use_tex1, "cannot draw a non-textured primitive with a color combiner using the TEX1 slot"); VALIDATE_ERR_CC(!cc_use_tex0alpha && !cc_use_tex1alpha, - "cannot draw a non-shaded primitive with a color combiner using the TEX%d_ALPHA slot", cc_use_tex0alpha ? 0 : 1); + "cannot draw a non-textured primitive with a color combiner using the TEX%d_ALPHA slot", cc_use_tex0alpha ? 0 : 1); } if (use_colors) { @@ -1238,9 +1247,9 @@ static void validate_draw_cmd(bool use_colors, bool use_tex, bool use_z, bool us "shaded primitive drawn but neither the color combiner nor the blender use the SHADE/SHADE_ALPHA slots"); } else { VALIDATE_ERR_CC(!cc_use_shade, - "cannot draw a non-shaded primitive with a color combiner using the SHADE slot"); + "cannot draw a non-shaded primitive with a color combiner using the SHADE slot (RH#004)"); VALIDATE_ERR_CC(!cc_use_shadealpha, - "cannot draw a non-shaded primitive with a color combiner using the SHADE_ALPHA slot"); + "cannot draw a non-shaded primitive with a color combiner using the SHADE_ALPHA slot (RH#004)"); VALIDATE_ERR_SOM(!bl_use_shadealpha, "cannot draw a non-shaded primitive with a blender using the SHADE_ALPHA slot"); } @@ -1256,6 +1265,32 @@ static void validate_draw_cmd(bool use_colors, bool use_tex, bool use_z, bool us } break; } + + // In 2-cycle mode, check for rgb.mul being set to COMBINED and receiving a 1 (or greater) input. + // Note "1" corresponds to a 256 value in the combiner unit (and not 255). + // Warn if that may be the case, as rgb.mul starts overflowing from 256 onwards. + if (rdp.som.cycle_type == 1) { // 2cyc + struct cc_cycle_s *ccA = &rdp.cc.cyc[0]; + if (rdp.cc.cyc[1].rgb.mul == 0) { // combined + if (ccA->rgb.mul >= 16 && ccA->rgb.mul <= 31) { // zero + if (ccA->rgb.add == 6) { // one + VALIDATE_WARN_CC(0, "combined rgb passed to mul input and evaluating to 1, overflow will occur"); + } + } else { + // Detect a lerp (where subb == add) + if ((ccA->rgb.subb == 1 && ccA->rgb.add == 1) // tex0 + || (ccA->rgb.subb == 2 && ccA->rgb.add == 2) // tex1 + || (ccA->rgb.subb == 3 && ccA->rgb.add == 3) // prim + || (ccA->rgb.subb == 4 && ccA->rgb.add == 4) // shade + || (ccA->rgb.subb == 5 && ccA->rgb.add == 5) // env + || (ccA->rgb.subb >= 8 && ccA->rgb.add == 7)) { // zero + // Lerps would only be problematic if subb == add == ONE, but subb cannot take the ONE input so lerps are always fine. + } else { + VALIDATE_WARN_CC(0, "combined rgb passed to mul input and exotic combiner detected, be mindful of overflow"); + } + } + } + } } static void validate_busy_pipe(void) { @@ -1310,10 +1345,11 @@ static bool check_loading_crash(int hpixels) { * * @param tidx tile ID * @param cycles Bitfield of cycles in which the tile is accessed + * @param slotid 0 if the tile is accessed via (the real) TEX0, 1 if it is accessed via (the real) TEX1 * @param texcoords Array of texture coordinates (S,T) used by the drawing command. * @param ncoords Number of vertices in the array (the actual array element count will be double this number) */ -static void validate_use_tile_internal(int tidx, int cycles, float *texcoords, int ncoords) { +static void validate_use_tile_internal(int tidx, int cycles, int slotid, float *texcoords, int ncoords) { struct tile_s *tile = &rdp.tile[tidx]; rdp.busy.tile[tidx] = true; bool use_outside = false; @@ -1336,21 +1372,32 @@ static void validate_use_tile_internal(int tidx, int cycles, float *texcoords, i // YUV render mode mistakes in 1-cyc/2-cyc, that is when YUV conversion can be done. // In copy mode, YUV textures are copied as-is if (tile->fmt == 1) { - if (cycles & 1) VALIDATE_ERR_SOM(!(rdp.som.tf_mode & (4>>0)), - "tile %d is YUV but texture filter in cycle %d does not activate YUV color conversion", tidx, 0); - if (cycles & 2) VALIDATE_ERR_SOM(!(rdp.som.tf_mode & (4>>1)), - "tile %d is YUV but texture filter in cycle %d does not activate YUV color conversion", tidx, 1); + // Bilinear filtering a YUV tile requires TF0_RGB & TF1_YUVTEX0 : basically + // you use the first stage to filter the YUV texture, and the second stage to convert it to RGB. + static const char* texinterp[] = { "point", "point", "bilinear", "median" }; if (rdp.som.sample_type > 1) { - static const char* texinterp[] = { "point", "point", "bilinear", "median" }; - VALIDATE_ERR_SOM(rdp.som.tf_mode == 6 && rdp.som.cycle_type == 1, - "tile %d is YUV and %s filtering is active: TF1_YUVTEX0 mode must be configured in SOM", tidx, texinterp[rdp.som.sample_type]); - VALIDATE_ERR_SOM(rdp.som.cycle_type == 1, - "tile %d is YUV and %s filtering is active: 2-cycle mode must be configured", tidx, texinterp[rdp.som.sample_type]); + VALIDATE_ERR_SOM(rdp.som.tf_mode0 == 1 && rdp.som.tf_mode1 == 1, + "tile %d is YUV and %s filtering is active: TF0_RGB & TF1_YUVTEX0 must be configured in SOM", tidx, texinterp[rdp.som.sample_type]); + VALIDATE_ERR_SOM(rdp.som.cycle_type == 1, "YUV bilinear filering requires 2-cycle mode"); + } + + // If the tile is using YUV, correct usage requires it to go through the special TF stage + // for color conversion. Depending on the slot, check how it is being accessed + if (slotid == 0) { + if (rdp.som.sample_type > 1 && rdp.som.tf_mode0 == 1 && rdp.som.tf_mode1 == 1) { + VALIDATE_ERR_SOM(0, "tile %d is YUV, bilinear filering is activated and configured via TF0_RGB & TF1_YUVTEX0, but the tile is accessed via TEX0 instead of TEX1", tidx); + } else { + VALIDATE_ERR_SOM(rdp.som.tf_mode0 == 0, + "tile %d is YUV, is accessed via TEX%d but SOM does not activate TF0_YUV", tidx, slotid); + } + } else { + VALIDATE_ERR_SOM(rdp.som.tf_mode1 == 1 || rdp.som.tf_mode1 == 2, + "tile %d is YUV, is accessed via TEX%d but SOM does not activate TF1_YUV", tidx, slotid); } } else { - if (cycles & 1) VALIDATE_ERR_SOM((rdp.som.tf_mode & (4>>0)), + if (cycles & 1) VALIDATE_ERR_SOM(rdp.som.tf_mode0 == 1, "tile %d is RGB-based, but cycle %d is configured for YUV color conversion; try setting SOM_TF%d_RGB", tidx, 0, 0); - if (cycles & 2) VALIDATE_ERR_SOM((rdp.som.tf_mode & (4>>1)), + if (cycles & 2) VALIDATE_ERR_SOM(rdp.som.tf_mode1 == 2, "tile %d is RGB-based, but cycle %d is configured for YUV color conversion; try setting SOM_TF%d_RGB", tidx, 1, 1); } // Validate clamp/mirror/wrap modes @@ -1408,7 +1455,7 @@ static void validate_use_tile(int tidx, float *texcoords, int ncoords) { if (rdp.som.cycle_type == 3) return; // FILL mode does not use tiles if (rdp.som.cycle_type == 2) { // In COPY mode, the tile is used only in the first cycle - validate_use_tile_internal(tidx, (1<<0), texcoords, ncoords); + validate_use_tile_internal(tidx, (1<<0), 0, texcoords, ncoords); return; } @@ -1419,11 +1466,16 @@ static void validate_use_tile(int tidx, float *texcoords, int ncoords) { if (rdp.som.cycle_type == 1) { int cyc_tex0 = cc_2cyc_use_tex0(); int cyc_tex1 = cc_2cyc_use_tex1(); - if (cyc_tex0) validate_use_tile_internal(tidx0, cyc_tex0, texcoords, ncoords); - if (cyc_tex1) validate_use_tile_internal(tidx1, cyc_tex1, texcoords, ncoords); + if (rdp.som.tf_mode1 == 1) { + // TEX1 is the color-conversion of TEX0, so usages of TEX1 actually refer + // to the same tile as TEX0 + tidx1 = tidx0; + } + if (cyc_tex0) validate_use_tile_internal(tidx0, cyc_tex0, 0, texcoords, ncoords); + if (cyc_tex1) validate_use_tile_internal(tidx1, cyc_tex1, 1, texcoords, ncoords); } else { int cyc_tex0 = cc_1cyc_use_tex0(); - if (cyc_tex0) validate_use_tile_internal(tidx0, cyc_tex0, texcoords, ncoords); + if (cyc_tex0) validate_use_tile_internal(tidx0, cyc_tex0, 0,texcoords, ncoords); } } @@ -1456,7 +1508,7 @@ void rdpq_validate(uint64_t *buf, uint32_t flags, int *r_errs, int *r_warns) tex_fmt_name[rdp.col.fmt], size, size); break; } uint32_t addr = BITS(buf[0], 0, 24); - if (RDPQ_VALIDATE_DETACH_ADDR && addr == RDPQ_VALIDATE_DETACH_ADDR) { + if ((RDPQ_VALIDATE_DETACH_ADDR & addr) == RDPQ_VALIDATE_DETACH_ADDR) { // special case for libdragon: if the address is 0x800000, then it means // that the developer requested to detach the framebuffer. Treat it as // if SET_COLOR_IMAGE was never sent. @@ -1475,7 +1527,7 @@ void rdpq_validate(uint64_t *buf, uint32_t flags, int *r_errs, int *r_warns) validate_busy_pipe(); VALIDATE_ERR(BITS(buf[0], 0, 5) == 0, "Z image must be aligned to 64 bytes"); uint32_t addr = BITS(buf[0], 0, 24); - if (RDPQ_VALIDATE_DETACH_ADDR && addr == RDPQ_VALIDATE_DETACH_ADDR) { + if ((RDPQ_VALIDATE_DETACH_ADDR & addr) == RDPQ_VALIDATE_DETACH_ADDR) { // special case for libdragon: if the address is 0x800000, then it means // that the developer requested to detach the Z buffer. Treat it as // if SET_Z_IMAGE was never sent. @@ -1558,8 +1610,8 @@ void rdpq_validate(uint64_t *buf, uint32_t flags, int *r_errs, int *r_warns) VALIDATE_ERR(t->tmem_addr >= 0x800, "palettes must be loaded in upper half of TMEM (address >= 0x800)"); VALIDATE_WARN(!(low&3) && !(high&3), "lowest 2 bits of palette start/stop must be 0"); VALIDATE_ERR(low>>2 < 256, "palette start index must be < 256"); - VALIDATE_ERR(high>>2 < 256, "palette stop index must be < 256"); VALIDATE_CRASH(low>>2 <= high>>2, "palette stop index is lower than palette start index"); + VALIDATE_ERR((high>>2)+1-(low>>2) <= 256, "palette length must be < 256"); } break; case 0x2F: // SET_OTHER_MODES validate_busy_pipe(); diff --git a/src/rdpq/rdpq_font.c b/src/rdpq/rdpq_font.c index 8d05b284f..b3ba12225 100644 --- a/src/rdpq/rdpq_font.c +++ b/src/rdpq/rdpq_font.c @@ -404,11 +404,13 @@ void rdpq_font_style(rdpq_font_t *fnt, uint8_t style_id, const rdpq_fontstyle_t if (fnt->num_styles == 1) { fnt->num_styles = 16; fnt->styles = calloc(16, sizeof(style_t)); + assertf(fnt->styles, "Out of memory"); memcpy(&fnt->styles[0], &fnt->builtin_style, sizeof(style_t)); } else { int old_styles = fnt->num_styles; fnt->num_styles = MAX(fnt->num_styles*2, MAX_STYLES); fnt->styles = realloc(fnt->styles, fnt->num_styles * sizeof(style_t)); + assertf(fnt->styles, "Out of memory"); memset(&fnt->styles[old_styles], 0, (fnt->num_styles - old_styles) * sizeof(style_t)); } } diff --git a/src/rdpq/rdpq_font_builtin.c b/src/rdpq/rdpq_font_builtin.c index dbb53ed87..0a8108d93 100644 --- a/src/rdpq/rdpq_font_builtin.c +++ b/src/rdpq/rdpq_font_builtin.c @@ -253,12 +253,12 @@ unsigned char __fontdb_monogram[] = { 0xa2, 0x00, 0x2a, 0xa8, 0xaa, 0xa2, 0xaa, 0xaa, 0xa2, 0x8a, 0xaa, 0xaa, 0x8a, 0xaa, 0xa8, 0xa2, 0x2a, 0x8a, 0xaa, 0x2a, 0x88, 0x08, 0x88, 0x08, 0x8a, 0xaa, 0x00, 0x08, 0x88, 0x00, 0x88, 0x80, 0x88, 0x88, 0x88, 0x00, - 0x00, 0x7c, 0x00, 0x04, 0x00, 0x00, 0x06, 0xe8, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x80, 0x00, 0x06, 0x00, 0x00, 0x06, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -500,13 +500,13 @@ unsigned char __fontdb_at01[] = { 0xa5, 0x56, 0xa8, 0x84, 0x84, 0x8a, 0x9a, 0xa9, 0xa8, 0x08, 0x48, 0x00, 0xa5, 0xa6, 0x69, 0xa0, 0xa9, 0x9a, 0x80, 0x84, 0x84, 0x8a, 0x65, 0x55, 0x68, 0x08, 0x88, 0x00, 0xaa, 0xa8, 0xaa, 0x20, 0x22, 0x22, 0x00, 0x88, - 0x88, 0x88, 0xaa, 0xaa, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x04, + 0x88, 0x88, 0xaa, 0xaa, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x06, 0x00, 0x00, 0x05, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x80, 0x00, 0x00, + 0x00, 0x20, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, diff --git a/src/rdpq/rdpq_font_internal.h b/src/rdpq/rdpq_font_internal.h index 146990dfa..66d3ad4ee 100644 --- a/src/rdpq/rdpq_font_internal.h +++ b/src/rdpq/rdpq_font_internal.h @@ -136,7 +136,7 @@ typedef struct rdpq_font_s { * * @param font Font to look up the glyph in * @param codepoint Unicode codepoint of the glyph - * @return int16_t Index of the glyph in the font, or -1 if not found + * @return Index of the glyph in the font, or -1 if not found */ int16_t __rdpq_font_glyph(const rdpq_font_t *font, uint32_t codepoint); diff --git a/src/rdpq/rdpq_internal.h b/src/rdpq/rdpq_internal.h index 972275df8..09104c4d0 100644 --- a/src/rdpq/rdpq_internal.h +++ b/src/rdpq/rdpq_internal.h @@ -10,6 +10,7 @@ #include "pputils.h" #include "rspq.h" +#include "fgeom2d.h" #include "../rspq/rspq_internal.h" /** @brief True if the rdpq module was inited */ @@ -110,13 +111,36 @@ typedef struct rdpq_block_state_s { } rdpq_block_state_t; void __rdpq_block_begin(); +void __rdpq_block_recycle(rdpq_block_t *head); rdpq_block_t* __rdpq_block_end(); void __rdpq_block_free(rdpq_block_t *block); -void __rdpq_block_run(rdpq_block_t *block); +void __rdpq_block_run_with_rdp(rdpq_block_t *block); +void __rdpq_block_run_no_rdp(void); +void __rdpq_block_run_maybe_rdp(void); void __rdpq_block_next_buffer(void); void __rdpq_block_update(volatile uint32_t *wptr); void __rdpq_block_reserve(int num_rdp_commands); +/** Close rdpq_attach subsystem */ +void __rdpq_attach_close(void); + +inline void __rdpq_tracking_state_reset(rdpq_tracking_t *state) { + *state = (rdpq_tracking_t){ + // current autosync status is unknown because blocks can be + // played in any context. So assume the worst: all resources + // are being used. This will cause all SYNCs to be generated, + // which is the safest option. + .autosync = ~0, + // we don't know whether mode changes will be frozen or not + // when the block will play. Assume the worst (and thus + // do not optimize out mode changes). + .mode_freeze = false, + // we don't know the cycle type after we run the block + .cycle_type_known = 0, + .cycle_type_frozen = 0, + }; +} + inline void __rdpq_autosync_use(uint32_t res) { rdpq_tracking.autosync |= res; @@ -127,7 +151,9 @@ void __rdpq_write8(uint32_t cmd_id, uint32_t arg0, uint32_t arg1); void __rdpq_write16(uint32_t cmd_id, uint32_t arg0, uint32_t arg1, uint32_t arg2, uint32_t arg3); void rdpq_triangle_cpu(const rdpq_trifmt_t *fmt, const float *v1, const float *v2, const float *v3); -void rdpq_triangle_rsp(const rdpq_trifmt_t *fmt, const float *v1, const float *v2, const float *v3); +void rdpq_triangle_rsp(const rdpq_trifmt_t *fmt, const float *v1, const float *v2, const float *v3, fm_mat3_t *mtx); + +extern volatile int __rdpq_syncpoint_at_syncfull; ///@cond @@ -161,7 +187,7 @@ void rdpq_triangle_rsp(const rdpq_trifmt_t *fmt, const float *v1, const float *v * @hideinitializer */ #define rdpq_passthrough_write(rdp_cmd) ({ \ - if (__builtin_expect(rspq_in_block(), 0)) { \ + if (__builtin_expect(rspq_block_is_recording(), 0)) { \ extern rdpq_block_state_t rdpq_block_state; \ int nwords = 0; __rdpcmd_count_words(rdp_cmd); \ while (__builtin_expect(rdpq_block_state.wptr + nwords > rdpq_block_state.wend, 0)) \ diff --git a/src/rdpq/rdpq_mat.c b/src/rdpq/rdpq_mat.c new file mode 100644 index 000000000..05460cef8 --- /dev/null +++ b/src/rdpq/rdpq_mat.c @@ -0,0 +1,448 @@ +/** + * @file rdpq_mat.c + * @author Giovanni Bajo + * @brief RDP Command queue: material system + * @ingroup rdpq + * + * ## Material database binary format + * + * A material database (.mdb file) contains multiple materials packed together + * for efficient loading and storage. The binary format is as follows: + * + * Header: + * - 3 bytes: ID string "MDB" + * - 1 byte: Version number (currently 1) + * - 2 bytes: Number of materials in the database (big-endian) + * - 2 bytes: Flags (reserved for future use) + * + * Index Section: + * After the header follows an index section with one entry per material: + * - 1 byte: Name length of the material + * - 1 byte: Total size of the material data (including name) + * + * After the index section, the actual material data follows. Each material + * is stored consecutively using the "Material binary format" described below. + * The materials are stored in the same order as their index entries. + * + * The #rdpq_matdb_load function uses the index to quickly locate a specific + * material by name without having to parse all materials sequentially. + * + * ## Material binary format + * + * A single material is stored in a packed binary format as follows. + * + * - The first byte is the name length (up to 255 characters). + * - The next bytes are the name of the material (without null terminator). + * - The next 2 bytes are the flags, which can be a combination of the various + * MATFLAG_* flags (see rdpq_mat_internal.h). Each flag is a bit that specifies + * whether a certain feature is present in the material. + * - The next byte is the offset of the extension data (since this position in the + * material), which is a variable-length array of extension keys and values. + * See below for a description of the extension system. + * - Then follows the various features of the material, depending on the flags. + * The features are stored in the following order: + * - If MATFLAG_TEXTURE is set, the next 8 bytes are the hashes of two textures + * (tex0 and tex1) that can be found in the texture database. + * - If MATFLAG_COMBINER is set, the next 8 bytes are the combiner formula to + * pass to #rdpq_mode_combiner. + * - If MATFLAG_BLENDER is set, the next 4 bytes are the blender formula to + * pass to #rdpq_mode_blender. + * - If MATFLAG_RMO_AA is set, the next 1 byte is the antialiasing mode to + * pass to #rdpq_mode_antialias. + * - If MATFLAG_RMO_FOG is set, the next 4 bytes are the fog blender formula to + * pass to #rdpq_mode_fog. + * - If MATFLAG_RMO_DITHERING is set, the next 1 byte is the dithering mode to + * pass to #rdpq_mode_dithering. + * - If MATFLAG_RMO_FILTERING is set, the next 1 byte is the filtering mode to + * pass to #rdpq_mode_filter. + * - If MATFLAG_RMO_ZMODE is set, the next 1 byte contains the Z-buffer mode + * (bit 0: enable Z-buffer, bit 1: enable Z-compare) to pass to #rdpq_mode_zbuf. + * - If MATFLAG_RMO_ZPRIM is set, the next 4 bytes are two 16-bit values + * (z_prim and deltaz_prim) to pass to #rdpq_mode_zoverride. + * - If MATFLAG_RMO_PERSP is set, the next 1 byte is the perspective correction + * mode to pass to #rdpq_mode_persp. + * - If MATFLAG_RMO_ACMP is set, the next 1 byte is the alpha compare mode to + * pass to #rdpq_mode_alphacompare. + * - If MATFLAG_UNIFORM_K4K5 is set, the next 2 bytes are the K4 and K5 YUV + * conversion parameters (K4 in upper byte, K5 in lower byte). + * - If MATFLAG_UNIFORM_CHROMAKEY is set, the next 2 bytes are the chroma key + * parameters (currently not implemented). + * - If MATFLAG_UNIFORM_PRIMLODFRAC is set, the next 1 byte is the primitive + * LOD fraction to pass to #rdpq_set_prim_lod_frac. + * - If MATFLAG_UNIFORM_PRIM is set, the next 4 bytes are the packed primitive + * color to pass to #rdpq_set_prim_color. + * - If MATFLAG_UNIFORM_ENV is set, the next 4 bytes are the packed environment + * color to pass to #rdpq_set_env_color. + * - Finally, the material ends with a terminator byte (0xAB). + * + * ## Ext binary format + * + * This is the binary format used to store optional extension keys and values, + * after the terminator byte (0xAB) of the material. The offset to this section + * can be found in the material (see above). + * + * First byte in the extension data is the header, which contains the number + * of extension keys (up to 63). + * + * Then there is the key array. It's a sequence of 2-byte keys (big-endian), which represents + * the lower 16 bits of the hash of the extension key. Keys are sorted in ascending order. + * + * Then there is the value array. It's a sequence of 2-byte metadata entries, one per extension key. + * Each metadata entry contains: the first byte is the flags containing the type (see EXT_FLAG_*), + * and the second byte is the offset of the actual value content from the current position + * (after the value array). + * + * Then the actual values follow, one after the other. Values are stored as follows: + * - int: 4 bytes, big-endian + * - float: 4 bytes, big-endian + * - string: null-terminated string + * - bool: as a special case, the bool value is stored as a single bit (EXT_FLAG_BOOLBIT) + * in the flags byte, no separate value storage needed. + * + * + */ +///@cond +#define _GNU_SOURCE +///@endcond +#include +#include +#include "asset.h" +#include "sprite.h" +#include "rdpq_mat.h" +#include "rdpq_tex.h" +#include "rdpq_debug.h" +#include "rdpq_mode.h" +#include "rdpq_sprite.h" +#include "rdpq_mat_internal.h" +#include "../hashtable_internal.h" +#include "../utils.h" + +/** Fetch a value from the binary packed material */ +#define FETCH(data, type) ({ \ + typedef type unaligned_type __attribute__((aligned(1))); \ + unaligned_type* __value = (unaligned_type*)data; \ + data += sizeof(type); \ + *__value; \ +}) + +/** Decode a binary packed pointer offset */ +#define PTR_DECODE(font, ptr) ((void*)(((uint8_t*)(font)) + (uint32_t)(ptr))) + +// Internal flags used for the material extension system +#define EXT_FLAG_TYPE 0x07 ///< Type of the extension value (mask) +#define EXT_FLAG_BOOLBIT 0x08 ///< Boolean value (in case the type is bool) + +// Types of extension values +#define EXT_TYPE_BOOL 0x00 ///< Extension type: boolean +#define EXT_TYPE_INT 0x01 ///< Extension type: integer +#define EXT_TYPE_STRING 0x02 ///< Extension type: string +#define EXT_TYPE_FLOAT 0x03 ///< Extension type: float + + +/** Global texture cache */ +static hashtable_t tex_cache; +static char *texdb_path; + +static void* tex_cache_loader(uint32_t key) +{ + assertf(texdb_path, "texture DB path not configured; call rdpq_mat_set_texture_path() first"); + + // Convert the key to a hex string within the texdb_path + int pos = strlen(texdb_path) - 8; + for (int i=0; i<8; i++) { + char hexdigit = "0123456789abcdef"[key & 0xF]; + key >>= 4; + texdb_path[pos - i] = hexdigit; + } + + return sprite_load(texdb_path); +} + +void rdpq_mat_set_texture_path(const char *path) +{ + free(texdb_path); + if (path == NULL) { + texdb_path = NULL; + } else { + asprintf(&texdb_path, "%s/00000000.sprite", path); + } +} + +static void mat_load(void *mat) +{ + mat += FETCH(mat, uint8_t); // skip the name + uint16_t flags = FETCH(mat, uint16_t); + mat++; // skip extension offset + + if (flags & MATFLAG_TEXTURE) { + uint32_t tex0 = FETCH(mat, uint32_t); + uint32_t tex1 = FETCH(mat, uint32_t); + + if (tex0) hashtable_insert(&tex_cache, tex0, NULL); + if (tex1) hashtable_insert(&tex_cache, tex1, NULL); + } +} + +static void mat_unload(void *mat) +{ + mat += FETCH(mat, uint8_t); // skip the name + uint16_t flags = FETCH(mat, uint16_t); + mat++; // skip extension offset + + if (flags & MATFLAG_TEXTURE) { + uint32_t tex0 = FETCH(mat, uint32_t); + uint32_t tex1 = FETCH(mat, uint32_t); + + if (tex0) { + sprite_t *tex = hashtable_remove(&tex_cache, tex0); + if (tex) sprite_free(tex); + } + if (tex1) { + sprite_t *tex = hashtable_remove(&tex_cache, tex1); + if (tex) sprite_free(tex); + } + } +} + +rdpq_matdb_t* rdpq_matdb_open(const char *filename) +{ + rdpq_matdb_t *mdb = asset_load(filename, NULL); + assertf(memcmp(mdb->id, "MDB", 3) == 0, "Invalid MDB header: %s", filename); + assertf(mdb->version == 1, "Invalid MDB version (%d): %s", mdb->version, filename); + return mdb; +} + +void rdpq_matdb_close(rdpq_matdb_t *mdb) +{ + free(mdb); +} + +rdpq_mat_t* rdpq_matdb_load(rdpq_matdb_t *mdb, const char *mat_name) +{ + uint8_t *index = mdb->index; + void *ptr = (void*)mdb + sizeof(rdpq_matdb_t) + mdb->num_materials * sizeof(uint8_t) * 2; + + if (!mat_name) { + mat_load(ptr); + return ptr; + } + + int mat_name_len = strlen(mat_name); + for (int i = 0; i < mdb->num_materials; i++) { + uint8_t namelen = *index++; + uint8_t size = *index++; + + if (namelen == mat_name_len) { + assertf(*(uint8_t*)ptr == namelen, "Invalid material name length in MDB"); + if (memcmp(ptr+1, mat_name, namelen) == 0) { + mat_load(ptr); + return ptr; + } + } + + ptr += size; + } + + return NULL; +} + +rdpq_mat_t* rdpq_mat_load_buf(void *buf, int size) +{ + rdpq_mat_t *ptr = buf; + mat_load(ptr); + return ptr; +} + +void rdpq_mat_draw_begin(rdpq_mat_t *mat_ptr) +{ + void *mat = mat_ptr; + int namelen = FETCH(mat, uint8_t); + char *name = (char*)mat; + mat += namelen; + + uint16_t flags = FETCH(mat, uint16_t); + bool has_overrides = flags & MATFLAG_SOM_MASK; + if (has_overrides) { + rdpq_mode_push(); + } + + // Skip extension offset + mat++; + + rdpq_mode_begin(); + + if (flags & MATFLAG_TEXTURE) { + int tex0 = FETCH(mat, uint32_t); + int tex1 = FETCH(mat, uint32_t); + if (tex0) { + if (tex1) rdpq_tex_multi_begin(); + sprite_t *tex0_sprite = hashtable_lookup(&tex_cache, tex0); + rdpq_sprite_upload(TILE0, tex0_sprite, NULL); + if (tex1) { + rdpq_sprite_upload(TILE1, hashtable_lookup(&tex_cache, tex1), NULL); + rdpq_tex_multi_end(); + } + } + } + if (flags & MATFLAG_COMBINER) { + rdpq_combiner_t cc = FETCH(mat, uint64_t); + rdpq_mode_combiner(cc); + } + if (flags & MATFLAG_BLENDER) { + rdpq_blender_t bl = FETCH(mat, uint32_t); + rdpq_mode_blender(bl); + } + if (flags & MATFLAG_RMO_AA) { + rdpq_antialias_t aa = FETCH(mat, uint8_t); + rdpq_mode_antialias(aa); + } + if (flags & MATFLAG_RMO_FOG) { + rdpq_blender_t fog = FETCH(mat, uint32_t); + rdpq_mode_fog(fog); + } + if (flags & MATFLAG_RMO_DITHERING) { + rdpq_dither_t dither = FETCH(mat, uint8_t); + rdpq_mode_dithering(dither); + } + if (flags & MATFLAG_RMO_FILTERING) { + rdpq_filter_t filter = FETCH(mat, uint8_t); + rdpq_mode_filter(filter); + } + if (flags & MATFLAG_RMO_ZMODE) { + uint8_t zmode = FETCH(mat, uint8_t); + rdpq_mode_zbuf(zmode & 1, zmode & 2); + } + if (flags & MATFLAG_RMO_ZPRIM) { + int16_t z_prim = FETCH(mat, int16_t); + int16_t deltaz_prim = FETCH(mat, int16_t); + rdpq_mode_zoverride(true, z_prim, deltaz_prim); + } + if (flags & MATFLAG_RMO_PERSP) { + uint8_t persp = FETCH(mat, uint8_t); + rdpq_mode_persp(persp); + } + if (flags & MATFLAG_RMO_ACMP) { + uint8_t acmp = FETCH(mat, uint8_t); + rdpq_mode_alphacompare(acmp); + } + if (flags & MATFLAG_UNIFORM_K4K5) { + uint16_t k4k5 = FETCH(mat, uint16_t); + rdpq_set_yuv_parms(0, 0, 0, 0, k4k5 >> 8, k4k5 & 0xFF); + } + if (flags & MATFLAG_UNIFORM_CHROMAKEY) { + (void)FETCH(mat, uint16_t); + assertf(0, "MATFLAG_UNIFORM_CHROMAKEY not implemented"); + } + if (flags & MATFLAG_UNIFORM_PRIMLODFRAC) { + uint8_t prim_lod_frac = FETCH(mat, uint8_t); + rdpq_set_prim_lod_frac(prim_lod_frac); + } + if (flags & MATFLAG_UNIFORM_PRIM) { + uint32_t prim = FETCH(mat, uint32_t); + rdpq_set_prim_color(color_from_packed32(prim)); + } + if (flags & MATFLAG_UNIFORM_ENV) { + uint32_t env = FETCH(mat, uint32_t); + rdpq_set_env_color(color_from_packed32(env)); + } + + assertf(FETCH(mat, uint8_t) == 0xAB, "material %.*s: missing terminator", namelen, name); + rdpq_mode_end(); +} + +void rdpq_mat_draw_end(rdpq_mat_t *mat_ptr) +{ + void *mat = mat_ptr; + mat += FETCH(mat, uint8_t); // skip the name + uint16_t flags = FETCH(mat, uint16_t); + bool has_overrides = flags & MATFLAG_SOM_MASK; + + if (has_overrides) + rdpq_mode_pop(); +} + +void rdpq_mat_free(rdpq_mat_t *mat) +{ + mat_unload(mat); +} + +static bool mat_ext_lookup(uint8_t **mat_ext, uint32_t ext_key, uint8_t *ext_flags) +{ + uint8_t head = *(*mat_ext)++; + uint8_t *keys = *mat_ext; + + int num_exts = head & 63; + uint8_t *meta = NULL; + + ext_key &= 0xFFFF; + *mat_ext += num_exts * 2; + int left = 0, right = num_exts - 1; + while (left <= right) { + int mid = (left + right) / 2; + uint16_t key = (keys[mid * 2] << 8) | keys[mid * 2 + 1]; + if (key == ext_key) { + meta = *mat_ext + mid * 2; + break; + } + if (key < ext_key) left = mid + 1; + else right = mid - 1; + } + if (!meta) return false; + + *mat_ext += num_exts * 2; + *ext_flags = *meta++; + *mat_ext += *meta++; + return true; +} + +static bool mat_ext_get(void *mat, uint32_t ext_key, void *ext_value, int ext_type) +{ + // Skip name and flag; now mat points to the extension offset + uint8_t namelen = FETCH(mat, uint8_t); + uint8_t *mat_ext = (uint8_t*)mat + namelen + 2; + if (*mat_ext == 0) + return false; + mat_ext += mat_ext[0]; + + uint8_t ext_flags; + if (!mat_ext_lookup(&mat_ext, ext_key, &ext_flags) || + (ext_flags & EXT_FLAG_TYPE) != ext_type) + return false; + + switch (ext_type) { + case EXT_TYPE_BOOL: + *(bool*)ext_value = (ext_flags & EXT_FLAG_BOOLBIT) != 0; + return true; + case EXT_TYPE_INT: + *(uint32_t*)ext_value = (mat_ext[0] << 24) | (mat_ext[1] << 16) | (mat_ext[2] << 8) | mat_ext[3]; + return true; + case EXT_TYPE_STRING: + *(char**)ext_value = (char*)mat_ext; + return true; + case EXT_TYPE_FLOAT: { + uint32_t fi = (mat_ext[0] << 24) | (mat_ext[1] << 16) | (mat_ext[2] << 8) | mat_ext[3]; + *(float*)ext_value = I2F(fi); + } return true; + default: + return false; // unsupported type + } +} + +///@cond +bool __rdpq_mat_ext_get_bool(rdpq_mat_t *mat, uint32_t ext_key, bool *value) +{ return mat_ext_get(mat, ext_key, value, EXT_TYPE_BOOL); } +bool __rdpq_mat_ext_get_int(rdpq_mat_t *mat, uint32_t ext_key, uint32_t *value) +{ return mat_ext_get(mat, ext_key, value, EXT_TYPE_INT); } +bool __rdpq_mat_ext_get_string(rdpq_mat_t *mat, uint32_t ext_key, char **value) +{ return mat_ext_get(mat, ext_key, value, EXT_TYPE_STRING); } +bool __rdpq_mat_ext_get_float(rdpq_mat_t *mat, uint32_t ext_key, float *value) +{ return mat_ext_get(mat, ext_key, value, EXT_TYPE_FLOAT); } +///@endcond + +/** Constructor to initialize the texture cache */ +__attribute__((constructor)) +void __rdpq_mat_init(void) +{ + // Initialize the texture cache + hashtable_init(&tex_cache, 32, tex_cache_loader); +} diff --git a/src/rdpq/rdpq_mat_internal.h b/src/rdpq/rdpq_mat_internal.h new file mode 100644 index 000000000..719bdec58 --- /dev/null +++ b/src/rdpq/rdpq_mat_internal.h @@ -0,0 +1,42 @@ +/** + * @file rdpq_mat_internal.h + * @author Giovanni Bajo + * @brief RDP Command queue: material system + * @ingroup rdpq + */ +#ifndef LIBDRAGON_RDPQ_MAT_INTERNAL_H +#define LIBDRAGON_RDPQ_MAT_INTERNAL_H + +#include + +#define MATFLAG_RMO_AA (1 << 0) ///< Render mode override: antialias +#define MATFLAG_RMO_FOG (1 << 1) ///< Render mode override: fog +#define MATFLAG_RMO_DITHERING (1 << 2) ///< Render mode override: dithering +#define MATFLAG_RMO_FILTERING (1 << 3) ///< Render mode override: filtering +#define MATFLAG_RMO_ZMODE (1 << 4) ///< Render mode override: Z mode +#define MATFLAG_RMO_ZPRIM (1 << 5) ///< Render mode override: Z primitive +#define MATFLAG_RMO_PERSP (1 << 6) ///< Render mode override: perspective correction +#define MATFLAG_RMO_ACMP (1 << 7) ///< Render mode override: alpha compare +#define MATFLAG_RMO_MASK (0xFF) ///< Mask for all render mode overrides + +#define MATFLAG_TEXTURE (1 << 8) ///< Texture is present +#define MATFLAG_BLENDER (1 << 9) ///< Blender formula is present +#define MATFLAG_COMBINER (1 << 10) ///< Combiner formula is present +#define MATFLAG_UNIFORM_K4K5 (1 << 11) ///< Combiner uniforms: K4 and K5 +#define MATFLAG_UNIFORM_CHROMAKEY (1 << 12) ///< Combiner uniforms: key center and key scale +#define MATFLAG_UNIFORM_PRIMLODFRAC (1 << 13) ///< Combiner uniforms: primitive LOD fraction +#define MATFLAG_UNIFORM_ENV (1 << 14) ///< Combiner uniforms: env color +#define MATFLAG_UNIFORM_PRIM (1 << 15) ///< Combiner uniforms: prim color + +#define MATFLAG_SOM_MASK (MATFLAG_RMO_MASK|MATFLAG_BLENDER) ///< Mask for all flags that set other modes + +/** Material database structure */ +typedef struct rdpq_matdb_s { + uint8_t id[3]; ///< ID: "MDB" + uint8_t version; ///< Version of the database + uint16_t num_materials; ///< Number of materials + uint16_t flags; ///< Flags + uint8_t index[]; ///< Material index (name length + size) +} rdpq_matdb_t; + +#endif diff --git a/src/rdpq/rdpq_mode.c b/src/rdpq/rdpq_mode.c index f3bce5f33..36287b34e 100644 --- a/src/rdpq/rdpq_mode.c +++ b/src/rdpq/rdpq_mode.c @@ -8,6 +8,7 @@ #include "rdpq_mode.h" #include "rspq.h" #include "rdpq_internal.h" +#include "yuv.h" /** * @brief Like #rdpq_write, but for mode commands. @@ -117,23 +118,26 @@ void rdpq_set_mode_standard(void) { void rdpq_set_mode_yuv(bool bilinear) { uint64_t cc, som; - if (!bilinear) { - som = SOM_RGBDITHER_NONE | SOM_ALPHADITHER_NONE | SOM_TF0_YUV; - cc = RDPQ_COMBINER1((TEX0, K4, K5, TEX0), (ZERO, ZERO, ZERO, ONE)); - } else { - som = SOM_RGBDITHER_NONE | SOM_ALPHADITHER_NONE | SOM_SAMPLE_BILINEAR | SOM_TF0_RGB | SOM_TF1_YUVTEX0; - cc = RDPQ_COMBINER2((TEX1, K4, K5, TEX1), (ZERO, ZERO, ZERO, ONE), - (ZERO, ZERO, ZERO, COMBINED), (ZERO, ZERO, ZERO, COMBINED)); - } + som = SOM_RGBDITHER_NONE | SOM_ALPHADITHER_NONE | SOM_TF0_YUV | SOM_TF1_YUV; + cc = RDPQ_COMBINER1((TEX0, K4, K5, TEX0), (0, 0, 0, 1)); + __rdpq_reset_render_mode( cc >> 32, cc & 0xFFFFFFFF, som >> 32, som & 0xFFFFFFFF); + if (bilinear) { + rdpq_mode_filter(FILTER_BILINEAR); + cc = RDPQ_COMBINER1((TEX1, K4, K5, TEX1), (0, 0, 0, 1)); + } + rdpq_mode_combiner(cc); // FIXME: this should not be required, but we need it for the mipmap mask + if (!rdpq_tracking.mode_freeze) rdpq_tracking.cycle_type_known = 1; else rdpq_tracking.cycle_type_frozen = 1; - rdpq_set_yuv_parms(179,-44,-91,227,111,43); // BT.601 coefficients (Kr=0.299, Kb=0.114, TV range) + // Set the YUV coefficients (FIXME: make this configurable) + const yuv_colorspace_t *cs = &YUV_BT601_TV; + rdpq_set_yuv_parms(cs->k0, cs->k1, cs->k2, cs->k3, cs->k4, cs->k5); } void rdpq_mode_begin(void) diff --git a/src/rdpq/rdpq_paragraph.c b/src/rdpq/rdpq_paragraph.c index 695a054ea..3a6fe513a 100644 --- a/src/rdpq/rdpq_paragraph.c +++ b/src/rdpq/rdpq_paragraph.c @@ -10,13 +10,10 @@ #include "rdpq_rect.h" #include "debug.h" #include "fmath.h" +#include "../utils.h" #include #include -/// @cond -#define UNLIKELY(x) __builtin_expect(!!(x), 0) -/// @endcond - static void __rdpq_paragraph_builder_newline(int ch_newline); static struct { @@ -32,43 +29,15 @@ static struct { int max_chars; bool skip_current_line; bool must_sort; + const char *last_consumed_ptr; //tracks the position of the last displayable char + const char *last_space_consumed_ptr; //tracks the position of the last displayable space + const char *ellipsis_scan_start; } builder; -uint32_t __utf8_decode(const char **str) -{ - const uint8_t *s = (const uint8_t*)*str; - uint32_t c = *s++; - if (c < 0x80) { - *str = (const char*)s; - return c; - } - if (c < 0xC0) { - *str = (const char*)s; - return 0xFFFD; - } - if (c < 0xE0) { - c = ((c & 0x1F) << 6) | (*s++ & 0x3F); - *str = (const char*)s; - return c; - } - if (c < 0xF0) { - c = ((c & 0x0F) << 12); c |= ((*s++ & 0x3F) << 6); c |= (*s++ & 0x3F); - *str = (const char*)s; - return c; - } - if (c < 0xF8) { - c = ((c & 0x07) << 18); c |= ((*s++ & 0x3F) << 12); c |= ((*s++ & 0x3F) << 6); c |= (*s++ & 0x3F); - *str = (const char*)s; - return c; - } - *str = (const char*)s; - return 0xFFFD; -} - static bool rdpq_paragraph_builder_full(void) { return (builder.parms->height && builder.y - builder.font->descent >= builder.parms->height) - || (builder.max_chars == 0); + || (builder.max_chars <= 0); } void rdpq_paragraph_builder_begin(const rdpq_textparms_t *parms, uint8_t initial_font_id, rdpq_paragraph_t *layout) @@ -88,7 +57,8 @@ void rdpq_paragraph_builder_begin(const rdpq_textparms_t *parms, uint8_t initial int flags = 0; int layout_cap = 32; if (!layout) { - layout = malloc(sizeof(rdpq_paragraph_t) + sizeof(rdpq_paragraph_char_t) * layout_cap); + layout = malloc(sizeof(rdpq_paragraph_t) + sizeof(rdpq_paragraph_char_t) * (layout_cap + 1)); + assertf(layout, "Out of memory"); flags = RDPQ_PARAGRAPH_FLAG_MALLOC; } else { flags = layout->flags & RDPQ_PARAGRAPH_FLAG_MALLOC; @@ -112,6 +82,7 @@ void rdpq_paragraph_builder_begin(const rdpq_textparms_t *parms, uint8_t initial builder.ch_last_space = -1; builder.max_chars = builder.parms->max_chars ? builder.parms->max_chars : INT32_MAX; builder.skip_current_line = rdpq_paragraph_builder_full(); + builder.last_consumed_ptr = NULL; } void rdpq_paragraph_builder_font(uint8_t font_id) @@ -137,7 +108,8 @@ static void paragraph_extend(void) { assertf(builder.layout->flags & RDPQ_PARAGRAPH_FLAG_MALLOC, "paragraph of text is too long and cannot be dynamically extended"); int new_cap = builder.layout->capacity * 2; - builder.layout = realloc(builder.layout, sizeof(rdpq_paragraph_t) + sizeof(rdpq_paragraph_char_t) * new_cap); + builder.layout = realloc(builder.layout, sizeof(rdpq_paragraph_t) + sizeof(rdpq_paragraph_char_t) * (new_cap + 1)); + assertf(builder.layout, "Out of memory"); builder.layout->capacity = new_cap; } @@ -193,6 +165,7 @@ void rdpq_paragraph_builder_span(const char *utf8_text, int nbytes) float ycur = builder.y; int16_t next_index = -1; bool is_tab = false; + builder.ellipsis_scan_start = NULL; /// @cond #define UTF8_DECODE_NEXT() ({ \ @@ -204,8 +177,7 @@ void rdpq_paragraph_builder_span(const char *utf8_text, int nbytes) /// @endcond while (utf8_text < end || next_index >= 0) { - if (UNLIKELY(builder.max_chars <= 0)) - return; + const char *char_start = utf8_text; int16_t index = next_index; next_index = -1; if (index < 0) index = UTF8_DECODE_NEXT(); if (UNLIKELY(index < 0)) continue; @@ -213,11 +185,16 @@ void rdpq_paragraph_builder_span(const char *utf8_text, int nbytes) float xadvance; int8_t xoff2; bool has_kerning; uint8_t atlas_id; __rdpq_font_glyph_metrics(fnt, index, &xadvance, NULL, &xoff2, &has_kerning, &atlas_id); xadvance += builder.parms->char_spacing; - builder.max_chars -= 1; // Check if this is a space character if (UNLIKELY(xoff2 == 0)) { + // If we finished the alloted number of chars, we're done. Check this only + // on spaces so that we don't mess wordwrapping. + if (UNLIKELY(builder.max_chars <= 0)) + return; + builder.ch_last_space = builder.layout->nchars; + builder.last_space_consumed_ptr = builder.last_consumed_ptr; if (UNLIKELY(is_tab)) { if (parms->tabstops) { @@ -243,6 +220,7 @@ void rdpq_paragraph_builder_span(const char *utf8_text, int nbytes) // pixel of relative distance between letters can be very visible). xcur = roundf(xcur); + builder.last_consumed_ptr = char_start; continue; } @@ -258,6 +236,9 @@ void rdpq_paragraph_builder_span(const char *utf8_text, int nbytes) .x = xcur+.5f, .y = ycur+.5f, }; + const char *prev_consumed_ptr = builder.last_consumed_ptr; + builder.last_consumed_ptr = utf8_text; + builder.max_chars -= 1; // Advance the cursor xcur += xadvance * builder.xscale; @@ -277,18 +258,25 @@ void rdpq_paragraph_builder_span(const char *utf8_text, int nbytes) // Check if we are allowed to wrap switch (parms->wrap) { case WRAP_CHAR: - if (!paragraph_wrap(builder.layout->nchars-1, &xcur, &ycur)) + if (!paragraph_wrap(builder.layout->nchars-1, &xcur, &ycur)){ + //char didn't fit. move the pointer to the last consumed char + builder.last_consumed_ptr = prev_consumed_ptr; return; + } break; case WRAP_WORD: // Find the last space in the line if (builder.ch_last_space >= 0) { - if (!paragraph_wrap(builder.ch_last_space, &xcur, &ycur)) + if (!paragraph_wrap(builder.ch_last_space, &xcur, &ycur)){ + //word didn't fit. move the pointer to the last consumed space + builder.last_consumed_ptr = builder.last_space_consumed_ptr; return; + } builder.ch_last_space = -1; break; } builder.layout->nchars -= 1; + builder.last_consumed_ptr = prev_consumed_ptr; // fallthrough! case WRAP_ELLIPSES: { const rdpq_font_t *wfnt = fnt; @@ -340,9 +328,10 @@ void rdpq_paragraph_builder_span(const char *utf8_text, int nbytes) .style_id = wrap_style_id, .glyph = wfnt->ellipsis_glyph, .x = (ellipsis_x + wfnt->ellipsis_advance * i * builder.xscale) + .5f, - .y = wrapch[-1].y + .5f, + .y = wrapch[0].y + .5f, }; } + builder.ellipsis_scan_start = utf8_text; } // fallthrough! case WRAP_NONE: // The text doesn't fit on this line anymore. @@ -455,6 +444,11 @@ rdpq_paragraph_t* rdpq_paragraph_builder_end(void) if (!__rdpq_paragraph_builder_update_bbox_width(builder.ch_line_start, builder.layout->nchars)) builder.layout->nlines -= 1; + // Clamp the number of chars to the maximum allowed (for typewriter effects). Now the text + // has been fully laid out, so we can safely clamp it. + if (builder.parms->max_chars && builder.layout->nchars > builder.parms->max_chars) + builder.layout->nchars = builder.parms->max_chars; + // Finish filling the metrics builder.layout->advance_x = builder.x; builder.layout->advance_y = builder.y; @@ -488,7 +482,8 @@ rdpq_paragraph_t* rdpq_paragraph_builder_end(void) } } - // Make sure there is always a terminator. + // Make sure there is always a terminator. Notice that the array is allocated with + // one extra slot (capactiy + 1) so that we can always add a terminator. assertf(builder.layout->nchars <= builder.layout->capacity, "paragraph too long (%d/%d chars)", builder.layout->nchars, builder.layout->capacity); builder.layout->chars[builder.layout->nchars].sort_key = 0; @@ -563,7 +558,19 @@ rdpq_paragraph_t* __rdpq_paragraph_build(const rdpq_textparms_t *parms, uint8_t if (buf != span) rdpq_paragraph_builder_span(span, buf - span); - *nbytes = buf - utf8_text; + + if(builder.ellipsis_scan_start) { + const char *scan = builder.ellipsis_scan_start; + while(scan < end && scan[0] != '\n') + ++scan; + *nbytes = (scan < end ? scan + 1 : scan) - utf8_text; + } + else if(builder.last_consumed_ptr) + *nbytes = (builder.last_consumed_ptr + (builder.parms->wrap == WRAP_WORD && builder.last_consumed_ptr < end)) + - utf8_text; + else + *nbytes = 0; + return rdpq_paragraph_builder_end(); } @@ -620,4 +627,4 @@ void __rdpq_paragraph_char_check_bitfield(void) ch.atlas_id = 0xBB; ch.style_id = 0xCC; assert((ch.sort_key & 0xFFFFFF00) == 0xAABBCC00); -} +} \ No newline at end of file diff --git a/src/rdpq/rdpq_sprite.c b/src/rdpq/rdpq_sprite.c index 5e8799ac0..e908396e8 100644 --- a/src/rdpq/rdpq_sprite.c +++ b/src/rdpq/rdpq_sprite.c @@ -31,7 +31,8 @@ static void sprite_upload_palette(sprite_t *sprite, int palidx, bool set_mode) // this today, but it could in the future (eg: sharing a palette across // multiple sprites). uint16_t *pal = sprite_get_palette(sprite); - if (pal) rdpq_tex_upload_tlut(pal, palidx*16, fmt == FMT_CI4 ? 16 : 256); + int num_colors = sprite_get_palette_used_colors(sprite); + if (pal) rdpq_tex_upload_tlut(pal, palidx*16, num_colors); } } @@ -52,6 +53,7 @@ int __rdpq_sprite_upload(rdpq_tile_t tile, sprite_t *sprite, const rdpq_texparms sprite_detail_t detail; rdpq_texparms_t detailtexparms = {0}; surface_t detailsurf = sprite_get_detail_pixels(sprite, &detail, &detailtexparms); bool use_detail = detailsurf.buffer != NULL; + bool is_shq = sprite_is_shq(sprite); rdpq_tex_multi_begin(); @@ -113,7 +115,11 @@ int __rdpq_sprite_upload(rdpq_tile_t tile, sprite_t *sprite, const rdpq_texparms if (__builtin_expect(set_mode, 1)) { // Enable/disable mipmapping - if(use_detail) rdpq_mode_mipmap(MIPMAP_INTERPOLATE_DETAIL, num_mipmaps+1); + if(is_shq) { + rdpq_mode_mipmap(MIPMAP_INTERPOLATE_SHQ, num_mipmaps); + rdpq_set_yuv_parms(0, 0, 0, 0, 0, 0xFF); + } + else if(use_detail) rdpq_mode_mipmap(MIPMAP_INTERPOLATE_DETAIL, num_mipmaps+1); else if (num_mipmaps) rdpq_mode_mipmap(MIPMAP_INTERPOLATE, num_mipmaps); else rdpq_mode_mipmap(MIPMAP_NONE, 0); } @@ -131,6 +137,8 @@ int rdpq_sprite_upload(rdpq_tile_t tile, sprite_t *sprite, const rdpq_texparms_t void rdpq_sprite_blit(sprite_t *sprite, float x0, float y0, const rdpq_blitparms_t *parms) { + assertf(!sprite_is_shq(sprite), "SHQ sprites only work with rdpq_sprite_upload, not rdpq_sprite_blit"); + // Upload the palette and configure the render mode sprite_upload_palette(sprite, 0, true); diff --git a/src/rdpq/rdpq_tex.c b/src/rdpq/rdpq_tex.c index 8eb993972..e305e25d6 100644 --- a/src/rdpq/rdpq_tex.c +++ b/src/rdpq/rdpq_tex.c @@ -13,6 +13,7 @@ #include "rdpq_tri.h" #include "rdpq_rect.h" #include "rdpq_tex.h" +#include "rdpq_xform.h" #include "rdpq_tex_internal.h" #include "utils.h" #include "fmath.h" @@ -131,8 +132,8 @@ static int texload_set_rect(tex_loader_t *tload, int s0, int t0, int s1, int t1) if (width != tload->rect.width) { // Calculate he new pitch in TMEM (in bytes). Notice that RGBA32 is special // as texture data is split in two halves, so the pitch can be halved. - int pitch_shift = fmt == FMT_RGBA32 ? 1 : 0; - int stride_mask = fmt == FMT_RGBA32 ? 15 : 7; + int pitch_shift = (fmt == FMT_RGBA32 || fmt == FMT_YUV16) ? 1 : 0; + int stride_mask = (fmt == FMT_RGBA32 || fmt == FMT_YUV16) ? 15 : 7; tload->rect.tmem_pitch = ROUND_UP(TEX_FORMAT_PIX2BYTES(fmt, width) >> pitch_shift, 8); // Verify whether we can use LOAD_BLOCK. The conditions we can verify just by looking at the @@ -168,11 +169,13 @@ static int texload_set_rect(tex_loader_t *tload, int s0, int t0, int s1, int t1) // within the 4K TMEM size). static const uint8_t block_max_lines_table[] = { 20, 42, 26, 14, 19, 32, 13, 28, 26, 8, 9, 4, 4, 5, 20, 13, 18, 3, 6, 3, 2, 16, 2, 2, 3, 14, 2, 13, 2, 1, 12, 4, 2, 2, 2, 2, 2, 2, 4, 10, 0, 1, 2, 9, 0, 1, 8, 0, 2, 0, 1, 0, 1, 8, 0, 0, 1, 0, 1, 0, 2, 0, 0, 1, 0, 6, 0, 0, 4, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - int words = tload->rect.tmem_pitch / 8; - if (words >= 11) + int words = (tload->rect.tmem_pitch << pitch_shift) / 8; + if (words < 11) + tload->rect.block_max_lines = 4096; // arbitrary high number, it will be limited by TMEM size anyway + else if (words - 11 < sizeof(block_max_lines_table)) tload->rect.block_max_lines = block_max_lines_table[words - 11]; else - tload->rect.block_max_lines = 4096; // arbitrary high number, it will be limited by TMEM size anyway + tload->rect.block_max_lines = 0; } else { tload->rect.block_max_lines = 0; } @@ -184,7 +187,7 @@ static int texload_set_rect(tex_loader_t *tload, int s0, int t0, int s1, int t1) // If the height changed, complete filling the rect structure, // and calculate whether we can really use LOAD_BLOCK or not. - int tmem_size = (fmt == FMT_RGBA32 || fmt == FMT_CI4 || fmt == FMT_CI8) ? 2048 : 4096; + int tmem_size = (fmt == FMT_RGBA32 || fmt == FMT_CI4 || fmt == FMT_CI8 || fmt == FMT_YUV16) ? 2048 : 4096; assertf(height * tload->rect.tmem_pitch <= tmem_size, "A rectangle of size %dx%d format %s is too big to fit in TMEM", width, height, tex_format_name(fmt)); tload->rect.width = width; @@ -258,7 +261,7 @@ static void texload_block(tex_loader_t *tload, int s0, int t0, int s1, int t1) tload->load_mode = TEX_LOAD_BLOCK; } - rdpq_load_block(tile_internal, s0, t0, tload->rect.num_texels, (fmt == FMT_RGBA32) ? tload->rect.tmem_pitch*2 : tload->rect.tmem_pitch); + rdpq_load_block(tile_internal, s0, t0, tload->rect.num_texels, (fmt == FMT_RGBA32 || fmt == FMT_YUV16) ? tload->rect.tmem_pitch*2 : tload->rect.tmem_pitch); s0 = s0*4 + tload->rect.s0fx; t0 = t0*4 + tload->rect.t0fx; @@ -361,12 +364,27 @@ int tex_loader_calc_max_height(tex_loader_t *tload, int s0, int s1) texload_set_rect(tload, s0, 0, s1, 1); tex_format_t fmt = surface_get_format(tload->tex); - int tmem_size = (fmt == FMT_RGBA32 || fmt == FMT_CI4 || fmt == FMT_CI8) ? 2048 : 4096; + int tmem_size = (fmt == FMT_RGBA32 || fmt == FMT_CI4 || fmt == FMT_CI8 || fmt == FMT_YUV16) ? 2048 : 4096; return tmem_size / tload->rect.tmem_pitch; } ///@endcond +bool rdpq_tex_can_upload(const surface_t *tex) +{ + tex_format_t fmt = surface_get_format(tex); + int width = tex->width; + + if (TEX_FORMAT_BITDEPTH(fmt) == 4) + width = (width + 1) & ~1; + + int pitch_shift = (fmt == FMT_RGBA32 || fmt == FMT_YUV16) ? 1 : 0; + int tmem_pitch = ROUND_UP(TEX_FORMAT_PIX2BYTES(fmt, width) >> pitch_shift, 8); + int tmem_size = (fmt == FMT_RGBA32 || fmt == FMT_CI4 || fmt == FMT_CI8 || fmt == FMT_YUV16) ? 2048 : 4096; + + return tex->height * tmem_pitch <= tmem_size; +} + int rdpq_tex_upload_sub(rdpq_tile_t tile, const surface_t *tex, const rdpq_texparms_t *parms, int s0, int t0, int s1, int t1) { last_tload = tex_loader_init(tile, tex); @@ -614,11 +632,48 @@ static void tex_xblit(const surface_t *surf, float x0, float y0, const rdpq_blit float v1[5] = { k1x, k1y, s1, t0, 1.0f }; float v2[5] = { k2x, k2y, s1, t1, 1.0f }; float v3[5] = { k3x, k3y, s0, t1, 1.0f }; - rdpq_triangle(&TRIFMT_TEX, v0, v1, v2); - rdpq_triangle(&TRIFMT_TEX, v0, v2, v3); + + rdpq_trifmt_t trifmt = TRIFMT_TEX; + trifmt.tex_tile = tile; + rdpq_triangle(&trifmt, v0, v1, v2); + rdpq_triangle(&trifmt, v0, v2, v3); + } + + (*ltd)(tile, surf, os0, ot0, os1, ot1, draw_cb, parms->filtering); +} + +__attribute__((noinline)) +static void tex_xblit_xform(const surface_t *surf, float x0, float y0, const rdpq_blitparms_t *parms, large_tex_draw ltd) +{ + rdpq_tile_t tile = parms->tile; + int src_width = parms->width ? parms->width : surf->width; + int src_height = parms->height ? parms->height : surf->height; + int os0 = parms->s0; + int ot0 = parms->t0; + int os1 = os0 + src_width; + int ot1 = ot0 + src_height; + bool flip_x = parms->flip_x; + bool flip_y = parms->flip_y; + float ofs_x = -(os0 + parms->cx); + float ofs_y = -(ot0 + parms->cy); + + float scalex = parms->scale_x == 0 ? 1.0f : parms->scale_x; + float scaley = parms->scale_y == 0 ? 1.0f : parms->scale_y; + rdpq_xform_push(); + rdpq_xform_mult_srt(x0, y0, parms->theta, scalex, scaley); + + void draw_cb(rdpq_tile_t tile, int s0, int t0, int s1, int t1) + { + int ks0 = s0, kt0 = t0, ks1 = s1, kt1 = t1; + + if (flip_x) { ks0 = os1 - s0 + os0 - 1; ks1 = os1 - s1 + os0 - 1; } + if (flip_y) { kt0 = ot1 - t0 + ot0 - 1; kt1 = ot1 - t1 + ot0 - 1; } + + rdpq_xform_texture_rectangle(tile, ofs_x + ks0, ofs_y + kt0, ofs_x + ks1, ofs_y + kt1, s0, t0); } (*ltd)(tile, surf, os0, ot0, os1, ot1, draw_cb, parms->filtering); + rdpq_xform_pop(); } /** @brief Internal implementation of #rdpq_tex_blit, using a custom large tex loader callback function */ @@ -626,9 +681,13 @@ void __rdpq_tex_blit(const surface_t *surf, float x0, float y0, const rdpq_blitp { static const rdpq_blitparms_t default_parms = {0}; if (!parms) parms = &default_parms; - + + if(parms->allow_xform) { + tex_xblit_xform(surf, x0, y0, parms, ltd); + return; + } // Check which implementation to use, depending on the requested features. - if (F2I(parms->theta) == 0) { + if (F2I(parms->theta) == 0 && !parms->filtering) { if (F2I(parms->scale_x) == 0 && F2I(parms->scale_y) == 0) tex_xblit_norotate_noscale(surf, x0, y0, parms, ltd); else @@ -645,13 +704,12 @@ void rdpq_tex_blit(const surface_t *surf, float x0, float y0, const rdpq_blitpar void rdpq_tex_upload_tlut(uint16_t *tlut, int color_idx, int num_colors) { - // TODO: this is a conservative limit. It should be possible to workaround - // this limit by playing with the tlut pointer passed to SET_TEX_IMAGE and - // then adjust the first_color offset in rdpq_load_tlut_raw. - assertf((PhysicalAddr(tlut) & 7) == 0, "TLUT pointer must be 8-byte aligned"); - rdpq_set_texture_image_raw(0, PhysicalAddr(tlut), FMT_RGBA16, 256, 1); - rdpq_set_tile(RDPQ_TILE_INTERNAL, FMT_I4, TMEM_PALETTE_ADDR + color_idx*4*2, 256, NULL); - rdpq_load_tlut_raw(RDPQ_TILE_INTERNAL, 0, num_colors); + assert(num_colors > 0); + int init_offset = (PhysicalAddr(tlut) & 7) / 2; + tlut -= init_offset; + rdpq_set_texture_image_raw(0, PhysicalAddr(tlut), FMT_RGBA16, 256+4, 1); + rdpq_set_tile(RDPQ_TILE_INTERNAL, FMT_I4, TMEM_PALETTE_ADDR + color_idx*4*2, 256+8, NULL); + rdpq_load_tlut_raw(RDPQ_TILE_INTERNAL, init_offset, num_colors); } void rdpq_tex_multi_begin(void) @@ -671,5 +729,5 @@ int rdpq_tex_multi_end(void) rdpq_set_tile_autotmem(-1); --multi_upload.used; assert(multi_upload.used >= 0); - return 0; + return multi_upload.bytes; } diff --git a/src/rdpq/rdpq_text.c b/src/rdpq/rdpq_text.c index 0888fb234..cad6b0ed0 100644 --- a/src/rdpq/rdpq_text.c +++ b/src/rdpq/rdpq_text.c @@ -32,7 +32,7 @@ const rdpq_font_t* rdpq_text_get_font(uint8_t font_id) { } /** @brief Inner implementation of #rdpq_text_printn, with explicit memory handling. */ -extern rdpq_paragraph_t* __rdpq_paragraph_build(const rdpq_textparms_t *parms, uint8_t initial_font_id, const char *utf8_text, int *nbytes, rdpq_paragraph_t *layout, bool optimize); +extern rdpq_paragraph_t* __rdpq_paragraph_build(const rdpq_textparms_t *parms, uint8_t initial_font_id, const char *utf8_text, int *nbytes, rdpq_paragraph_t *layout); rdpq_textmetrics_t rdpq_text_printn(const rdpq_textparms_t *parms, uint8_t initial_font_id, float x0, float y0, const char *utf8_text, int nbytes) @@ -41,7 +41,7 @@ rdpq_textmetrics_t rdpq_text_printn(const rdpq_textparms_t *parms, uint8_t initi memset(layout, 0, sizeof(*layout)); layout->capacity = nbytes+1; - layout = __rdpq_paragraph_build(parms, initial_font_id, utf8_text, &nbytes, layout, false); + layout = __rdpq_paragraph_build(parms, initial_font_id, utf8_text, &nbytes, layout); rdpq_paragraph_render(layout, x0, y0); return (rdpq_textmetrics_t){ .advance_x = layout->advance_x, diff --git a/src/rdpq/rdpq_tri.c b/src/rdpq/rdpq_tri.c index 97ceeebad..6fa43af82 100644 --- a/src/rdpq/rdpq_tri.c +++ b/src/rdpq/rdpq_tri.c @@ -25,6 +25,7 @@ #include "rdpq_constants.h" #include "utils.h" #include "debug.h" +#include "fgeom2d.h" /** @brief Set to 1 to activate tracing of all parameters of all triangles. */ #define TRIANGLE_TRACE 0 @@ -343,7 +344,7 @@ static inline void __rdpq_write_tex_coeffs(rspq_write_t *w, rdpq_tri_edge_data_t rspq_write_arg(w, (DwDy_fixed&0xffff0000)); rspq_write_arg(w, (DsDe_fixed<<16) | (DtDe_fixed&0xffff)); rspq_write_arg(w, (DwDe_fixed<<16)); - rspq_write_arg(w, (DsDy_fixed<<16) | (DtDy_fixed&&0xffff)); + rspq_write_arg(w, (DsDy_fixed<<16) | (DtDy_fixed&0xffff)); rspq_write_arg(w, (DwDy_fixed<<16)); tracef("invw1-mul: %f (%08lx)\n", invw1, (int32_t)(invw1*65536)); @@ -408,6 +409,17 @@ static inline void __rdpq_write_zbuf_coeffs(rspq_write_t *w, rdpq_tri_edge_data_ tracef("dzde: %f (%08llx)\n", DzDe, (uint64_t)(DzDe * 65536.0f)); } +/** @brief Compute the RDPQ_CMD_TRI command ID and its cost in RDP 64-bit words. */ +static uint32_t __rdpq_triangle_cmd(const rdpq_trifmt_t *fmt, uint32_t *cmd_id) +{ + *cmd_id = RDPQ_CMD_TRI; + uint32_t rdp_words = 4; + if (fmt->shade_offset >= 0) { *cmd_id |= 0x4; rdp_words += 8; } + if (fmt->tex_offset >= 0) { *cmd_id |= 0x2; rdp_words += 8; } + if (fmt->z_offset >= 0) { *cmd_id |= 0x1; rdp_words += 2; } + return rdp_words; +} + /** @brief RDP triangle primitive assembled on the CPU */ void rdpq_triangle_cpu(const rdpq_trifmt_t *fmt, const float *v1, const float *v2, const float *v3) { @@ -421,23 +433,11 @@ void rdpq_triangle_cpu(const rdpq_trifmt_t *fmt, const float *v1, const float *v } __rdpq_autosync_use(res); - uint32_t cmd_id = RDPQ_CMD_TRI; - - uint32_t size = 8; - if (fmt->shade_offset >= 0) { - size += 16; - cmd_id |= 0x4; - } - if (fmt->tex_offset >= 0) { - size += 16; - cmd_id |= 0x2; - } - if (fmt->z_offset >= 0) { - size += 4; - cmd_id |= 0x1; - } + uint32_t cmd_id; + uint32_t rdp_words = __rdpq_triangle_cmd(fmt, &cmd_id); + uint32_t size = rdp_words * 2; // rdpq_write_begin's size is in 32-bit words, not 64-bit RDP words - rspq_write_t w = rspq_write_begin(RDPQ_OVL_ID, cmd_id, size); + rspq_write_t w = rdpq_write_begin(rdp_words, RDPQ_OVL_ID, cmd_id, size); if( v1[fmt->pos_offset + 1] > v2[fmt->pos_offset + 1] ) { SWAP(v1, v2); } if( v2[fmt->pos_offset + 1] > v3[fmt->pos_offset + 1] ) { SWAP(v2, v3); } @@ -464,7 +464,7 @@ void rdpq_triangle_cpu(const rdpq_trifmt_t *fmt, const float *v1, const float *v } /** @brief RDP triangle primitive assembled on the RSP */ -void rdpq_triangle_rsp(const rdpq_trifmt_t *fmt, const float *v1, const float *v2, const float *v3) +void rdpq_triangle_rsp(const rdpq_trifmt_t *fmt, const float *v1, const float *v2, const float *v3, fm_mat3_t *mtx) { uint32_t res = AUTOSYNC_PIPE; if (fmt->tex_offset >= 0) { @@ -476,20 +476,26 @@ void rdpq_triangle_rsp(const rdpq_trifmt_t *fmt, const float *v1, const float *v } __rdpq_autosync_use(res); - uint32_t cmd_id = RDPQ_CMD_TRI; - if (fmt->shade_offset >= 0) cmd_id |= 0x4; - if (fmt->tex_offset >= 0) cmd_id |= 0x2; - if (fmt->z_offset >= 0) cmd_id |= 0x1; + uint32_t cmd_id; + uint32_t rdp_words = __rdpq_triangle_cmd(fmt, &cmd_id); const int TRI_DATA_LEN = ROUND_UP((2+1+1+3)*4, 16); const float *vtx[3] = {v1, v2, v3}; for (int i=0;i<3;i++) { const float *v = vtx[i]; - + //Transform vertex + fm_vec2_t pos = {{v[fmt->pos_offset+0], v[fmt->pos_offset+1]}}; + fm_vec3_t temp; + if(mtx != NULL) { + fm_mat3_mul_vec2(&temp, mtx, &pos); + } else { + temp.x = pos.x; + temp.y = pos.y; + } // X,Y: s13.2 - int16_t x = floorf(v[fmt->pos_offset+0] * 4.0f); - int16_t y = floorf(v[fmt->pos_offset+1] * 4.0f); + int16_t x = floorf(temp.x * 4.0f); + int16_t y = floorf(temp.y * 4.0f); int16_t z = 0; if (fmt->z_offset >= 0) { @@ -499,10 +505,10 @@ void rdpq_triangle_rsp(const rdpq_trifmt_t *fmt, const float *v1, const float *v int32_t rgba = 0; if (fmt->shade_offset >= 0) { const float *v_shade = fmt->shade_flat ? v1 : v; - uint32_t r = v_shade[fmt->shade_offset+0] * 255.0; - uint32_t g = v_shade[fmt->shade_offset+1] * 255.0; - uint32_t b = v_shade[fmt->shade_offset+2] * 255.0; - uint32_t a = v_shade[fmt->shade_offset+3] * 255.0; + uint32_t r = v_shade[fmt->shade_offset+0] * 255.0f; + uint32_t g = v_shade[fmt->shade_offset+1] * 255.0f; + uint32_t b = v_shade[fmt->shade_offset+2] * 255.0f; + uint32_t a = v_shade[fmt->shade_offset+3] * 255.0f; rgba = (r << 24) | (g << 16) | (b << 8) | a; } @@ -525,7 +531,7 @@ void rdpq_triangle_rsp(const rdpq_trifmt_t *fmt, const float *v1, const float *v inv_w); } - rspq_write(RDPQ_OVL_ID, RDPQ_CMD_TRIANGLE, + rdpq_write(rdp_words, RDPQ_OVL_ID, RDPQ_CMD_TRIANGLE, 0xC000 | (cmd_id << 8) | (fmt->tex_mipmaps ? (fmt->tex_mipmaps-1) << 3 : 0) | (fmt->tex_tile & 7)); @@ -536,6 +542,6 @@ void rdpq_triangle(const rdpq_trifmt_t *fmt, const float *v1, const float *v2, c #if RDPQ_TRIANGLE_REFERENCE rdpq_triangle_cpu(fmt, v1, v2, v3); #else - rdpq_triangle_rsp(fmt, v1, v2, v3); + rdpq_triangle_rsp(fmt, v1, v2, v3, NULL); #endif } diff --git a/src/rdpq/rdpq_xform.c b/src/rdpq/rdpq_xform.c new file mode 100644 index 000000000..3b6ee2d16 --- /dev/null +++ b/src/rdpq/rdpq_xform.c @@ -0,0 +1,351 @@ +/** + * @file rdpq_xform.c + */ +#include "fgeom2d.h" +#include "rdpq_xform_internal.h" +#include "rdpq_internal.h" +#include "rdpq_xform.h" +#include "rdpq_rect.h" +#include "rdpq_tri.h" + +#include "debug.h" +#include "utils.h" +#include +#include +#include + +/** + * @brief Type of the transformation + * + * This affects the type of primitive required to be generated when drawing + * transformed rectangles (both texture and fill rectangles). + * + * Ordering is based on the primitive that must be emitted after the transformation. + * Earlier values can use more direct paths for emitting the primitive. + */ +typedef enum { + RDPQ_XFORM_NONE, ///< No transformation + RDPQ_XFORM_TRANSLATE, ///< Transformation with only translation + RDPQ_XFORM_SCALE, ///< Any transformation that has no rotation or skew + RDPQ_XFORM_ALL ///< Any 2D transformation +} rdpq_xform_type; + +/** + * @brief Maximum size of matrix stack used by RDPQ Transform + */ +#define RDPQ_XFORM_STACK_SIZE 16 + +/** + * @brief Data for RDPQ transformation + */ +typedef struct rdpq_xform_s { + fm_mat3_t mtx; ///< 3x3 Transformation matrix associated with transformation + rdpq_xform_type type; ///< Type of transformation +} rdpq_xform_t; + +static rdpq_xform_t *curr_xform; +static rdpq_xform_t xform_stack[RDPQ_XFORM_STACK_SIZE]; +static int xform_stack_depth; + +void __rdpq_xform_init(void) +{ + //Initialize matrix stack to point to top + xform_stack_depth = 0; + curr_xform = &xform_stack[xform_stack_depth]; + rdpq_xform_identity(); +} + +void __rdpq_xform_close(void) +{ + //Make sure that trying to push/load matrices causes crashes + curr_xform = NULL; +} + +static rdpq_xform_type get_mtx_type(fm_mat3_t *mtx) +{ + if(mtx->m[0][1] != 0.0f || mtx->m[1][0] != 0.0f) { //Check for skew/rotation + return RDPQ_XFORM_ALL; + } else if(mtx->m[0][0] != 1.0f || mtx->m[1][1] != 1.0f) { //Check for non-identity scale + return RDPQ_XFORM_SCALE; + } else if(mtx->m[2][0] != 0.0f || mtx->m[2][1] != 0.0f) { //Check for translation + return RDPQ_XFORM_TRANSLATE; + } else { + return RDPQ_XFORM_NONE; + } +} + +void rdpq_xform_mtx_load(fm_mat3_t *mtx) +{ + curr_xform->mtx = *mtx; + curr_xform->type = get_mtx_type(mtx); +} + +void rdpq_xform_mtx_mult(fm_mat3_t *mtx) +{ + fm_mat3_mul(&curr_xform->mtx, &curr_xform->mtx, mtx); + //Prefer least strict transformation type + int type = get_mtx_type(mtx); + if(type > curr_xform->type) { + curr_xform->type = type; + } +} + +void rdpq_xform_identity(void) +{ + fm_mat3_t mtx; + fm_mat3_identity(&mtx); + rdpq_xform_mtx_load(&mtx); +} + +void rdpq_xform_rotate(float theta) +{ + fm_mat3_t mtx; + fm_mat3_from_rotation(&mtx, theta); + rdpq_xform_mtx_mult(&mtx); +} + +void rdpq_xform_translate(float x, float y) +{ + fm_mat3_t mtx; + fm_vec2_t translate = {{ x, y }}; + fm_mat3_from_translation(&mtx, &translate); + rdpq_xform_mtx_mult(&mtx); +} + +void rdpq_xform_scale(float x, float y) +{ + fm_mat3_t mtx; + fm_vec2_t scale = {{ x, y }}; + fm_mat3_from_scale(&mtx, &scale); + rdpq_xform_mtx_mult(&mtx); +} + +void rdpq_xform_load_rst(float x, float y, float theta, float scale_x, float scale_y) +{ + fm_mat3_t mtx; + fm_vec2_t scale = {{ scale_x, scale_y }}; + fm_vec2_t translate = {{ x, y }}; + fm_mat3_from_rst(&mtx, theta, &scale, &translate); + rdpq_xform_mtx_load(&mtx); +} + +void rdpq_xform_mult_rst(float x, float y, float theta, float scale_x, float scale_y) +{ + fm_mat3_t mtx; + fm_vec2_t scale = {{ scale_x, scale_y }}; + fm_vec2_t translate = {{ x, y }}; + fm_mat3_from_rst(&mtx, theta, &scale, &translate); + rdpq_xform_mtx_mult(&mtx); +} + +void rdpq_xform_load_srt(float x, float y, float theta, float scale_x, float scale_y) +{ + fm_mat3_t mtx; + fm_vec2_t scale = {{ scale_x, scale_y }}; + fm_vec2_t translate = {{ x, y }}; + fm_mat3_from_srt(&mtx, &scale, theta, &translate); + rdpq_xform_mtx_load(&mtx); +} + +void rdpq_xform_mult_srt(float x, float y, float theta, float scale_x, float scale_y) +{ + fm_mat3_t mtx; + fm_vec2_t scale = {{ scale_x, scale_y }}; + fm_vec2_t translate = {{ x, y }}; + fm_mat3_from_srt(&mtx, &scale, theta, &translate); + rdpq_xform_mtx_mult(&mtx); +} + +void rdpq_xform_push(void) +{ + int new_depth = xform_stack_depth+1; + assertf(curr_xform, "RDPQ Transform not Initialized"); + assertf(new_depth < RDPQ_XFORM_STACK_SIZE, "RDPQ Transform Stack has reached maximum depth of %d", RDPQ_XFORM_STACK_SIZE); + //Copy matrix to top of stack + xform_stack[new_depth] = xform_stack[new_depth-1]; + xform_stack_depth = new_depth; + //Update current matrix + curr_xform = &xform_stack[new_depth]; +} + +void rdpq_xform_pop(void) +{ + int new_depth = xform_stack_depth-1; + assertf(curr_xform, "RDPQ Transform not Initialized"); + assertf(new_depth >= 0, "RDPQ Transform Stack is already at depth 0"); + //Update current matrix and depth + curr_xform = &xform_stack[new_depth]; + xform_stack_depth = new_depth; +} + +static fm_vec2_t transform_point(float x, float y) +{ + fm_vec2_t in = {{x, y}}; + fm_vec3_t temp; + fm_mat3_mul_vec2(&temp, &curr_xform->mtx, &in); + return (fm_vec2_t){{temp.x, temp.y}}; +} + +void rdpq_xform_fill_rectangle(float x0, float y0, float x1, float y1) +{ + switch(curr_xform->type) { + case RDPQ_XFORM_NONE: + rdpq_fill_rectangle(x0, y0, x1, y1); + break; + + case RDPQ_XFORM_TRANSLATE: + //Apply X Translation to x0 and x1 + x0 += curr_xform->mtx.m[2][0]; + x1 += curr_xform->mtx.m[2][0]; + //Apply Y Translation to y0 and y1 + y0 += curr_xform->mtx.m[2][1]; + y1 += curr_xform->mtx.m[2][1]; + rdpq_fill_rectangle(x0, y0, x1, y1); + break; + + case RDPQ_XFORM_SCALE: + { + //Transform corner vertex coordinates + fm_vec2_t p0 = transform_point(x0, y0); + fm_vec2_t p1 = transform_point(x1, y1); + rdpq_fill_rectangle(p0.x, p0.y, p1.x, p1.y); + } + break; + + case RDPQ_XFORM_ALL: + { + //Transform corner vertex coordinates + fm_vec2_t p0 = transform_point(x0, y0); + fm_vec2_t p1 = transform_point(x1, y0); + fm_vec2_t p2 = transform_point(x1, y1); + fm_vec2_t p3 = transform_point(x0, y1); + //Draw quadrilateral + rdpq_triangle(&TRIFMT_FILL, p0.v, p1.v, p2.v); + rdpq_triangle(&TRIFMT_FILL, p0.v, p2.v, p3.v); + } + break; + } +} + +void rdpq_xform_texture_rectangle(rdpq_tile_t tile, float x0, float y0, float x1, float y1, float s, float t) +{ + switch(curr_xform->type) { + case RDPQ_XFORM_NONE: + rdpq_texture_rectangle(tile, x0, y0, x1, y1, s, t); + break; + + case RDPQ_XFORM_TRANSLATE: + //Apply X Translation to x0 and x1 + x0 += curr_xform->mtx.m[2][0]; + x1 += curr_xform->mtx.m[2][0]; + //Apply Y Translation to y0 and y1 + y0 += curr_xform->mtx.m[2][1]; + y1 += curr_xform->mtx.m[2][1]; + rdpq_texture_rectangle(tile, x0, y0, x1, y1, s, t); + break; + + case RDPQ_XFORM_SCALE: + { + //Calculate texture coordinates + //Absolute value of width (x1-x0) and height (y1-y0) are needed to + //calculate texture coordinates for flipped textures properly + float s0 = s; + float t0 = t; + float s1 = fabsf(x1-x0)+s0; + float t1 = fabsf(y1-y0)+t0; + //Transform corner vertex coordinates + fm_vec2_t p0 = transform_point(x0, y0); + fm_vec2_t p1 = transform_point(x1, y1); + rdpq_texture_rectangle_scaled(tile, p0.x, p0.y, p1.x, p1.y, s0, t0, s1, t1); + } + break; + + case RDPQ_XFORM_ALL: + { + //Calculate texture coordinates + //Absolute value of width (x1-x0) and height (y1-y0) are needed to + //calculate texture coordinates for flipped textures properly + float s0 = s; + float t0 = t; + float s1 = fabsf(x1-x0)+s0; + float t1 = fabsf(y1-y0)+t0; + //Transform corner vertex coordinates + fm_vec2_t p0 = transform_point(x0, y0); + fm_vec2_t p1 = transform_point(x1, y0); + fm_vec2_t p2 = transform_point(x1, y1); + fm_vec2_t p3 = transform_point(x0, y1); + + //Generate vertex buffers for pair of triangles + float v0[5] = { p0.x, p0.y, s0, t0, 1.0f }; + float v1[5] = { p1.x, p1.y, s1, t0, 1.0f }; + float v2[5] = { p2.x, p2.y, s1, t1, 1.0f }; + float v3[5] = { p3.x, p3.y, s0, t1, 1.0f }; + //Draw quadrilateral + rdpq_triangle(&TRIFMT_TEX, v0, v1, v2); + rdpq_triangle(&TRIFMT_TEX, v0, v2, v3); + } + break; + } +} + +void rdpq_xform_texture_rectangle_scaled(rdpq_tile_t tile, float x0, float y0, float x1, float y1, float s0, float t0, float s1, float t1) +{ + switch(curr_xform->type) { + case RDPQ_XFORM_NONE: + rdpq_texture_rectangle_scaled(tile, x0, y0, x1, y1, s0, t0, s1, t1); + break; + + case RDPQ_XFORM_TRANSLATE: + x0 += curr_xform->mtx.m[2][0]; + x1 += curr_xform->mtx.m[2][0]; + //Apply Y Translation to y0 and y1 + y0 += curr_xform->mtx.m[2][1]; + y1 += curr_xform->mtx.m[2][1]; + rdpq_texture_rectangle_scaled(tile, x0, y0, x1, y1, s0, t0, s1, t1); + break; + + case RDPQ_XFORM_SCALE: + { + //Transform corner vertex coordinates + fm_vec2_t p0 = transform_point(x0, y0); + fm_vec2_t p1 = transform_point(x1, y1); + rdpq_texture_rectangle_scaled(tile, p0.x, p0.y, p1.x, p1.y, s0, t0, s1, t1); + } + break; + + case RDPQ_XFORM_ALL: + { + //Transform corner vertex coordinates + fm_vec2_t p0 = transform_point(x0, y0); + fm_vec2_t p1 = transform_point(x1, y0); + fm_vec2_t p2 = transform_point(x1, y1); + fm_vec2_t p3 = transform_point(x0, y1); + + //Generate vertex buffers for pair of triangles + float v0[5] = { p0.x, p0.y, s0, t0, 1.0f }; + float v1[5] = { p1.x, p1.y, s1, t0, 1.0f }; + float v2[5] = { p2.x, p2.y, s1, t1, 1.0f }; + float v3[5] = { p3.x, p3.y, s0, t1, 1.0f }; + //Draw quadrilateral + rdpq_triangle(&TRIFMT_TEX, v0, v1, v2); + rdpq_triangle(&TRIFMT_TEX, v0, v2, v3); + } + break; + } +} + +void rdpq_xform_triangle(const rdpq_trifmt_t *fmt, const float *v1, const float *v2, const float *v3) +{ + switch(curr_xform->type) { + case RDPQ_XFORM_NONE: + rdpq_triangle(fmt, v1, v2, v3); + break; + + case RDPQ_XFORM_TRANSLATE: + case RDPQ_XFORM_SCALE: + case RDPQ_XFORM_ALL: + //Call RSP Triangle Implementation Directly + rdpq_triangle_rsp(fmt, v1, v2, v3, &curr_xform->mtx); + break; + } +} diff --git a/src/rdpq/rdpq_xform_internal.h b/src/rdpq/rdpq_xform_internal.h new file mode 100644 index 000000000..3c4d5d557 --- /dev/null +++ b/src/rdpq/rdpq_xform_internal.h @@ -0,0 +1,18 @@ +/** + * @file rdpq_xform_internal.h + */ +#ifndef LIBDRAGON_RDPQ_XFORM_INTERNAL_H +#define LIBDRAGON_RDPQ_XFORM_INTERNAL_H + +#include "rdpq.h" + +/** + * @brief Internal function to initialize the RDPQ Transform Library + */ +void __rdpq_xform_init(void); +/** + * @brief Internal function to shut down the RDPQ Transform Library + */ +void __rdpq_xform_close(void); + +#endif diff --git a/src/rdpq/rsp_rdpq.S b/src/rdpq/rsp_rdpq.S index c914bcf0e..a8f7f06cc 100644 --- a/src/rdpq/rsp_rdpq.S +++ b/src/rdpq/rsp_rdpq.S @@ -16,7 +16,7 @@ RSPQ_DefineCommand RDPQCmd_Passthrough8, 8 # 0xC0 NOOP RSPQ_DefineCommand RDPQCmd_SetLookupAddress, 8 # 0xC1 Set lookup address RSPQ_DefineCommand RDPQCmd_RectEx, 8 # 0xC2 Fill Rectangle (esclusive bounds) - RSPQ_DefineCommand RSPQCmd_Noop, 8 # 0xC3 + RSPQ_DefineCommand RDPQCmd_ClearZBuffer, 16 # 0xC3 CLear Z-Buffer RSPQ_DefineCommand RDPQCmd_ResetMode, 16 # 0xC4 Reset Mode (set mode standard) RSPQ_DefineCommand RDPQCmd_SetCombineMode_2Pass, 8 # 0xC5 SET_COMBINE_MODE (two pass) RSPQ_DefineCommand RDPQCmd_PushMode, 4 # 0xC6 Push Mode @@ -35,22 +35,22 @@ RSPQ_DefineCommand RDPQCmd_SetScissorEx, 8 # 0xD2 Set Scissor (exclusive bounds) RSPQ_DefineCommand RDPQCmd_SetPrimColorComponent, 8 # 0xD3 Set Primimive Color Component (minlod or primlod or rgba) RSPQ_DefineCommand RDPQCmd_ModifyOtherModes, 12 # 0xD4 Modify SOM - RSPQ_DefineCommand RSPQCmd_Noop, 8 # 0xD5 + RSPQ_DefineCommand RSPQ_Loop, 8 # 0xD5 RSPQ_DefineCommand RDPQCmd_SetFillColor32, 8 # 0xD6 - RSPQ_DefineCommand RSPQCmd_Noop, 8 # 0xD7 + RSPQ_DefineCommand RSPQ_Loop, 8 # 0xD7 RSPQ_DefineCommand RDPQCmd_SetBlendingMode, 8 # 0xD8 Set Blending Mode RSPQ_DefineCommand RDPQCmd_SetFogMode, 8 # 0xD9 Set Fog Mode - RSPQ_DefineCommand RSPQCmd_Noop, 8 # 0xDA + RSPQ_DefineCommand RDPQCmd_RdpWaitIdle, 4 # 0xDA RDP Wait Idle RSPQ_DefineCommand RDPQCmd_SetCombineMode_1Pass, 16 # 0xDB SET_COMBINE_MODE (one pass) RSPQ_DefineCommand RDPQCmd_AutoTmem_SetAddr, 4 # 0xDC AutoTmem_SetAddr RSPQ_DefineCommand RDPQCmd_AutoTmem_SetTile, 8 # 0xDD AutoTmem_SetTile RSPQ_DefineCommand RDPQCmd_Triangle, 4 # 0xDE Triangle (assembled by RSP) RSPQ_DefineCommand RDPQCmd_TriangleData, 28 # 0xDF Set Triangle Data - RSPQ_DefineCommand RSPQCmd_Noop, 8 # 0xE0 - RSPQ_DefineCommand RSPQCmd_Noop, 8 # 0xE1 - RSPQ_DefineCommand RSPQCmd_Noop, 8 # 0xE2 - RSPQ_DefineCommand RSPQCmd_Noop, 8 # 0xE3 + RSPQ_DefineCommand RSPQ_Loop, 8 # 0xE0 + RSPQ_DefineCommand RSPQ_Loop, 8 # 0xE1 + RSPQ_DefineCommand RSPQ_Loop, 8 # 0xE2 + RSPQ_DefineCommand RSPQ_Loop, 8 # 0xE3 RSPQ_DefineCommand RDPQCmd_Passthrough16, 16 # 0xE4 TEXTURE_RECTANGLE RSPQ_DefineCommand RDPQCmd_Passthrough16, 16 # 0xE5 TEXTURE_RECTANGLE_FLIP RSPQ_DefineCommand RDPQCmd_Passthrough8, 8 # 0xE6 SYNC_LOAD @@ -367,8 +367,11 @@ RDPQCmd_SetFixupImage: ############################################################# .func RDPQCmd_SetColorImage RDPQCmd_SetColorImage: + lbu t1, %lo(RDPQ_TARGET_BITDEPTH) srl t0, a0, 19 andi t0, 3 + andi t1, 0x80 + or t0, t1 # fixup DRAM address using address slots jal RDPQ_FixupAddress sb t0, %lo(RDPQ_TARGET_BITDEPTH) @@ -476,12 +479,61 @@ RDPQCmd_SetLookupAddress: ############################################################# .func RDPQ_FixupAddress RDPQ_FixupAddress: - srl t0, a1, 26 + srl t0, a1, 24 lw t1, %lo(RDPQ_ADDRESS_TABLE)(t0) jr ra add a1, t1 .endfunc + ############################################################# + # RSPQCmd_RdpWaitIdle + # + # Wait until RDP is idle. + # + # *NOTE*: a SYNC_FULL command *must* have been already written + # to the RDP output buffer, otherwise this function will stall + # forever. In fact, once the RDP is running, it never gets back + # to idle state until a SYNC_FULL is found, even if it has no + # more commands to run. + ############################################################# + .func RDPQCmd_RdpWaitIdle +RDPQCmd_RdpWaitIdle: + li t3, DP_STATUS_BUSY + +RdpWait: +#if RSPQ_PROFILE + mfc0 t0, COP0_DP_CLOCK +#endif + mfc0 t2, COP0_DP_STATUS +1: + # Wait for selected RDP status bits to become 0. + and t1, t2, t3 + bnez t1, 1b + mfc0 t2, COP0_DP_STATUS +#if RSPQ_PROFILE + # Measure the time we have been waiting for the RDP and save it to the special "RDP idle" slot. + # Also, subtract this time from the sample that is currently being recorded by adding it to the current start time. + mfc0 t1, COP0_DP_CLOCK + + # If DP_STATUS_BUSY is set we are waiting for a FULL_SYNC. Divert this to yet another special slot. + andi t2, t3, DP_STATUS_BUSY + bnez t2, 1f + li s0, %lo(RSPQ_PROFILESLOT_IDLE_RDPSYNCFULL) + li s0, %lo(RSPQ_PROFILESLOT_IDLE_RDPBUSY) +1: + lw t2, %lo(RSPQ_PROFILE_START_TIME) + subu t1, t0 + and t0, t1, 0xFFFFFF + # Masking the updated start time is not necessary here because that will happen in RSPQ_Loop anyway + add t2, t0 + j RSPQ_ProfileAccumulate + sw t2, %lo(RSPQ_PROFILE_START_TIME) +#else + jr ra + nop +#endif + .endfunc + ############################################################# # RDPQCmd_SyncFull # @@ -492,8 +544,24 @@ RDPQ_FixupAddress: RDPQCmd_SyncFull: # Wait until: # * the previous SYNC_FULL interrupt has been processed by the CPU. + # * any pending RSP interrupt for checkpoint has been processed by the CPU. + # We need to wait for checkpoints so that we can fetch the correct checkpoint ID, + # which in turn is necessary for implementing rdpq_call_deferred. +#if RSPQ_PROFILE + mfc0 t1, COP0_DP_CLOCK +#endif jal SpStatusWait - li t2, SP_STATUS_SIG_RDPSYNCFULL + li t2, SP_STATUS_SIG_RDPSYNCFULL | SP_STATUS_SIG_SYNCPOINT +#if RSPQ_PROFILE + mfc0 t0, COP0_DP_CLOCK + lw t2, %lo(RSPQ_PROFILE_START_TIME) + li s0, %lo(RSPQ_PROFILESLOT_IDLE_RDPSYNCFULLMULTI) + subu t0, t1 + and t0, 0xFFFFFF + add t2, t0 + jal RSPQ_ProfileAccumulate + sw t2, %lo(RSPQ_PROFILE_START_TIME) +#endif # Fetch the current syncpoint ID and put it into the state lw s0, %lo(RDPQ_RDRAM_SYNCPOINT_ADDR) @@ -714,8 +782,12 @@ RDPQ_Triangle_Cull: .func RDPQCmd_SetDebugMode RDPQCmd_SetDebugMode: - jr ra - sb a0, %lo(RDPQ_DEBUG) + lbu t0, %lo(RDPQ_TARGET_BITDEPTH) + beqz a0, 1f + andi t0, 0x7F + ori t0, 0x80 +1: jr ra + sb t0, %lo(RDPQ_TARGET_BITDEPTH) .endfunc @@ -816,5 +888,67 @@ autotmem_lowerlimit: sh t0, %lo(RDPQ_AUTOTMEM_LIMIT) .endfunc + + ####################################################################### + # RDPQCmd_ClearZBuffer + # + # Clear the Z-Buffer by filling it with ZBUF_MAX. This is quite a bit + # faster than using RDP, though it does require a RDP fence to make + # sure the Z-Buffer is not in use anymore + # + # a0: Address of the Z-Buffer + # a1: Size of the Z-Buffer in bytes + # a2: Temporary buffer + # a3: Z-Buffer clear value + # + ####################################################################### + + .func RDPQCmd_ClearZBuffer +RDPQCmd_ClearZBuffer: + # Prepare the fill value in $v01 + mtc2 a3, $v01.e0 + vor $v01, vzero, $v01.e0 + li t7, %lo(_data_end) + + # If this is hit, increase the temporary buffer in __rdpq_clear_z_with_rsp() + assert_le t7, 1280, RDPQ_ASSERT_ZCLEAR_INVALID_BUFFER + + # Backup DMEM + jal ClearZBuf_DMEM_Backup + li t2, DMA_OUT # FIXME: switch to ASYNC here when Ares is fixed + + # Now start filling the whole DMEM with the clear code. Start from the + # end of data, so that we don't risk racing with DMA. + li s4, 1280 + addiu s3, s4, 4096 +1: sqv $v01, 0x00,s4 + sqv $v01, 0x10,s4 + sqv $v01, 0x20,s4 + addiu s4, 0x40 + bne s4, s3, 1b + sqv $v01, -0x10,s4 + + # Clear the Z-Buffer + jal DMAWaitReady + nop + mtc0 $0, COP0_DMA_SPADDR + mtc0 a0, COP0_DMA_RAMADDR + mtc0 a1, COP0_DMA_WRITE + + # Restore DMEM + li t2, DMA_IN + jal_and_j ClearZBuf_DMEM_Backup, RSPQ_Loop + + .endfunc + +ClearZBuf_DMEM_Backup: + # Backup the used DMEM area + li s4, 0 + move s0, a2 + j DMAExec + addiu t0, t7, -1 + + + # Include RDPQ library #include diff --git a/src/rompak.c b/src/rompak.c index cd45ad09f..462c7c5e9 100644 --- a/src/rompak.c +++ b/src/rompak.c @@ -9,22 +9,28 @@ #include "dma.h" #include "debug.h" #include +#include #include #include #define TOC_MAGIC 0x544F4330 ///< Magic ID "TOC0" +static pi_addr_t toc_addr = 0; ///< Address of the TOC in the ROM +static uint32_t toc_cookie = 0; ///< Cookie of the TOC in the ROM + /** @brief ROMPAK TOC header */ typedef struct { uint32_t magic; ///< Magic (#TOC_MAGIC) + uint32_t cookie; ///< Cookie to uniquely identify this ROM uint32_t toc_size; ///< Size of the TOC in bytes - uint32_t entry_size; ///< Size of an entry of the TOC (in bytes) - uint32_t num_entries; ///< Number of entries in the TOC + uint16_t entry_size; ///< Size of an entry of the TOC (in bytes) + uint16_t num_entries; ///< Number of entries in the TOC } header_t; /** @brief ROMPAK TOC entry */ typedef struct { uint32_t offset; ///< Offset of the file in the ROM + uint32_t size; ///< Size of the file in bytes char name[]; ///< Name of the file } entry_t; @@ -38,13 +44,12 @@ static bool extension_match(const char *ext, const char *name) return strcmp(ext, name + name_len - ext_len) == 0; } -uint32_t rompak_search_ext(const char *ext) +static bool rompak_get_header(header_t *header) { static bool rompak_corrupted = false; - static uint32_t toc_addr = 0; if (rompak_corrupted) { - return 0; + return false; } if (toc_addr == 0) { @@ -65,20 +70,44 @@ uint32_t rompak_search_ext(const char *ext) rompak_corrupted = true; return 0; } + toc_cookie = io_read(toc_addr + 4); } - header_t header; - data_cache_hit_writeback_invalidate(&header, sizeof(header_t)); - dma_read(&header, toc_addr, sizeof(header_t)); + data_cache_hit_writeback_invalidate(header, sizeof(header_t)); + dma_read(header, toc_addr, sizeof(header_t)); + // Check if the cookie changed. This can only happen in a very rare + // but possible situation where a flashcart messed up with the original ROM + // contents (eg: a flashcart menu loaded a ROM). This provides some nicer + // experience to flashcart developers by avoiding random/unreadble crashes + // (as the TOC is used to load symbols to populate crash screens). + if (header->cookie != toc_cookie) { + debugf("ROMPAK: Cookie changed (expected 0x%08lx, got 0x%08lx)\n", toc_cookie, header->cookie); + rompak_corrupted = true; + return false; + } + // These asserts prevent a miscompiled TOC from causing a hard-to-diagnose // stack overflow because of alloca. The number 1024 is arbitrary, we just // want to protect against important corruptions (eg: little-endian / big-endian mistakes). - if (header.entry_size >= 1024 || header.num_entries >= 1024) { + if (header->entry_size >= 1024 || header->num_entries >= 1024) { + debugf("ROMPAK: Corrupted TOC detected (entry_size=0x%04x, num_entries=0x%04x)\n", + header->entry_size, header->num_entries); rompak_corrupted = true; - assertf(header.entry_size < 1024, "Corrupted rompak TOC: entry size too big (0x%lx)", header.entry_size); - assertf(header.num_entries < 1024, "Corrupted rompak TOC: too many entries (0x%lx)", header.num_entries); - return 0; + assertf(header->entry_size < 1024, "Corrupted rompak TOC: entry size too big (0x%x)", header->entry_size); + assertf(header->num_entries < 1024, "Corrupted rompak TOC: too many entries (0x%x)", header->num_entries); + return false; + } + + return true; +} + +__attribute__((noinline)) +void rompak_walk(rompak_walk_callback_t callback, void *ctx) +{ + header_t header; + if (!rompak_get_header(&header)) { + return; } entry_t *entry = alloca(header.entry_size); @@ -86,9 +115,103 @@ uint32_t rompak_search_ext(const char *ext) data_cache_hit_writeback_invalidate(entry, header.entry_size); dma_read(entry, toc_addr + sizeof(header_t) + i*header.entry_size, header.entry_size); - if (extension_match(ext, entry->name)) - return 0x10000000 + entry->offset; + if (!callback(ctx, entry->name, 0x10000000 + entry->offset, entry->size)) + return; + } +} + +pi_addr_t rompak_search_ext(const char *ext, size_t *size) +{ + pi_addr_t addr = 0; + + bool walkfn(void *ctx, const char *name, pi_addr_t address, size_t sz) + { + if (extension_match(ext, name)) { + if (size) *size = sz; + addr = address; + return false; + } + return true; } - return 0; + rompak_walk(walkfn, NULL); + return addr; +} + + +void rompak_version_parse(pi_addr_t file_addr, size_t size, rompak_version_callback_t callback, void *ctx) +{ + char *data = alloca(size + 1); + data_cache_hit_writeback_invalidate(data, size); + dma_read(data, file_addr, size); + data[size] = 0; /* sentinel to safely terminate tokens */ + + /* + * Minimal JSON parsing for simplified subset: + * - One top level object + * - Keys must be strings + * - Values are returned as they are, without any parsing + */ + char *p = data, *end = data + size; + while (p < end && *p != '{' && *p != '"') p++; + while (p < end) { + // Search next quote, assume it's the key + while (p < end && *p != '"') { + if (*p == '}') return; + p++; + } + if (p >= end) break; + p++; + + // Parse key until closing quote + char *key = p; + while (p < end) { + if (*p == '\\') { if (++p < end) p++; continue; } + if (*p == '"') { *p++ = '\0'; break; } + p++; + } + if (p >= end) break; + + // Search next colon, assume it's the value separator + while (p < end && *p != ':') { + if (*p == '}') return; + p++; + } + if (p >= end) break; + p++; + + // Skip whitespace + while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++; + if (p >= end) continue; + + char *val = p; + if (*p == '"') { + // String value: return pointer without surrounding quotes + val = ++p; + while (p < end) { + if (*p == '\\') { if (++p < end) p++; continue; } + if (*p == '"') break; + p++; + } + } else { + // "Word" value: return contiguous non-space token up to comma or '}' + while (p < end) { + char c = *p; + if (c == ',' || c == '}' || c == ' ' || c == '\t' || c == '\n' || c == '\r') break; + p++; + } + } + if (p > end) break; + *p++ = '\0'; // terminate right after closing quote + callback(ctx, key, val); + } +} + +/** @brief Initialize rompak module */ +__attribute__((constructor)) +void __rompak_init(void) +{ + // Search for the TOC once at boot, so that we store the cookie immediately + header_t header; + rompak_get_header(&header); } diff --git a/src/rompak_internal.h b/src/rompak_internal.h index 44d1bf29b..c3f385b5f 100644 --- a/src/rompak_internal.h +++ b/src/rompak_internal.h @@ -9,6 +9,9 @@ #define __LIBDRAGON_ROM_INTERNAL_H #include +#include +#include +#include "n64types.h" /** * @defgroup rompak ROM bundle support @@ -29,6 +32,29 @@ * @{ */ +#include "n64types.h" + +/** + * @brief Callback function for rompak walking + * + * @param ctx Context pointer to pass to the callback + * @param name Name of the file + * @param address Physical address of the file in the ROM + * @param size Size of the file + * @return true If the callback should continue walking + * @return false If the callback should stop walking + */ +typedef bool (*rompak_walk_callback_t)(void *ctx, const char *name, pi_addr_t address, size_t size); + + /** + * @brief Walk the rompak and call the callback for each file found + * + * @param callback Callback function to call for each file found + * @param ctx Context pointer to pass to the callback + */ + void rompak_walk(rompak_walk_callback_t callback, void *ctx); + + /** * @brief Search a file in the rompak by extension * @@ -39,10 +65,42 @@ * * @param ext Extension to search for (will be matched case sensitively). * This extension must contain the dot, e.g. ".bin". + * @param size Optional pointer to store the size of the file. * @return Physical address of the file in the ROM, or 0 if the file * doesn't exist or the TOC is not present. */ -uint32_t rompak_search_ext(const char *ext); +pi_addr_t rompak_search_ext(const char *ext, size_t *size); + + + +/** + * @brief Callback function for version file parsing + * + * @param ctx Context pointer to pass to the callback + * @param key Key of the version entry + * @param value Value of the version entry + */ +typedef void (*rompak_version_callback_t)(void *ctx, char *key, char *value); + + +/** + * @brief Parse a version file and call the callback for each key-value pair + * + * Rompak can contain version files which are JSON files that contain versions + * related to libdragon, the toolchain or other dependencies. These files + * have extension ".version". + * + * Version files are in JSON format to facilitate parsing also with PC tools. + * This function provides a minimal, non-validating parser that just extracts + * keys and values are raw strings. + * + * @param file_addr Physical address of the version file in the ROM + * @param size Size of the version file in bytes + * @param callback Callback function to call for each key-value pair + * @param ctx Context pointer to pass to the callback + */ +void rompak_version_parse(pi_addr_t file_addr, size_t size, + rompak_version_callback_t callback, void *ctx); /** @} */ diff --git a/src/rsp.c b/src/rsp.c index 4606edd1b..ed2d57d08 100644 --- a/src/rsp.c +++ b/src/rsp.c @@ -21,6 +21,7 @@ #include "n64sys.h" #include "interrupt.h" #include "rdpq/rdpq_debug_internal.h" +#include "accounting_internal.h" /** * RSP crash handler ucode (rsp_crash.S) @@ -159,7 +160,7 @@ void __rsp_run_async(uint32_t status_flags) void rsp_wait(void) { - RSP_WAIT_LOOP(500) { + ACCT_SCOPE(ACCT_CAT_RSP) RSP_WAIT_LOOP(500) { // Wait for the RSP to halt and the DMA engine to be idle. uint32_t status = *SP_STATUS; if (status & SP_STATUS_HALTED && diff --git a/src/rsp_asm.h b/src/rsp_asm.h new file mode 100644 index 000000000..549d90cbf --- /dev/null +++ b/src/rsp_asm.h @@ -0,0 +1,72 @@ +#ifndef __RSP_ASM +#define __RSP_ASM + +#include + +typedef enum { + VLOAD_BYTE = 0b00000, + VLOAD_HALF = 0b00001, + VLOAD_LONG = 0b00010, + VLOAD_DOUBLE = 0b00011, + VLOAD_QUAD = 0b00100, + VLOAD_REST = 0b00101, + VLOAD_PACK = 0b00110, + VLOAD_UPACK = 0b00111, + VLOAD_HPACK = 0b01000, + VLOAD_FPACK = 0b01001, + VLOAD_TRANSPOSE = 0b01011, +} vload_size_t; + +#define LB 0b100000 +#define LH 0b100001 +#define LW 0b100011 +#define LBU 0b100100 +#define LHU 0b100101 +#define LWU 0b100111 +#define LWC2 0b110010 +#define ADDI 0b001000 +#define COP2 0b010010 + +#define VOR 0b101010 + +inline uint32_t rsp_asm_lwc2(vload_size_t size, uint8_t dst_vreg, uint8_t element, uint16_t offset, uint8_t base_reg) +{ + return (LWC2 << 26) | (base_reg << 21) | (dst_vreg << 16) | (size << 11) | (element << 7) | offset; +} + +inline uint32_t rsp_asm_llv(uint8_t dst_vreg, uint8_t element, uint16_t offset, uint8_t base_reg) +{ + return rsp_asm_lwc2(VLOAD_LONG, dst_vreg, element, offset, base_reg); +} + +inline uint32_t rsp_asm_lbu(uint8_t dst_reg, uint16_t offset, uint8_t base_reg) +{ + return (LBU << 26) | (base_reg << 21) | (dst_reg << 16) | offset; +} + +inline uint32_t rsp_asm_lw(uint8_t dst_reg, uint16_t offset, uint8_t base_reg) +{ + return (LW << 26) | (base_reg << 21) | (dst_reg << 16) | offset; +} + +inline uint32_t rsp_asm_addi(uint8_t rt_reg, uint8_t rs_reg, uint16_t immediate) +{ + return (ADDI << 26) | (rs_reg << 21) | (rt_reg << 16) | immediate; +} + +inline uint32_t rsp_asm_vector_op(uint8_t op, uint8_t vd_reg, uint8_t vs_reg, uint8_t vt_reg, uint8_t element) +{ + return (COP2 << 26) | (1<<25) | (element << 21) | (vt_reg << 16) | (vs_reg << 11) | (vd_reg << 6) | op; +} + +inline uint32_t rsp_asm_vor(uint8_t vd_reg, uint8_t vs_reg, uint8_t vt_reg, uint8_t element) +{ + return rsp_asm_vector_op(VOR, vd_reg, vs_reg, vt_reg, element); +} + +inline uint32_t rsp_asm_vcopy(uint8_t vd_reg, uint8_t vs_reg) +{ + return rsp_asm_vor(vd_reg, 0, vs_reg, 0); +} + +#endif diff --git a/src/rsp_lspr1.S b/src/rsp_lspr1.S new file mode 100644 index 000000000..4de5a42cc --- /dev/null +++ b/src/rsp_lspr1.S @@ -0,0 +1,447 @@ +############################################################ +# rsp_lspr1.S +# +# RSP ucode for BC1/DXT1 (BC1Q) sprite decoding. Decodes BC1 +# block-compressed data into RGBA5551 pixels in RDRAM via SP DMA. +# +# Pipeline: +# 1. CPU calls cmd_lspr1_set_buffers once per sprite to record the +# RDRAM physical addresses of the compressed payload and the +# decoded RGBA5551 destination, plus the destination row stride. +# 2. CPU then enqueues a series of cmd_lspr1_decode_strip commands, +# one per horizontal strip of up to N_STRIP_BLOCKS_MAX blocks. +# Each strip DMAs in its compressed blocks, decodes them with a +# vectorized per-block kernel, and DMAs the four output pixel rows +# back to RDRAM at the appropriate destination row offsets. +# +# The per-block math mirrors src/lspr1.c::lspr1_decode_block() bit +# for bit so the RSP output is bit-exact with the reference C decoder. +# +# Authors: Christopher Bonhage +# Giovanni Bajo +############################################################ + +#include + + .set noreorder + .set noat + + .data + + RSPQ_BeginOverlayHeader + RSPQ_DefineCommand cmd_lspr1_set_buffers, 12 + RSPQ_DefineCommand cmd_lspr1_decode_strip, 12 + RSPQ_EndOverlayHeader + + .align 4 + .ascii " Dragon RSP BC1Q" + + RSPQ_BeginSavedState +RDRAM_IN_BASE: .long 0 # phys addr of payload[] (8-byte aligned) +RDRAM_OUT_BASE: .long 0 # phys addr of decoded RGBA5551 region +RDRAM_OUT_STRIDE: .long 0 # bytes between RGBA rows (= width * 2) + RSPQ_EndSavedState + +# Vector constants: +# e0 = 0x1F (5-bit channel mask for the vectorized endpoint unpack) +# e1 = 2048 (<<11) +# e2 = 64 (<<6) +# e3 = 2 (<<1) +# e4 = 1 (alpha bit for RGBA5551) +# e5 = 0x8000 (vmudl logical >>1) +# e6 = 0x0100 (vmudl logical >>8) +# e7 = 21888 (= 171<<7: vmudm accumulates full products, vd = ACC>>16, +# so a *21888 MAC chain yields (sum*171)>>9 = sum/3) + .align 4 +BC1Q_VCONST: + .half 0x1F, 2048, 64, 2, 1, 0x8000, 0x0100, 21888 +BC1Q_PACKCONST: + .half 2048, 64, 2, 0, 0, 0, 0, 0 +# Per-lane vmudl multipliers that logically shift an RGBA5551 endpoint right so +# each 5-bit channel lands in bits [4:0] ((c*m)>>16): R>>11 (*32), G>>6 (*1024), +# B>>1 (*32768). Lanes 0..2 and 4..6 hold the two endpoints; 3/7 are unused. +BC1Q_UNPACK: + .half 32, 1024, 0x8000, 0, 32, 1024, 0x8000, 0 +# Per-lane left-shift multipliers for the vectorized index extraction. For pixel +# lane j the 2-bit index lives at bits [2j:2j+1] of the 16-bit indices half; a +# vmudn by 2^(14-2j) lands that field in bits [14:15] of lane j. The gather then +# reads only bit15 (index bit1, the sign) and bit14 (index bit0), so the low +# garbage bits left by the multiply are irrelevant and no shift/mask is needed. +BC1Q_LOOKUP: + .half 16384, 4096, 1024, 256, 64, 16, 4, 1 +# Low-half-only pack constants for alpha_mode. Identical to the pack/alpha-bit +# multipliers but zeroed in lanes 4..7, so the packed result is 0 there +# (v_pal.e4 = pal[3] = fully transparent). Four contiguous vectors, loaded via +# lqv at +0/+16/+32/+48. + .align 4 +BC1Q_PACKLO: + .half 2048, 2048, 2048, 2048, 0, 0, 0, 0 # R<<11 in lanes 0..3 + .half 64, 64, 64, 64, 0, 0, 0, 0 # G<<6 + .half 2, 2, 2, 2, 0, 0, 0, 0 # B<<1 + .half 1, 1, 1, 1, 0, 0, 0, 0 # alpha bit + +# Strip buffers. +.equ N_STRIP_BLOCKS_MAX, 32 + + .bss + + .align 4 +INBUF: .ds.b N_STRIP_BLOCKS_MAX * 8 + + .align 4 +# Output layout within OUTBUF (row-interleaved across the strip): +# row 0 of all blocks, contiguous [n_blocks * 8 bytes] +# row 1 of all blocks ... +# row 2 of all blocks +# row 3 of all blocks +# Per-strip row stride = n_blocks * 8 bytes; total = 4 * n_blocks * 8. +OUTBUF: .ds.b N_STRIP_BLOCKS_MAX * 16 * 2 + + .text + +# ============================================================ +# cmd_lspr1_set_buffers +# a0 = RDRAM_IN_BASE (phys addr of payload[]) +# a1 = RDRAM_OUT_BASE (phys addr of decoded RGBA5551 region) +# a2 = RDRAM_OUT_STRIDE (bytes per RGBA row = width * 2) +# ============================================================ + .func cmd_lspr1_set_buffers +cmd_lspr1_set_buffers: + sw a0, %lo(RDRAM_IN_BASE) + sw a1, %lo(RDRAM_OUT_BASE) + jr ra + sw a2, %lo(RDRAM_OUT_STRIDE) + .endfunc + + +# ============================================================ +# cmd_lspr1_decode_strip +# a0 = in_offset_bytes (offset from RDRAM_IN_BASE to first block) +# a1 = out_offset_bytes (offset from RDRAM_OUT_BASE to pixel (0,0) +# of this strip's top-left block) +# a2 = n_blocks (1..N_STRIP_BLOCKS_MAX) +# ============================================================ + .func cmd_lspr1_decode_strip +cmd_lspr1_decode_strip: + #define out_offset s5 + #define n_blocks s6 + #define block_idx s7 + #define row_stride s8 + #define v_const $v16 + #define v_ep0 $v20 + #define v_ep1 $v21 + #define v_tmp0 $v22 + # v_pal: packed RGBA5551 palette for the current block, pal[2] in lane e0, + # pal[3] in lane e4. + #define v_pal $v24 + #define v_unpack $v26 + # Low-half-only pack constants used by alpha_mode (0 in lanes 4..7 so the + # packed pal[3] is 0 = transparent). + #define v_pack_r_lo $v07 + #define v_pack_g_lo $v08 + #define v_pack_b_lo $v23 + #define v_one_lo $v25 + #define v_pack $v28 + #define v_pack_r $v17 + #define v_pack_g $v18 + #define v_pack_b $v19 + #define v_one $v29 + # v_shl: per-lane index-extraction shifts (pixel lookup phase) + #define v_shl $v27 + # Per-block decode scratch (rspq reserves only $v00/$v30/$v31, so $v01..$v15 + # are available here). The 16 pixels are processed as two 8-pixel halves: + # half A = pixels 0..7 (output rows 0/1), half B = pixels 8..15 (rows 2/3). + # v_pp0 / v_pp1 = pal[0] / pal[1], broadcast to all 8 lanes + # v_tA / v_tB = the two 16-bit index words (pixels 0..7 / 8..15) in lane 0 + # v_idxA/ v_idxB = per-lane 2-bit pixel indices for half A / half B + # v_b0A / v_b0B = index bit0 of half A / half B + # v_evA / v_odA = half A even/odd palette candidates (idx&2 == 0 / != 0) + # v_evB / v_odB = half B even/odd palette candidates + # v_rA / v_rB = final packed 8-pixel results of half A / half B + # v_junk = throwaway vlt/vge destination (only VCC matters) + #define v_tA $v01 + #define v_tB $v02 + #define v_idxA $v03 + #define v_idxB $v04 + #define v_pp0 $v05 + #define v_pp1 $v06 + #define v_b0A $v09 + #define v_b0B $v10 + #define v_evA $v11 + #define v_odA $v12 + #define v_evB $v13 + #define v_odB $v14 + #define v_junk $v15 + # v_rA/v_rB share the even-candidate registers; v_tA/v_tB are reserved to hold + # the next block's prefetched index words (see pixel_lookup). + #define v_rA v_evA + #define v_rB v_evB + + # Preserve args we need after DMAIn returns + move out_offset, a1 + move n_blocks, a2 + li t0, %lo(BC1Q_VCONST) + lqv v_const, 0, 0, t0 + li t0, %lo(BC1Q_PACKCONST) + lqv v_pack, 0, 0, t0 + li t0, %lo(BC1Q_UNPACK) + lqv v_unpack, 0, 0, t0 + li t0, %lo(BC1Q_LOOKUP) + lqv v_shl, 0, 0, t0 + li t0, %lo(BC1Q_PACKLO) + lqv v_pack_r_lo, 0, 0, t0 + lqv v_pack_g_lo, 0, 16, t0 + lqv v_pack_b_lo, 0, 32, t0 + lqv v_one_lo, 0, 48, t0 + vcopy v_pack_r, v_pack.e0 + vcopy v_pack_g, v_pack.e1 + vcopy v_pack_b, v_pack.e2 + vcopy v_one, v_const.e4 # 1 in all lanes: alpha bit via MAC + + # ---- DMA compressed blocks into INBUF ---- + lw s0, %lo(RDRAM_IN_BASE) + li s4, %lo(INBUF) + add s0, s0, a0 + sll t0, n_blocks, 3 # n_blocks * 8 (bytes) + li t1, 0 # height=1, pitch ignored + jal DMAIn + addi t0, t0, -1 # DMA_SIZE wants width-1 + + # ---- Decode loop ---- + # A block's 8-byte input (endpoints c0/c1 + the 32-bit index word) is fetched + # one iteration ahead of where it is decoded: while block N is being decoded, + # block N+1's input is loaded (in pixel_lookup) into the registers below. The + # prologue loads block 0, then the loop body begins directly at the unpack. + li block_idx, 0 + sll row_stride, n_blocks, 3 # bytes per strip row in OUTBUF (= n_blocks * 8) + + # ---- Prologue: load block 0's input (same layout as the prefetch below) ---- + # c0/c1 go into lanes e0/e4 of the endpoint vectors (halves swapped between + # ep0/ep1). The two 16-bit index words are read straight from DMEM into lane 0 + # of v_tA/v_tB: in the big-endian 32-bit index word, pixels 0..7 are the low + # u16 (the last two bytes, offset 6) and pixels 8..15 the high u16 (offset 4). + li t6, %lo(INBUF) # &INBUF[0] + lhu k0, 0(t6) # c0 (BE u16 from disk == native u16 on RSP) + lhu k1, 2(t6) # c1 + lsv v_ep0.e0, 0, t6 # c0 -> ep0 lane 0 / ep1 lane 4 (halves swapped) + lsv v_ep0.e4, 2, t6 # c1 + lsv v_ep1.e0, 2, t6 # c1 + lsv v_ep1.e4, 0, t6 # c0 + lsv v_tA.e0, 6, t6 # pixels 0..7 indices (low u16 @ byte 6) + lsv v_tB.e0, 4, t6 # pixels 8..15 indices (high u16 @ byte 4) + sltu v1, k1, k0 # mode: v1 = (c0 > c1) + + # Loop body throughput (one block = 16 pixels): ~38 cycles in opaque mode, + # ~37 in alpha mode, i.e. ~2.4 cycles per decoded pixel. +decode_block: + # ---- Vectorized endpoint unpack (RGBA5551 -> per-channel 5/5/5 lanes) ---- + # Inputs were loaded by the previous iteration (or the prologue for block 0): + # c0/c1 in lanes e0/e4 of v_ep0/v_ep1 (halves swapped), the two 16-bit index + # words in lane 0 of v_tA/v_tB, and v1 = mode flag. The .h0 selector on the + # endpoint vectors broadcasts c0/c1 within each half (lanes 0..3 = c0/ep0, + # 4..7 = c1/ep1); .e0 broadcasts the index word across all lanes. + # + # Three things are produced: + # v_ep0/v_ep1 : each 5-bit channel logically shifted into bits [4:0] via the + # vmudl multipliers (R>>11, G>>6, B>>1), then masked to 5 bits. + # v_pp0/v_pp1 : pal[0]/pal[1] = (c0 | alpha) / (c1 | alpha), one per lane. + # v_idxA/idxB : vmudn lands each pixel's 2-bit index in bits [14:15] of its + # lane (the low bits are ignored later). + # vor must read v_ep0 before vmudl overwrites it. + vor v_pp0, v_one, v_ep0.h0 # [pal0=c0|1 x4 | pal1=c1|1 x4] + vmudl v_ep0, v_unpack, v_ep0.h0 # ep0 channels -> [4:0] + vmudl v_ep1, v_unpack, v_ep1.h0 # ep1 channels -> [4:0] + vmudn v_idxA, v_shl, v_tA.e0 # pixels 0..7 indices + vmudn v_idxB, v_shl, v_tB.e0 # pixels 8..15 indices + vand v_ep0, v_ep0, v_const.e0 # mask to 5 bits + vand v_ep1, v_ep1, v_const.e0 + vcopy v_pp1, v_pp0.e4 # pal[1] in all lanes + # ---- Mode select: c0 > c1 -> opaque (4-color); else alpha (3-color + transp) ---- + beqz v1, alpha_mode + vcopy v_pp0, v_pp0.e0 # pal[0] in all lanes + + # ============================================================ + # OPAQUE MODE + # pal[2] = ((2*r0+r1)/3, (2*g0+g1)/3, (2*b0+b1)/3, A=1) + # pal[3] = ((r0+2*r1)/3, (g0+2*g1)/3, (b0+2*b1)/3, A=1) + # ============================================================ +opaque_mode: + # pal[2]/pal[3] interpolate the two endpoints at 1/3 and 2/3. The /3 is done + # exactly as (sum*171)>>9: vmudm/vmadm accumulate the full 32-bit products in + # the accumulator and expose ACC>>16 as vd, so the sum stays exact and the + # rounding happens once. With a *21888 (= 171<<7) multiplier: + # ACC = (2*ep0 + ep1) * 21888 ; vd = ACC>>16 = (2*ep0+ep1)*171>>9 = sum/3 + # The endpoint vectors hold ep0 in lanes 0..2 and ep1 in lanes 4..6, so: + # lanes 0..2 => pal[2] = (2*ep0 + ep1)/3 + # lanes 4..6 => pal[3] = (2*ep1 + ep0)/3 + vmudm v_tmp0, v_ep0, v_const.e7 + vmadm v_tmp0, v_ep0, v_const.e7 + vmadm v_tmp0, v_ep1, v_const.e7 + # Destination pointer for each of the 4 output rows (used by the stores in + # pixel_lookup): row dy dst = OUTBUF + block_idx*8 + dy*row_stride. + sll t0, block_idx, 3 + addi t0, t0, %lo(OUTBUF) # row 0 + add t1, t0, row_stride # row 1 + add t2, t1, row_stride # row 2 + add t3, t2, row_stride # row 3 + + # ---- Pack pal[2] and pal[3] into v_pal in one vector pass ---- + # All channels are 5-bit (RGBA5551), so v_tmp0 feeds the packers directly. + # Three MACs shift each channel into place (R<<11, G<<6, B<<1) and OR them by + # accumulation; a 4th MAC term (+1*1) sets the RGBA5551 alpha bit. Lane e0 ends + # up holding the packed pal[2], lane e4 the packed pal[3]. + vmudn v_pal, v_pack_r, v_tmp0.h0 + vmadn v_pal, v_pack_g, v_tmp0.h1 + vmadn v_pal, v_pack_b, v_tmp0.h2 + j pixel_lookup + vmadn v_pal, v_one, v_one # +alpha bit (delay slot) + + # ============================================================ + # ALPHA MODE + # pal[2] = ((r0+r1)/2, (g0+g1)/2, (b0+b1)/2, A=1) + # pal[3] = 0x0000 (fully transparent) + # ============================================================ +alpha_mode: + # pal[2] is the midpoint (ep0 + ep1)/2 of the two endpoints. As with the + # opaque /3, the >>1 is done via the accumulator: a *0x8000 (= 1<<15) MAC + # chain accumulates (ep0+ep1)*0x8000 and vd = ACC>>16 = (ep0+ep1)>>1, exact. + vmudm v_tmp0, v_ep0, v_const.e5 + vmadm v_tmp0, v_ep1, v_const.e5 + # Output-row destination pointers (see opaque_mode). + sll t0, block_idx, 3 + addi t0, t0, %lo(OUTBUF) # row 0 + add t1, t0, row_stride # row 1 + add t2, t1, row_stride # row 2 + add t3, t2, row_stride # row 3 + + # ---- Pack pal[2] into v_pal; pal[3] is transparent ---- + # Same 3-MAC channel pack as opaque_mode (R<<11, G<<6, B<<1) plus the alpha + # bit, but using the low-half-only pack constants (0 in lanes 4..7). The result + # is the packed pal[2] in lane e0 and 0 in lane e4 (pal[3] = fully transparent). + vmudn v_pal, v_pack_r_lo, v_tmp0.e0 + vmadn v_pal, v_pack_g_lo, v_tmp0.e1 + vmadn v_pal, v_pack_b_lo, v_tmp0.e2 + vmadn v_pal, v_one_lo, v_one_lo + + # ============================================================ + # PIXEL LOOKUP & STORE (vectorized) + # For each of 16 pixels, idx = (indices >> (2*pixel)) & 3, and + # OUTBUF[dy=pixel/4][block=block_idx][dx=pixel%4] = pal[idx]. + # OUTBUF layout: row-interleaved across the strip, so + # dst_row[dy] = OUTBUF + dy*row_stride + block_idx*8 + # + # The 16 indices are already in v_idxA (pixels 0..7) and v_idxB (pixels 8..15) + # from the unpack phase, with each pixel's 2-bit index sitting in bits [14:15] + # of its lane. pal[0]/pal[1] are in the broadcasts v_pp0/v_pp1; pal[2]/pal[3] + # are in v_pal.e0/.e4. Each half selects pal[idx] per lane with two bit-keyed + # vmrg merges (vmrg vd = VCC ? vs : vt), driven by the two index bits: + # bit1 = sign(idx): vge idx,0 -> VCC=(idx<2); picks even(pal0/pal2)/odd(pal1/pal3) + # bit0 = idx & 0x4000 (=K16384): vlt 0,bit0 -> VCC; picks within the pair + # The 8 packed RGBA5551 pixels of each half are then stored as two rows by sdv. + # ============================================================ +pixel_lookup: + # While this block's gather runs, the right column prefetches the NEXT block's + # input into the registers freed by this block (v_ep0/v_ep1 and v_tA/v_tB): + # endpoints, the two index words, and the mode flag, exactly as the prologue + # loaded block 0. The next INBUF address is INBUF + (block_idx+1)*8. On the + # final iteration this reads one block past the data, which is harmless as it + # is never decoded. + vge v_junk, v_idxA, vzero; sll t6, block_idx, 3 # VCC=(idxA<2); \ next INBUF + vand v_b0A, v_idxA, K16384; addi t6, t6, %lo(INBUF) + 8 # bit0A; / address + vand v_b0B, v_idxB, K16384; lhu k0, 0(t6) # bit0B; next c0 + vmrg v_evA, v_pp0, v_pal.e0; lhu k1, 2(t6) # evenA; next c1 + vmrg v_odA, v_pp1, v_pal.e4; lsv v_tA.e0, 6, t6 # oddA; idx pixels 0..7 + vge v_junk, v_idxB, vzero; lsv v_tB.e0, 4, t6 # VCC=(idxB<2); idx pixels 8..15 + vmrg v_evB, v_pp0, v_pal.e0; lsv v_ep0.e0, 0, t6 # evenB; c0->ep0.e0 + vmrg v_odB, v_pp1, v_pal.e4; lsv v_ep0.e4, 2, t6 # oddB; c1->ep0.e4 + vlt v_junk, vzero, v_b0A; lsv v_ep1.e0, 2, t6 # VCC=bit0A; c1->ep1.e0 + vmrg v_rA, v_odA, v_evA; lsv v_ep1.e4, 0, t6 # half A done; c0->ep1.e4 + vlt v_junk, vzero, v_b0B; sltu v1, k1, k0 # VCC=bit0B; next mode + vmrg v_rB, v_odB, v_evB; addi block_idx, block_idx, 1 # half B done; loop bump + sdv v_rA.e0, 0, t0 # pixels 0..3 -> row 0 + sdv v_rA.e4, 0, t1 # pixels 4..7 -> row 1 + sdv v_rB.e0, 0, t2 # pixels 8..11 -> row 2 + bne block_idx, n_blocks, decode_block # loop + sdv v_rB.e4, 0, t3 # pixels 12..15-> row 3 (delay slot) + + # ============================================================ + # DMA the 4 output rows back to RDRAM. + # Row r destination = RDRAM_OUT_BASE + out_offset + r*RDRAM_OUT_STRIDE. + # Each row carries n_blocks*8 bytes (= row_stride). + # ============================================================ + lw s1, %lo(RDRAM_OUT_BASE) + lw s2, %lo(RDRAM_OUT_STRIDE) + add s1, s1, out_offset # s1 = row 0 RDRAM dst + + # row_stride - 1 (for DMA_SIZE width field) + addi t0, row_stride, -1 + + # Row 0 + move s0, s1 + li s4, %lo(OUTBUF) + li t1, 0 + jal DMAOutAsync + nop + + # Row 1 + add s1, s1, s2 + move s0, s1 + li s4, %lo(OUTBUF) + add s4, s4, row_stride + jal DMAOutAsync + nop + + # Row 2 + add s1, s1, s2 + move s0, s1 + li s4, %lo(OUTBUF) + add s4, s4, row_stride + add s4, s4, row_stride + jal DMAOutAsync + nop + + # Row 3 (final — sync) + add s1, s1, s2 + move s0, s1 + li s4, %lo(OUTBUF) + add s4, s4, row_stride + add s4, s4, row_stride + add s4, s4, row_stride + jal_and_j DMAOut, RSPQ_Loop + + #undef out_offset + #undef n_blocks + #undef block_idx + #undef row_stride + #undef v_const + #undef v_ep0 + #undef v_ep1 + #undef v_tmp0 + #undef v_pal + #undef v_unpack + #undef v_pack_r_lo + #undef v_pack_g_lo + #undef v_pack_b_lo + #undef v_one_lo + #undef v_pack + #undef v_pack_r + #undef v_pack_g + #undef v_pack_b + #undef v_one + #undef v_shl + #undef v_pp0 + #undef v_pp1 + #undef v_tA + #undef v_tB + #undef v_idxA + #undef v_idxB + #undef v_b0A + #undef v_b0B + #undef v_evA + #undef v_odA + #undef v_evB + #undef v_odB + #undef v_rA + #undef v_rB + #undef v_junk + .endfunc diff --git a/src/rspq/libdragon.mk b/src/rspq/libdragon.mk index bec580cd2..01839bcdb 100644 --- a/src/rspq/libdragon.mk +++ b/src/rspq/libdragon.mk @@ -1,3 +1,5 @@ LIBDRAGON_OBJS += \ $(BUILD_DIR)/rspq/rspq.o \ - $(BUILD_DIR)/rspq/rsp_queue.o \ No newline at end of file + $(BUILD_DIR)/rspq/rsp_queue.o \ + $(BUILD_DIR)/rspq/rspq_profile.o \ + $(BUILD_DIR)/rspq/rsp_profile.o \ No newline at end of file diff --git a/src/rspq/rsp_profile.S b/src/rspq/rsp_profile.S new file mode 100644 index 000000000..6d1f576bc --- /dev/null +++ b/src/rspq/rsp_profile.S @@ -0,0 +1,133 @@ +############################################################ +# rsp_profile.S +# +# Authors: Giovanni Bajo +############################################################ +#include +#if RSPQ_PROFILE + + .data + + RSPQ_BeginOverlayHeader +RSPQ_DefineCommand Cmd_ProfileFrame, 4 # 0x00 + RSPQ_EndOverlayHeader + + RSPQ_BeginSavedState + +PROFILE_STATE_START: +PROFILE_CNTR1: .long 0 + .long 0 # padding +PROFILE_OVL_SLOTS: .space 8 * RSPQ_MAX_OVERLAYS +PROFILE_CMN_SLOTS: .space 8 * RSPQ_PROFILE_CSLOT_COUNT +PROFILE_CMN_SLOTS_END: +PROFILE_FRAME_TIME: .long 0 +PROFILE_BUSY_TIME: .long 0 +PROFILE_FRAME_LAST: .long 0 +PROFILE_BUSY_LAST: .long 0 + .long 0 # padding +PROFILE_CNTR2: .long 0 +PROFILE_STATE_END: + + RSPQ_EndSavedState + + .text + + .func Cmd_ProfileFrame +Cmd_ProfileFrame: + # Update various per-frame counters + mfc0 t0, COP0_DP_CLOCK + mfc0 t3, COP0_DP_BUSY + sll t0, 8 + sll t3, 8 + lw t1, %lo(PROFILE_FRAME_LAST) + lw t4, %lo(PROFILE_BUSY_LAST) + sw t0, %lo(PROFILE_FRAME_LAST) + sw t3, %lo(PROFILE_BUSY_LAST) + subu t2, t0, t1 + subu t5, t3, t4 + sra t2, 8 + sra t5, 8 + lw t0, %lo(PROFILE_FRAME_TIME) + lw t1, %lo(PROFILE_BUSY_TIME) + addu t0, t0, t2 + addu t1, t1, t5 + sw t0, %lo(PROFILE_FRAME_TIME) + sw t1, %lo(PROFILE_BUSY_TIME) + + # Copy common slots + li s0, %lo(RSPQ_PROFILE_DATA) + li s1, %lo(PROFILE_CMN_SLOTS) + li s2, %lo(PROFILE_CMN_SLOTS_END) +loop_cmn: + lw t0, 0(s0) + lw t1, 4(s0) + addiu s1, 8 + sw t0, -8(s1) + sw t1, -4(s1) + bne s1, s2, loop_cmn + addiu s0, 8 + + #define inptr s1 + #define inptr_end t5 + #define outptr s3 + + # Copy builtin overlay data to OVL_PROFILE_SLOTS + li s0, %lo(RSPQ_PROFILESLOT_BUILTINS) + li outptr, %lo(PROFILE_OVL_SLOTS) + ldv $v01, 0,s0 + sdv $v01, 0,outptr + addiu outptr, 8 + + # Loop through the overlay table + li inptr, %lo(RSPQ_OVERLAY_TABLE)+4 + li inptr_end, %lo(RSPQ_OVERLAY_TABLE)+4*RSPQ_MAX_OVERLAYS +loop_ovl: + # Load packed size+address of data segment. Ignore the size + # We want to load the first two words which are the state offset and size + lw s2, 0(inptr) + beqz s2, loop_ovl_epilog + + # Fetch the profile slot offset in the data segment + addiu s0, s2, RSPQ_OH_PROFILESLOT + li s4, %lo(RSPQ_SCRATCH_MEM) + jal DMAIn + li t0, DMA_SIZE(8, 1) + + # Read the profile offset, and then fetch the profile slot + lhu s0, 0(s4) + addiu s0, -(_ovl_data_start - _data_start) + addu s0, s2 + move s4, outptr + jal DMAIn + li t0, DMA_SIZE(8, 1) + +loop_ovl_epilog: + addiu inptr, 4 + bne inptr, inptr_end, loop_ovl + addiu outptr, 8 + + #undef inptr + #undef inptr_end + #undef outptr + + # Increment the two counters used to avoid race conditions on the CPU + lw t0, %lo(PROFILE_CNTR1) + lw t1, %lo(PROFILE_CNTR2) + addiu t0, 1 + addiu t1, 1 + sw t0, %lo(PROFILE_CNTR1) + sw t1, %lo(PROFILE_CNTR2) + +profile_end: + # DMA the whole profile state to RDRAM where requested by the CPU + move s0, a0 + li s4, %lo(PROFILE_STATE_START) + jal DMAOut + li t0, DMA_SIZE(PROFILE_STATE_END - PROFILE_STATE_START, 1) + + j RSPQ_Loop + nop + + .endfunc + +#endif diff --git a/src/rspq/rspq.c b/src/rspq/rspq.c index 9b5858d5d..fcf358c98 100644 --- a/src/rspq/rspq.c +++ b/src/rspq/rspq.c @@ -109,13 +109,10 @@ * * Internally, double buffering is used to implement the queue. The size of * each of the buffers is RSPQ_DRAM_LOWPRI_BUFFER_SIZE. When a buffer is full, - * the queue engine writes a #RSPQ_CMD_JUMP command with the address of the - * other buffer, to tell the RSP to jump there when it is done. - * - * Moreover, just before the jump, the engine also enqueue a #RSPQ_CMD_WRITE_STATUS - * command that sets the SP_STATUS_SIG_BUFDONE_LOW signal. This is used to - * keep track when the RSP has finished processing a buffer, so that we know - * it becomes free again for more commands. + * the low-priority queue engine stages the address of the other buffer with a + * #RSPQ_CMD_WRITE_WORD, then executes #RSPQ_CMD_SWAP_BUFFERS. The latter sets + * the buffer-done signal and jumps to the other buffer. The signal tells + * the CPU when a buffer becomes free for reuse. * * This logic is implemented in #rspq_next_buffer. * @@ -145,6 +142,20 @@ * is then used as call slot in both all future calls to the block, and by * the RSPQ_CMD_RET command placed at the end of the block itself. * + * ## Queues + * + * Queues are mutable command chains built in RDRAM, similar to blocks but + * intended to be reused and cleared. Internally, a queue consists of one or + * more chunks linked by #RSPQ_CMD_JUMP guards at the end of each chunk, with + * the last chunk ending in #RSPQ_CMD_RET. Chunk growth reuses the same doubling + * strategy used by blocks. + * + * Recording a queue redirects #rspq_write into the queue buffer, just like + * blocks. The main difference is that #rspq_queue_run enqueues a CALL in the + * lowpri queue and advances an internal run_start pointer. The current write + * pointer remains in the queue, so commands appended during execution will be + * picked up on the next #rspq_queue_run call. + * * ## Highpri queue * * The high priority queue is implemented as an alternative couple of buffers, @@ -191,6 +202,7 @@ #include "rspq.h" #include "rspq_internal.h" #include "rspq_constants.h" +#include "rspq_profile.h" #include "rdp.h" #include "rdpq_constants.h" #include "rdpq/rdpq_internal.h" @@ -199,6 +211,7 @@ #include "utils.h" #include "n64sys.h" #include "debug.h" +#include "accounting_internal.h" #include #include #include @@ -245,6 +258,12 @@ _Static_assert(RSPQ_MAX_COMMAND_SIZE * 4 <= RSPQ_DESCRIPTOR_MAX_SIZE); ptr += 3; \ }) +/** @brief Number of words reserved for the lowpri buffer handoff sequence. */ +#define RSPQ_LOWPRI_HANDOFF_WORDS 5 + +/** @brief Block-call slot temporarily reused by the lowpri buffer handoff. */ +#define RSPQ_LOWPRI_HANDOFF_SLOT 0 + static void rspq_crash_handler(rsp_snapshot_t *state); static void rspq_assert_handler(rsp_snapshot_t *state, uint16_t assert_code); @@ -261,11 +280,16 @@ DEFINE_RSP_UCODE(rsp_queue, */ typedef struct __attribute__((packed)) rspq_overlay_header_t { uint16_t state_start; ///< Start of the portion of DMEM used as "state" - uint16_t state_size; ///< Size of the portion of DMEM used as "state" + uint16_t state_size; ///< Size of the portion of DMEM used as "state" (minus 1) uint32_t state_rdram; ///< RDRAM address of the portion of DMEM used as "state" - uint32_t text_rdram; ///< RDRAM address of the overlay's text section - uint16_t text_size; ///< Size of the overlay's text section uint16_t command_base; ///< Primary overlay ID used for this overlay + uint16_t overlay_id; ///< Overlay ID (multiplied by 4) + uint32_t text_rdram; ///< RDRAM address of the overlay's text section + uint16_t text_size; ///< Size of the overlay's text section (minus 1) + uint16_t text_start; ///< Offset of the overlay's text section in DMEM + uint32_t extraseg_rdram; ///< RDRAM address of extra segment + uint16_t extraseg_size; ///< Size of extra segment (minus 1) + uint16_t extraseg_start; ///< Offset of extra segment in DMEM #if RSPQ_PROFILE uint16_t profile_slot_dmem; ///< Start of the profile slots in DMEM #endif @@ -318,8 +342,41 @@ typedef struct { volatile uint32_t *sentinel; ///< Current write sentinel within the active buffer } rspq_ctx_t; +/** + * @brief Command chain used while a block or a queue is being recorded. + * + * A command chain is a linked list of chunks of commands. Each chunk is a + * contiguous block of commands that is allocated from the heap. + * + * When RSP executes a command chain, it will start from the first chunk, and + * follow the JUMP commands to the next chunk until it reaches the last chunk. + * The last chunk is terminated with a RET command. + * + * To simplify traversal of the chain by the CPU, the final JUMP/RET command + * is also always duplicated as last word in each chunk. + */ +typedef struct rspq_cmd_chain_s { + uint32_t *first_chunk; ///< First chunk of commands + uint32_t *cur_chunk; ///< Current chunk being written + int cur_chunk_size; ///< Size of the current chunk (in 32-bit words) + volatile uint32_t *cur; ///< Current write pointer within the current chunk + volatile uint32_t *sentinel; ///< Current write sentinel within the current chunk +} rspq_cmd_chain_t; + +/** + * @brief A rspq queue: mutable buffered sequence of commands + */ +typedef struct rspq_queue_s { + rspq_cmd_chain_t chain; ///< Command chain for this queue + uint32_t nesting_level; ///< Nesting level of the queue + volatile uint32_t *run_start; ///< Start address for the next run + rdpq_tracking_t rdpq_tracking; ///< Tracking state of the queue + uint32_t cmds[]; ///< First chunk contents +} rspq_queue_t; + static rspq_ctx_t lowpri; ///< Lowpri queue context static rspq_ctx_t highpri; ///< Highpri queue context +static int highpri_nesting; ///< Nesting level of #rspq_highpri_begin rspq_ctx_t *rspq_ctx; ///< Current context volatile uint32_t *rspq_cur_pointer; ///< Copy of the current write pointer (see #rspq_ctx_t) @@ -336,8 +393,10 @@ static bool rspq_initialized = 0; /** @brief Pointer to the current block being built, or NULL. */ rspq_block_t *rspq_block; -/** @brief Size of the current block memory buffer (in 32-bit words). */ -static int rspq_block_size; +/** @brief Command chain used while a block is being recorded. */ +static rspq_cmd_chain_t rspq_block_chain; +/** @brief Pointer to the current queue being recorded, or NULL. */ +static rspq_queue_t *rspq_queue_recording; /** @brief ID that will be used for the next syncpoint that will be created. */ static int rspq_syncpoints_genid; @@ -350,7 +409,101 @@ static bool rspq_is_running; /** @brief Dummy state used for overlay 0 */ static uint64_t dummy_overlay_state[2] __attribute__((aligned(16))); +/** @brief Deferred calls: head of list */ +static rspq_deferred_call_t *defcalls_head; +/** @brief Deferred calls: tail of list */ +static rspq_deferred_call_t *defcalls_tail; +/** @brief Deferred calls: count */ +static uint32_t defcalls_count; +/** @brief Deferred calls: maximum count */ +static uint32_t defcalls_run_threshold; + static void rspq_flush_internal(void); +static volatile uint32_t* rspq_switch_buffer(uint32_t *new, int size, bool clear); + +/** @brief Reset a command chain to the initial state. */ +static void rspq_chain_reset(rspq_cmd_chain_t *ch, uint32_t *first, int size) +{ + ch->first_chunk = first; + ch->cur_chunk = first; + ch->cur_chunk_size = size; + ch->cur = ch->cur_chunk; + ch->sentinel = ch->cur_chunk + size - (RSPQ_MAX_SHORT_COMMAND_SIZE + 2); +} + +/* @brief Initialize a command chain */ +static void rspq_chain_init(rspq_cmd_chain_t *ch, uint32_t *first, int size) +{ + rspq_chain_reset(ch, first, size); + + // Guard word at the end of the chunk (used for traversal on destroy). + volatile uint32_t *guard = first + size - 1; + rspq_append1(guard, RSPQ_CMD_RET, 0); +} + +/* @brief Create/advance to the next chunk in a command chain. + Returns the pointer to the next chunk. */ +static uint32_t* rspq_chain_next(rspq_cmd_chain_t *ch) +{ + volatile uint32_t *prev_guard = ch->cur_chunk + ch->cur_chunk_size - 1; + uint32_t cmd = *prev_guard; + uint32_t *next = NULL; + int next_size = ch->cur_chunk_size; + + if (cmd >> 24 == RSPQ_CMD_JUMP) { + next = VirtualUncachedAddr(cmd & 0xFFFFFF); + if (next_size < RSPQ_BLOCK_MAX_SIZE) next_size *= 2; + } else if (cmd >> 24 == RSPQ_CMD_RET) { + if (next_size < RSPQ_BLOCK_MAX_SIZE) next_size *= 2; + next = malloc_uncached(next_size * sizeof(uint32_t)); + assertf(next, "Out of memory"); + + volatile uint32_t *guard = next + next_size - 1; + rspq_append1(guard, RSPQ_CMD_RET, 0); + + rspq_append1(prev_guard, RSPQ_CMD_JUMP, PhysicalAddr(next)); + } else { + assertf(0, "invalid terminator command in chain: %08lx\n", cmd); + } + + // Write the JUMP command for the RSP at the current write position + rspq_append1(rspq_cur_pointer, RSPQ_CMD_JUMP, PhysicalAddr(next)); + + ch->cur_chunk = next; + ch->cur_chunk_size = next_size; + + rspq_switch_buffer(next, next_size, false); + ch->cur = rspq_cur_pointer; + ch->sentinel = rspq_cur_sentinel; + + return next; +} + +/** @brief Free a command chain. */ +static void rspq_chain_free(uint32_t *first_chunk, int size) +{ + uint32_t *ptr = first_chunk; + while (1) { + uint32_t cmd = ptr[size-1]; + + // Free all the chunks except the first one (builtin in + // the containing structure) + if (ptr != first_chunk) + free_uncached(ptr); + + // If the guard command is a jump, there is a next chunk + if (cmd >> 24 == RSPQ_CMD_JUMP) { + ptr = UncachedAddr(0x80000000 | (cmd & 0xFFFFFF)); + if (size < RSPQ_BLOCK_MAX_SIZE) size *= 2; + continue; + } + // If the guard command is a return, we are done + if (cmd >> 24 == RSPQ_CMD_RET) { + return; + } + assertf(0, "invalid terminator command in chain: %08lx\n", cmd); + } +} /** @brief RSP interrupt handler, used for syncpoints. */ static void rspq_sp_interrupt(void) @@ -378,9 +531,9 @@ static void rspq_sp_interrupt(void) } /** @brief Extract the current overlay index and name from the RSP queue state */ -static void rspq_get_current_ovl(rsp_queue_t *rspq, uint8_t *ovl_id, const char **ovl_name) +static void rspq_get_current_ovl(rspq_overlay_header_t *ovl_header, uint8_t *ovl_id, const char **ovl_name) { - *ovl_id = rspq->current_ovl; + *ovl_id = ovl_header->overlay_id >> 2; if (*ovl_id == 0) { *ovl_name = "builtin"; } else if (*ovl_id < RSPQ_MAX_OVERLAYS && rspq_overlay_ucodes[*ovl_id]) { @@ -392,12 +545,13 @@ static void rspq_get_current_ovl(rsp_queue_t *rspq, uint8_t *ovl_id, const char /** @brief RSPQ crash handler. This shows RSPQ-specific info the in RSP crash screen. */ static void rspq_crash_handler(rsp_snapshot_t *state) { - rsp_queue_t *rspq = (rsp_queue_t*)(state->dmem + RSPQ_DATA_ADDRESS); + uint32_t rsp_queue_data_size = rsp_queue_data_end - rsp_queue_data_start; + rsp_queue_t *rspq = (rsp_queue_t*)(state->dmem); + rspq_overlay_header_t *ovl_header = (rspq_overlay_header_t*)(state->dmem + rsp_queue_data_size); uint32_t cur = rspq->rspq_dram_addr + state->gpr[28]; - uint32_t dmem_buffer = ROUND_UP(RSPQ_DATA_ADDRESS + sizeof(rsp_queue_t), 8); const char *ovl_name; uint8_t ovl_id; - rspq_get_current_ovl(rspq, &ovl_id, &ovl_name); + rspq_get_current_ovl(ovl_header, &ovl_id, &ovl_name); printf("RSPQ: Normal DRAM address: %08lx\n", rspq->rspq_dram_lowpri_addr); printf("RSPQ: Highpri DRAM address: %08lx\n", rspq->rspq_dram_highpri_addr); @@ -412,7 +566,7 @@ static void rspq_crash_handler(rsp_snapshot_t *state) debugf("RSPQ: Command queue:\n"); for (int j=0;j<4;j++) { for (int i=0;i<16;i++) - debugf("%08lx%c", ((uint32_t*)state->dmem)[dmem_buffer/4+i+j*16], state->gpr[28] == (j*16+i)*4 ? '*' : ' '); + debugf("%08lx%c", rspq->cmds[i+j*16], state->gpr[28] == (j*16+i)*4 ? '*' : ' '); debugf("\n"); } @@ -439,19 +593,18 @@ static void rspq_crash_handler(rsp_snapshot_t *state) /** @brief Special RSP assert handler for ASSERT_INVALID_COMMAND */ static void rspq_assert_invalid_command(rsp_snapshot_t *state) { - rsp_queue_t *rspq = (rsp_queue_t*)(state->dmem + RSPQ_DATA_ADDRESS); + rspq_overlay_header_t *ovl_header = (rspq_overlay_header_t*)(state->dmem + (rsp_queue_data_end - rsp_queue_data_start)); const char *ovl_name; uint8_t ovl_id; - rspq_get_current_ovl(rspq, &ovl_id, &ovl_name); + rspq_get_current_ovl(ovl_header, &ovl_id, &ovl_name); - uint32_t dmem_buffer = ROUND_UP(RSPQ_DATA_ADDRESS + sizeof(rsp_queue_t), 8); - uint32_t cur = dmem_buffer + state->gpr[28]; + uint32_t cur = state->gpr[28]/4; printf("Invalid command\nCommand %02x not found in overlay %s (0x%01x)\n", state->dmem[cur], ovl_name, ovl_id); } /** @brief Special RSP assert handler for ASSERT_INVALID_OVERLAY */ static void rspq_assert_invalid_overlay(rsp_snapshot_t *state) { - printf("Invalid overlay\nOverlay 0x%01lx not registered\n", state->gpr[8]); + printf("Invalid overlay\nOverlay 0x%01lx not registered\n", state->gpr[15]); } /** @brief RSP assert handler for rspq */ @@ -465,16 +618,17 @@ static void rspq_assert_handler(rsp_snapshot_t *state, uint16_t assert_code) rspq_assert_invalid_command(state); break; default: { - rsp_queue_t *rspq = (rsp_queue_t*)(state->dmem + RSPQ_DATA_ADDRESS); - // Check if there is an assert handler for the current overlay. // If it exists, forward request to it. // Be defensive against DMEM corruptions. - int ovl_id = rspq->current_ovl; + rspq_overlay_header_t *ovl_header = (rspq_overlay_header_t*)(state->dmem + (rsp_queue_data_end - rsp_queue_data_start)); + uint8_t ovl_id = ovl_header->overlay_id >> 2; if (ovl_id < RSPQ_MAX_OVERLAYS && rspq_overlay_ucodes[ovl_id] && rspq_overlay_ucodes[ovl_id]->assert_handler) rspq_overlay_ucodes[ovl_id]->assert_handler(state, assert_code); + else + printf("\n"); break; } } @@ -506,12 +660,18 @@ static volatile uint32_t* rspq_switch_buffer(uint32_t *new, int size, bool clear // Notice that the buffer must have been cleared before, as the // command queue are expected to always contain 0 on unwritten data. // We don't do this for performance reasons. - assert(size >= RSPQ_MAX_COMMAND_SIZE); + int terminator_words = clear && rspq_ctx == &lowpri ? + RSPQ_LOWPRI_HANDOFF_WORDS : 2; + assert(size >= RSPQ_MAX_COMMAND_SIZE + terminator_words); if (clear) memset(new, 0, size * sizeof(uint32_t)); - // Switch to the new buffer, and calculate the new sentinel. + // Switch to the new buffer, and calculate the new sentinel. The sentinel + // must allow for a maximum size command (RSPQ_MAX_SHORT_COMMAND_SIZE) to + // be written, plus either the lowpri handoff sequence or the two-word + // reservation used by highpri, blocks, and recorded queues. rspq_cur_pointer = new; - rspq_cur_sentinel = new + size - RSPQ_MAX_SHORT_COMMAND_SIZE; + rspq_cur_sentinel = new + size - + (RSPQ_MAX_SHORT_COMMAND_SIZE + terminator_words); // Return a pointer to the previous buffer return prev; @@ -529,7 +689,7 @@ static void rspq_start(void) // Load data with initialized overlays into DMEM data_cache_hit_writeback(&rspq_data, sizeof(rsp_queue_t)); - rsp_load_data(&rspq_data, sizeof(rsp_queue_t), RSPQ_DATA_ADDRESS); + rsp_load_data(&rspq_data, sizeof(rsp_queue_t), 0); static rspq_overlay_header_t dummy_header = (rspq_overlay_header_t){ .state_start = 0, @@ -569,6 +729,7 @@ static void rspq_init_context(rspq_ctx_t *ctx, int buf_size) memset(ctx, 0, sizeof(rspq_ctx_t)); ctx->buffers[0] = malloc_uncached(buf_size * sizeof(uint32_t)); ctx->buffers[1] = malloc_uncached(buf_size * sizeof(uint32_t)); + assertf(ctx->buffers[0] && ctx->buffers[1], "Out of memory"); memset(ctx->buffers[0], 0, buf_size * sizeof(uint32_t)); memset(ctx->buffers[1], 0, buf_size * sizeof(uint32_t)); ctx->buf_idx = 0; @@ -610,14 +771,15 @@ void rspq_init(void) // Allocate the RDP dynamic buffers. rspq_rdp_dynamic_buffers[0] = malloc_uncached(RDPQ_DYNAMIC_BUFFER_SIZE); rspq_rdp_dynamic_buffers[1] = malloc_uncached(RDPQ_DYNAMIC_BUFFER_SIZE); - + assertf(rspq_rdp_dynamic_buffers[0] && rspq_rdp_dynamic_buffers[1], "Out of memory"); + // Verify consistency of state - int banner_offset = RSPQ_DATA_ADDRESS + offsetof(rsp_queue_t, banner); + int banner_offset = offsetof(rsp_queue_t, banner); assertf(!memcmp(rsp_queue.data + banner_offset, "Dragon RSP Queue", 16), "rsp_queue_t does not seem to match DMEM; did you forget to update it?"); // Load initial settings - memcpy(&rspq_data, rsp_queue.data + RSPQ_DATA_ADDRESS, sizeof(rsp_queue_t)); + memcpy(&rspq_data, rsp_queue.data, sizeof(rsp_queue_t)); rspq_data.rspq_dram_lowpri_addr = PhysicalAddr(lowpri.cur); rspq_data.rspq_dram_highpri_addr = PhysicalAddr(highpri.cur); rspq_data.rspq_dram_addr = rspq_data.rspq_dram_lowpri_addr; @@ -626,6 +788,10 @@ void rspq_init(void) rspq_data.rspq_rdp_current = rspq_data.rspq_rdp_buffers[0]; rspq_data.rspq_rdp_sentinel = rspq_data.rspq_rdp_buffers[0] + RDPQ_DYNAMIC_BUFFER_SIZE; rspq_data.rspq_ovl_table.data_rdram[0] = PhysicalAddr(dummy_overlay_state) | (0<<24); + +#if RSPQ_PROFILE + rspq_data.rspq_profile_cur_slot = -1; +#endif // Init syncpoints rspq_syncpoints_genid = 0; @@ -633,7 +799,9 @@ void rspq_init(void) // Init blocks rspq_block = NULL; + rspq_queue_recording = NULL; rspq_is_running = false; + highpri_nesting = 0; // Activate SP interrupt (used for syncpoints) register_SP_handler(rspq_sp_interrupt); @@ -645,7 +813,7 @@ void rspq_init(void) MEMORY_BARRIER(); *DP_STATUS = DP_WSTATUS_RESET_XBUS_DMEM_DMA | DP_WSTATUS_RESET_FLUSH | DP_WSTATUS_RESET_FREEZE; MEMORY_BARRIER(); - RSP_WAIT_LOOP(500) { + ACCT_SCOPE(ACCT_CAT_RSPQ) RSP_WAIT_LOOP(500) { if (!(*DP_STATUS & (DP_STATUS_START_VALID | DP_STATUS_END_VALID))) { break; } @@ -720,8 +888,8 @@ void* rspq_overlay_get_state(rsp_ucode_t *overlay_ucode) // state for. If so, read back the latest updated state from DMEM // manually via DMA, so that the caller finds the latest contents. const char *ovl_name; uint8_t ovl_id; - rsp_queue_t *rspq = (rsp_queue_t*)((uint8_t*)SP_DMEM + RSPQ_DATA_ADDRESS); - rspq_get_current_ovl(rspq, &ovl_id, &ovl_name); + rspq_overlay_header_t *ovl_header = (void*)SP_DMEM + (rsp_queue_data_end - rsp_queue_data_start); + rspq_get_current_ovl(ovl_header, &ovl_id, &ovl_name); if (ovl_id && rspq_overlay_ucodes[ovl_id] == overlay_ucode) { rsp_read_data(state_ptr, state_size, state_ptr - overlay_ucode->data); @@ -738,7 +906,7 @@ rsp_queue_t* __rspq_get_state(void) rspq_wait(); // Read the state and return it - rsp_read_data(&rspq_data, sizeof(rsp_queue_t), RSPQ_DATA_ADDRESS); + rsp_read_data(&rspq_data, sizeof(rsp_queue_t), 0); return &rspq_data; } @@ -792,7 +960,7 @@ static void rspq_update_tables(bool is_highpri) data_cache_hit_writeback_invalidate(&rspq_data.rspq_ovl_table, sizeof(rspq_ovl_table_t)); if (is_highpri) rspq_highpri_begin(); rspq_dma_to_dmem( - RSPQ_DATA_ADDRESS + offsetof(rsp_queue_t, rspq_ovl_table), + offsetof(rsp_queue_t, rspq_ovl_table), &rspq_data.rspq_ovl_table, sizeof(rspq_ovl_table_t), false); if (is_highpri) rspq_highpri_end(); } @@ -848,7 +1016,7 @@ static uint32_t rspq_overlay_register_internal(rsp_ucode_t *overlay_ucode, uint3 for (uint32_t i = 0; i < slot_count; i++) { rspq_data.rspq_ovl_table.data_rdram[id + i] = PhysicalAddr(overlay_data) | (((overlay_data_size - 1) >> 4) << 24); - rspq_data.rspq_ovl_table.idmap[id + i] = id; + rspq_data.rspq_ovl_table.idmap[id + i] = id << 2; } // Fill information in the overlay header @@ -856,6 +1024,7 @@ static uint32_t rspq_overlay_register_internal(rsp_ucode_t *overlay_ucode, uint3 overlay_header->text_rdram = PhysicalAddr(overlay_code); overlay_header->state_rdram = PhysicalAddr(overlay_ucode->data) + overlay_header->state_start; overlay_header->command_base = id << 5; + overlay_header->overlay_id = id << 2; data_cache_hit_writeback_invalidate(overlay_header, sizeof(rspq_overlay_header_t)); // Save the overlay pointer @@ -878,6 +1047,31 @@ void rspq_overlay_register_static(rsp_ucode_t *overlay_ucode, uint32_t overlay_i rspq_overlay_register_internal(overlay_ucode, overlay_id); } +void rspq_overlay_share_state(rsp_ucode_t *overlay_dest, rsp_ucode_t *overlay_source) +{ + rspq_overlay_header_t *dsth = (rspq_overlay_header_t*)(overlay_dest->data + (rsp_queue_data_end - rsp_queue_data_start)); + rspq_overlay_header_t *srch = (rspq_overlay_header_t*)(overlay_source->data + (rsp_queue_data_end - rsp_queue_data_start)); + + assertf((dsth->text_rdram & (1<<31)) == 0, + "Overlay %s is already using another shared state", overlay_dest->name); + assertf(dsth->state_size <= srch->state_size, "Overlays %s (dest) and %s (src) have non-compatible state size (%d vs %d)", + overlay_dest->name, overlay_source->name, dsth->state_size, srch->state_size); + + // Mark as using shared state. We need to mark it in the text segment because + // it's what the rsp_queue ucode checks for. + dsth->text_rdram |= (1<<31); + + // At overlay load time, copy state from source overlay into DMEM + dsth->extraseg_rdram = srch->state_rdram; + dsth->extraseg_size = dsth->state_size; + dsth->extraseg_start = dsth->state_start; + + // At overlay save time, copy state from DMEM into source overlay + dsth->state_rdram = srch->state_rdram; + + data_cache_hit_writeback_invalidate(dsth, sizeof(rspq_overlay_header_t)); +} + void rspq_overlay_unregister(uint32_t overlay_id) { assertf(overlay_id != 0, "Overlay 0 cannot be unregistered!"); @@ -920,18 +1114,13 @@ __attribute__((noinline)) void rspq_next_buffer(void) { // If we're creating a block if (rspq_block) { - // Allocate next chunk (double the size of the current one). - // We use doubling here to reduce overheads for large blocks - // and at the same time start small. - if (rspq_block_size < RSPQ_BLOCK_MAX_SIZE) rspq_block_size *= 2; - - // Allocate a new chunk of the block and switch to it. - uint32_t *rspq2 = malloc_uncached(rspq_block_size*sizeof(uint32_t)); - volatile uint32_t *prev = rspq_switch_buffer(rspq2, rspq_block_size, true); - - // Terminate the previous chunk with a JUMP op to the new chunk. - rspq_append1(prev, RSPQ_CMD_JUMP, PhysicalAddr(rspq2)); + rspq_chain_next(&rspq_block_chain); + return; + } + // If we're recording a queue + if (rspq_queue_recording) { + rspq_chain_next(&rspq_queue_recording->chain); return; } @@ -940,6 +1129,10 @@ void rspq_next_buffer(void) { // commands. if (rdpq_trace) rdpq_trace(); + // Poll the deferred list at least once per buffer switch. We will poll + // more if we need to wait + __rspq_deferred_poll(); + // Wait until the previous buffer is executed by the RSP. // We cannot write to it if it's still being executed. // FIXME: this should probably transition to a sync-point, @@ -948,7 +1141,8 @@ void rspq_next_buffer(void) { MEMORY_BARRIER(); if (!(*SP_STATUS & rspq_ctx->sp_status_bufdone)) { rspq_flush_internal(); - RSP_WAIT_LOOP(200) { + ACCT_SCOPE(ACCT_CAT_RSPQ) RSP_WAIT_LOOP(200) { + __rspq_deferred_poll(); if (*SP_STATUS & rspq_ctx->sp_status_bufdone) break; } @@ -962,12 +1156,31 @@ void rspq_next_buffer(void) { uint32_t *new = rspq_ctx->buffers[rspq_ctx->buf_idx]; volatile uint32_t *prev = rspq_switch_buffer(new, rspq_ctx->buf_size, true); - // Terminate the previous buffer with an op to set SIG_BUFDONE - // (to notify when the RSP finishes the buffer), plus a jump to - // the new buffer. - rspq_append1(prev, RSPQ_CMD_WRITE_STATUS, rspq_ctx->sp_wstatus_set_bufdone); - rspq_append1(prev, RSPQ_CMD_JUMP, PhysicalAddr(new)); - assert(prev+1 < (uint32_t*)(rspq_ctx->buffers[1-rspq_ctx->buf_idx]) + rspq_ctx->buf_size); + if (rspq_ctx == &lowpri) { + // Stage the target in call slot 0 (RSPQ_LOWPRI_HANDOFF_SLOT). + // All previously scheduled block calls have returned before this + // top-level handoff can execute, so the slot is inactive. A highpri + // request may safely preempt after this setup command because SIG_BUFDONE + // has not been set yet. + const uint32_t handoff_slot_offset = RSPQ_LOWPRI_HANDOFF_SLOT << 2; + rspq_append2(prev, RSPQ_CMD_WRITE_WORD, + offsetof(rsp_queue_t, rspq_pointer_stack) + handoff_slot_offset, + PhysicalAddr(new)); + + // SWAP_BUFFERS sets SIG_BUFDONE, loads the staged target, and jumps to + // it without returning to RSPQ_Loop. Reusing the same slot for its + // discarded return address is safe because the target is loaded first. + rspq_append3(prev, RSPQ_CMD_SWAP_BUFFERS, + handoff_slot_offset, handoff_slot_offset, + rspq_ctx->sp_wstatus_set_bufdone); + } else { + // Highpri execution cannot itself be preempted, so we can use a smaller + // sequence instead, which would be vulnerable to be preempted in the middle. + rspq_append1(prev, RSPQ_CMD_WRITE_STATUS, + rspq_ctx->sp_wstatus_set_bufdone); + rspq_append1(prev, RSPQ_CMD_JUMP, PhysicalAddr(new)); + } + assert(prev <= (uint32_t*)(rspq_ctx->buffers[1-rspq_ctx->buf_idx]) + rspq_ctx->buf_size); rspq_flush_internal(); } @@ -982,16 +1195,20 @@ static void rspq_flush_internal(void) // Most of the times, the above is enough. But there is a small and very rare // race condition that can happen: if the above status change happens // exactly in the few instructions between RSP checking for the status - // register ("mfc0 t0, COP0_SP_STATUS") RSP halting itself("break"), - // the call to rspq_flush might have no effect (see command_wait_new_input in - // rsp_queue.S). + // register ("mfc0 t0, COP0_SP_STATUS") RSP halting itself ("break"), + // the call to rspq_flush might have no effect (see RSPQCmd_WaitNewInput in + // rsp_queue.inc). // In general this is not a big problem even if it happens, as the RSP // would wake up at the next flush anyway, but we guarantee that rspq_flush // does actually make the RSP finish the current buffer. To keep this // invariant, we wait 10 cycles and then issue the command again. This // make sure that even if the race condition happened, we still succeed // in waking up the RSP. - __asm("nop; nop; nop; nop; nop; nop; nop; nop; nop; nop;"); + // We also a dummy branch in the nop sequence to hint emulators to resync + // the CPU and the RSP. This happens to currently work with Ares. In general, + // emulators should at least resync CPU and RSP on SP_STATUS, and possibly + // boost interleaving for a while after that. + __asm("nop; nop; nop; b 1f; 1:nop; nop; nop; nop; nop; nop;"); MEMORY_BARRIER(); *SP_STATUS = SP_WSTATUS_SET_SIG_MORE | SP_WSTATUS_CLEAR_HALT | SP_WSTATUS_CLEAR_BROKE; MEMORY_BARRIER(); @@ -1000,7 +1217,7 @@ static void rspq_flush_internal(void) void rspq_flush(void) { // If we are recording a block, flushes can be ignored. - if (rspq_block) return; + if (rspq_block || rspq_queue_recording) return; rspq_flush_internal(); if (rdpq_trace) rdpq_trace(); @@ -1008,8 +1225,13 @@ void rspq_flush(void) void rspq_highpri_begin(void) { - assertf(rspq_ctx != &highpri, "already in highpri mode"); assertf(!rspq_block, "cannot switch to highpri mode while creating a block"); + assertf(!rspq_queue_recording, "cannot switch to highpri mode while recording a queue"); + + if (rspq_ctx == &highpri) { + highpri_nesting++; + return; + } rspq_switch_context(&highpri); @@ -1047,10 +1269,20 @@ void rspq_highpri_begin(void) // add a command in case the previous epilog was skipped. Otherwise, // a dummy SIG_HIGHPRI_REQUESTED could stay on and eventually highpri // mode would enter once again. - rspq_append1(rspq_cur_pointer, RSPQ_CMD_WRITE_STATUS, - SP_WSTATUS_CLEAR_SIG_HIGHPRI_REQUESTED | SP_WSTATUS_SET_SIG_HIGHPRI_RUNNING); + // Set SIG_HIGHPRI_REQUESTED *before* writing the WRITE_STATUS command that + // clears it. If the RSP is already in highpri mode and caught up with the + // write cursor, it can execute the appended WRITE_STATUS within a few + // cycles of the append; with the old order (append first, then set), the + // clear could be consumed before the set landed, leaving a dangling + // REQUESTED that made the RSP re-enter highpri after the final epilog and + // park forever on the empty queue with SIG_HIGHPRI_RUNNING set (deadlocking + // rspq_highpri_sync). Setting the signal first closes the race: the RSP + // cannot execute the clear before it is written, which is after the set. MEMORY_BARRIER(); *SP_STATUS = SP_WSTATUS_SET_SIG_HIGHPRI_REQUESTED; + MEMORY_BARRIER(); + rspq_append1(rspq_cur_pointer, RSPQ_CMD_WRITE_STATUS, + SP_WSTATUS_CLEAR_SIG_HIGHPRI_REQUESTED | SP_WSTATUS_SET_SIG_HIGHPRI_RUNNING); rspq_flush_internal(); } @@ -1058,6 +1290,11 @@ void rspq_highpri_end(void) { assertf(rspq_ctx == &highpri, "not in highpri mode"); + if (highpri_nesting) { + highpri_nesting--; + return; + } + // Write the highpri epilog. The epilog starts with a JUMP to the next // instruction because we want to force the RSP to reload the buffer // from RDRAM in case the epilog has been overwritten by a new highpri @@ -1072,12 +1309,32 @@ void rspq_highpri_end(void) void rspq_highpri_sync(void) { - assertf(rspq_ctx != &highpri, "this function can only be called outside of highpri mode"); - // Make sure the RSP is running, otherwise we might be blocking forever. + // This also clears HALT, so that the check below cannot be fooled by a + // halted state that predates the commands we are waiting for. rspq_flush_internal(); - RSP_WAIT_LOOP(200) { + if (rspq_ctx == &highpri) { + // We are in the middle of building a highpri sequence, so its epilog + // (the only thing that clears SIG_HIGHPRI_RUNNING) has not been written + // yet and the wait below would never be satisfied. Wait on a different + // condition: an unterminated highpri queue simply ends at our write + // pointer, where the RSP finds the queue terminator and halts itself + // (see RSPQCmd_WaitNewInput in rsp_queue.inc). So the halt means that + // everything written so far has been executed. DMA status must be + // checked too: "break" also runs while an asynchronous transfer + // started by the last command is still in flight. + ACCT_SCOPE(ACCT_CAT_RSPQ) RSP_WAIT_LOOP(200) { + uint32_t status = *SP_STATUS; + if ((status & SP_STATUS_HALTED) && + !(status & (SP_STATUS_DMA_BUSY | SP_STATUS_DMA_FULL))) + break; + } + return; + } + + ACCT_SCOPE(ACCT_CAT_RSPQ) RSP_WAIT_LOOP(200) { + __rspq_deferred_poll(); if (!(*SP_STATUS & (SP_STATUS_SIG_HIGHPRI_REQUESTED | SP_STATUS_SIG_HIGHPRI_RUNNING))) break; } @@ -1088,23 +1345,57 @@ bool rspq_in_highpri(void) return (rspq_ctx == &highpri); } -void rspq_block_begin(void) +/** Invoke all #rspq_block_atexit callbacks and free the list nodes. */ +static void rspq_block_free_atexit_chain(rspq_block_t *block) +{ + rspq_block_cb_t *cb = block->atexit; + while (cb) { + cb->cb(cb->ctx); + rspq_block_cb_t *next = cb->next; + free(cb); + cb = next; + } + block->atexit = NULL; +} + +void rspq_block_begin_reuse(rspq_block_t *reuse_block) { assertf(!rspq_block, "a block was already being created"); assertf(rspq_ctx != &highpri, "cannot create a block in highpri mode"); - - // Allocate a new block (at minimum size) and initialize it. - rspq_block_size = RSPQ_BLOCK_MIN_SIZE; - rspq_block = malloc_uncached(sizeof(rspq_block_t) + rspq_block_size*sizeof(uint32_t)); - rspq_block->nesting_level = 0; - rspq_block->rdp_block = NULL; + assertf(!rspq_queue_recording, "cannot create a block while recording a queue"); + + rspq_block_t *block; + int block_size = RSPQ_BLOCK_MIN_SIZE; + + if (!reuse_block) { + block = malloc_uncached(sizeof(rspq_block_t) + block_size*sizeof(uint32_t)); + assertf(block, "Out of memory"); + block->nesting_level = 0; + block->min_ph_level = RSPQ_MAX_BLOCK_NESTING_LEVEL; + block->rdp_block = NULL; + block->atexit = NULL; + rspq_chain_init(&rspq_block_chain, block->cmds, block_size); + } else { + block = reuse_block; + rspq_block_free_atexit_chain(block); + block->nesting_level = 0; + block->min_ph_level = RSPQ_MAX_BLOCK_NESTING_LEVEL; + rspq_chain_reset(&rspq_block_chain, block->cmds, block_size); + } // Switch to the block buffer. From now on, all rspq_writes will // go into the block. rspq_switch_context(NULL); - rspq_switch_buffer(rspq_block->cmds, rspq_block_size, true); + rspq_switch_buffer(block->cmds, block_size, false); + rspq_block_chain.cur = rspq_cur_pointer; + rspq_block_chain.sentinel = rspq_cur_sentinel; + + rspq_block = block; - __rdpq_block_begin(); + if (block->rdp_block) + __rdpq_block_recycle(block->rdp_block); + else + __rdpq_block_begin(); } rspq_block_t* rspq_block_end(void) @@ -1132,35 +1423,29 @@ void rspq_block_free(rspq_block_t *block) // Free RDP blocks first __rdpq_block_free(block->rdp_block); - // Start from the commands in the first chunk of the block - int size = RSPQ_BLOCK_MIN_SIZE; - void *start = block; - uint32_t *ptr = block->cmds + size; - while (1) { - // Rollback until we find a non-zero command - while (*--ptr == 0x00) {} - uint32_t cmd = *ptr; - - // If the last command is a JUMP - if (cmd>>24 == RSPQ_CMD_JUMP) { - // Free the memory of the current chunk. - free_uncached(start); - // Get the pointer to the next chunk - start = UncachedAddr(0x80000000 | (cmd & 0xFFFFFF)); - if (size < RSPQ_BLOCK_MAX_SIZE) size *= 2; - ptr = (uint32_t*)start + size; - continue; - } - // If the last command is a RET - if (cmd>>24 == RSPQ_CMD_RET) { - // This is the last chunk, free it and exit - free_uncached(start); - return; - } - // The last command is neither a JUMP nor a RET: - // this is an invalid chunk of a block, better assert. - assertf(0, "invalid terminator command in block: %08lx\n", cmd); - } + rspq_chain_free(block->cmds, RSPQ_BLOCK_MIN_SIZE); + + // Lastly, invoke callbacks (in reverse order of registration) + rspq_block_free_atexit_chain(block); + + free_uncached(block); +} + +void rspq_block_set_placeholder( + rspq_block_t *ph, + rspq_block_t *ph_target +) { + uint32_t slot = (uint32_t)ph; + assertf(slot < RSPQ_BLOCK_PLACEHOLDER_COUNT, "Invalid placeholder: %08lX", slot); + slot = (RSPQ_MAX_BLOCK_NESTING_LEVEL-1) - slot; + + assertf(ph_target->nesting_level == 0, "Nested blocks cannot be used as placeholders"); + + uint32_t ptr_stack = offsetof(rsp_queue_t, rspq_pointer_stack); + rspq_int_write(RSPQ_CMD_WRITE_WORD, + ptr_stack + (slot << 2), + PhysicalAddr(ph_target->cmds) + ); } void rspq_block_run(rspq_block_t *block) @@ -1170,8 +1455,40 @@ void rspq_block_run(rspq_block_t *block) // in highpri mode (to avoid stepping on the call stack of lowpri). This // would basically mean that a block can either work in highpri or in lowpri // mode, but it might be an acceptable limitation. + // NOTE: during highpri mode, the slot 0 (RSPQ_LOWPRI_HANDOFF_SLOT) might be + // in-use if highpri preempted lowpri exactly during a buffer swap, so make + // sure to avoid using it. assertf(rspq_ctx != &highpri, "block run is not supported in highpri mode"); + if((uint32_t)block < RSPQ_BLOCK_PLACEHOLDER_COUNT) + { + assertf(rspq_block, "Calling a placeholder is only supported inside a block"); + uint32_t slot = (RSPQ_MAX_BLOCK_NESTING_LEVEL-1) - (uint32_t)block; + + if(slot < rspq_block->min_ph_level) { + rspq_block->min_ph_level = slot; + } + + uint32_t dmem_ph_addr = offsetof(rsp_queue_t, rspq_pointer_stack); + dmem_ph_addr += slot << 2; + + // always assume the called block has no further nesting, this is asserted in 'rspq_block_set_placeholder'. + const uint32_t block_nesting = 0; + + rspq_int_write(RSPQ_CMD_CALL, dmem_ph_addr, (block_nesting << 2) | (1<<31)); + + // bump up the current blocks level, it only has to make room for one level once + if (rspq_block->nesting_level == 0) { + rspq_block->nesting_level = 1; + } + + // set RDP to unknown state, since we don't know yet what it may contain + __rdpq_block_run_maybe_rdp(); + return; + } + + assertf(block->nesting_level < block->min_ph_level, "Block nesting level overlaps with used placeholders"); + // Write the CALL op. The second argument is the nesting level // which is used as stack slot in the RSP to save the current // pointer position. @@ -1185,14 +1502,23 @@ void rspq_block_run(rspq_block_t *block) assertf(rspq_block->nesting_level < RSPQ_MAX_BLOCK_NESTING_LEVEL, "reached maximum number of nested block runs"); } + if (rspq_queue_recording && rspq_queue_recording->nesting_level <= block->nesting_level) { + rspq_queue_recording->nesting_level = block->nesting_level + 1; + assertf(rspq_queue_recording->nesting_level < RSPQ_MAX_BLOCK_NESTING_LEVEL, + "reached maximum number of nested queue runs"); + } // Notify rdpq engine we have run a block - __rdpq_block_run(block->rdp_block); + if(block->rdp_block) { + __rdpq_block_run_with_rdp(block->rdp_block); + } else { + __rdpq_block_run_no_rdp(); + } } void rspq_block_run_rsp(int nesting_level) { - __rdpq_block_run(NULL); + __rdpq_block_run_no_rdp(); if (rspq_block && rspq_block->nesting_level <= nesting_level) { rspq_block->nesting_level = nesting_level + 1; assertf(rspq_block->nesting_level < RSPQ_MAX_BLOCK_NESTING_LEVEL, @@ -1200,15 +1526,130 @@ void rspq_block_run_rsp(int nesting_level) } } +void rspq_block_atexit(void (*cb)(void*), void* ctx) +{ + assertf(rspq_block, "no block is being created"); + rspq_block_cb_t *new_cb = malloc(sizeof(rspq_block_cb_t)); + assertf(new_cb, "Out of memory"); + new_cb->cb = cb; + new_cb->ctx = ctx; + // Insert at the front of the list. + // This means that iteration will happen in reverse order of insertion. + new_cb->next = rspq_block->atexit; + rspq_block->atexit = new_cb; +} + +rspq_queue_t* rspq_queue_create(void) +{ + rspq_queue_t *q = malloc_uncached(sizeof(rspq_queue_t) + RSPQ_BLOCK_MIN_SIZE * sizeof(uint32_t)); + assertf(q, "Out of memory"); + + rspq_chain_init(&q->chain, q->cmds, RSPQ_BLOCK_MIN_SIZE); + q->run_start = q->chain.first_chunk; + q->nesting_level = 0; + + __rdpq_tracking_state_reset(&q->rdpq_tracking); + return q; +} + +void rspq_queue_switch(rspq_queue_t* q) +{ + assertf(rspq_ctx != &highpri, "cannot switch queue in highpri mode"); + assertf(!rspq_block, "cannot switch queue while creating a block"); + + if (q == rspq_queue_recording) + return; + + if (rspq_queue_recording) { + rspq_queue_recording->chain.cur = rspq_cur_pointer; + rspq_queue_recording->chain.sentinel = rspq_cur_sentinel; + // there is already a queue active we want to move away from now, + // save its current tracking back to the queue, + // and restore what the queue backed up + SWAP(rdpq_tracking, rspq_queue_recording->rdpq_tracking); + } + + if (!q) { + rspq_queue_recording = NULL; + rspq_switch_context(&lowpri); + return; + } + + // we want to move into a queue, take the tracking state from the queue + // and backup the current main one + SWAP(rdpq_tracking, q->rdpq_tracking); + rspq_queue_recording = q; + + if (rspq_ctx != NULL) + rspq_switch_context(NULL); + rspq_cur_pointer = q->chain.cur; + rspq_cur_sentinel = q->chain.sentinel; +} + +void rspq_queue_run(rspq_queue_t* q) +{ + assertf(q, "queue is NULL"); + assertf(rspq_ctx != &highpri, "queue run is not supported in highpri mode"); + assertf(!rspq_block, "queue run is not supported while creating a block"); + + // Switch to the lowpri context. + rspq_queue_t *prev_recording = rspq_queue_recording; + if (prev_recording) + rspq_queue_switch(NULL); + + // Terminate the current chunk with a RET command (do not advance cur). + rspq_append1(q->chain.cur, RSPQ_CMD_RET, q->nesting_level << 2); + + // Enqueue a CALL in the lowpri context to run the queue. + rspq_int_write(RSPQ_CMD_CALL, PhysicalAddr(q->run_start), q->nesting_level << 2); + + // Update run_start so that the next call will start from the current position. + q->run_start = q->chain.cur; + + // Switch back to the recording context. + if (prev_recording) + rspq_queue_switch(prev_recording); + + // after the queue was executed, we can take its tracking state going forward + rdpq_tracking = q->rdpq_tracking; + __rdpq_tracking_state_reset(&q->rdpq_tracking); +} + +void rspq_queue_clear(rspq_queue_t* q) +{ + assertf(q, "queue is NULL"); + + rspq_chain_reset(&q->chain, q->chain.first_chunk, RSPQ_BLOCK_MIN_SIZE); + q->nesting_level = 0; + q->run_start = q->chain.first_chunk; + __rdpq_tracking_state_reset(&q->rdpq_tracking); + + if (q == rspq_queue_recording) { + rspq_cur_pointer = q->chain.cur; + rspq_cur_sentinel = q->chain.sentinel; + } +} + +void rspq_queue_destroy(rspq_queue_t* q) +{ + assertf(q, "queue is NULL"); + assertf(q != rspq_queue_recording, "cannot destroy queue while recording it"); + + rspq_chain_free(q->chain.first_chunk, RSPQ_BLOCK_MIN_SIZE); + free_uncached(q); +} + void rspq_noop() { - rspq_int_write(RSPQ_CMD_NOOP); + // WRITE_STATUS performs a write to COP0_SP_STATUS, which does nothing if the argument is zero + rspq_int_write(RSPQ_CMD_WRITE_STATUS, 0); } rspq_syncpoint_t rspq_syncpoint_new(void) { assertf(rspq_ctx != &highpri, "cannot create syncpoint in highpri mode"); assertf(!rspq_block, "cannot create syncpoint in a block"); + assertf(!rspq_queue_recording, "cannot create syncpoint in a queue"); assertf(rspq_ctx != &highpri, "cannot create syncpoint in highpri mode"); // To create a syncpoint, schedule a CMD_TEST_WRITE_STATUS command that: @@ -1247,14 +1688,114 @@ void rspq_syncpoint_wait(rspq_syncpoint_t sync_id) // Spinwait until the the syncpoint is reached. // TODO: with the kernel, it will be possible to wait for the RSP interrupt // to happen, without spinwaiting. - RSP_WAIT_LOOP(200) { + ACCT_SCOPE(ACCT_CAT_RSPQ) RSP_WAIT_LOOP(200) { + __rspq_deferred_poll(); if (rspq_syncpoint_check(sync_id)) break; } } +/** + * @brief Polls the deferred calls list, calling callbacks ready to be called. + * + * This function will check the deferred call list and if there is one callback + * ready to be called, it will call it and remove it from the list. + * + * The function will process maximum one callback per call, so that it does + * not steal too much CPU time. + * + * @return true if there are still callbacks to be processed + * @return false if there are no more callbacks to be processed + */ +bool __rspq_deferred_poll(void) +{ + rspq_deferred_call_t *prev = NULL, *cur = defcalls_head; + while (cur != NULL) { + rspq_deferred_call_t *next = cur->next; + + // Since the list is chronologically sorted, once we reach the first + // call that is still waiting for its RSP checkpoint, we can stop. + if (!rspq_syncpoint_check(cur->sync)) + break; + + // If this call requires waiting for SYNC_FULL, check if we reached it. + // Otherwise, jsut skio it and go through the list: maybe a later callback + // does not require RDP and can be called. + if (cur->flags & RSPQ_DCF_WAITRDP) { + int difference = (int)((uint32_t)(cur->sync) - (uint32_t)(__rdpq_syncpoint_at_syncfull)); + if (difference <= 0) + cur->flags &= ~RSPQ_DCF_WAITRDP; + } + + // If this call does not require waiting for next SYNC_FULL, call it. + if (!(cur->flags & RSPQ_DCF_WAITRDP)) { + // Call the deferred calllback. Account the time to user-time as + // this is actually a non-kernel activity that's just deferred. + ACCT_SCOPE(ACCT_CAT_USER) cur->func(cur->arg); + + // Remove it from the list (possibly updating the head/tail pointer) + if (prev) + prev->next = next; + else + defcalls_head = next; + if (!next) + defcalls_tail = prev; + defcalls_count--; + free(cur); + + if (defcalls_count < defcalls_run_threshold) + break; + } else { + prev = cur; + } + + cur = next; + } + + defcalls_run_threshold = (defcalls_run_threshold * 4 / 8) + (defcalls_count * 4 / 8); + + return defcalls_head != NULL; +} + +rspq_syncpoint_t __rspq_call_deferred(void (*func)(void *), void *arg, bool waitrdp) +{ + assertf(rspq_ctx != &highpri, "cannot defer in highpri mode"); + assertf(!rspq_block, "cannot defer in a block"); + assertf(!rspq_queue_recording, "cannot defer in a queue"); + + // Allocate a new deferred call + rspq_deferred_call_t *call = malloc(sizeof(rspq_deferred_call_t)); + assertf(call, "Out of memory"); + call->func = func; + call->arg = arg; + call->next = NULL; + call->sync = rspq_syncpoint_new(); + if (waitrdp) + call->flags |= RSPQ_DCF_WAITRDP; + + // Add it to the list of deferred calls + if (defcalls_tail) { + defcalls_tail->next = call; + } else { + defcalls_head = call; + } + defcalls_tail = call; + defcalls_count++; + + return call->sync; +} + +rspq_syncpoint_t rspq_syncpoint_new_cb(void (*func)(void *), void *arg) +{ + return __rspq_call_deferred(func, arg, false); +} + void rspq_wait(void) { + assertf(!rspq_queue_recording, "cannot wait in a queue"); + assertf(!rspq_block, "cannot wait in a block"); + assertf(rspq_ctx != &highpri, "cannot wait in highpri mode"); + // Check if the RDPQ module was initialized. if (__rdpq_inited) { // If so, a full sync requires also waiting for RDP to finish. @@ -1272,8 +1813,36 @@ void rspq_wait(void) // Update the tracing engine (if enabled) if (rdpq_trace) rdpq_trace(); + + // Make sure to process all deferred calls. Since this is a full sync point, + // it makes sense to give this guarantee to the user. + ACCT_SCOPE(ACCT_CAT_RSPQ) RSP_WAIT_LOOP(500) { + if (!__rspq_deferred_poll()) + break; + } + + // Last thing to check is whether there is a RSP DMA in progress. This is + // basically impossible because RSP DMA is very fast, but we still keep + // this code even just as documentation that we want to ensure that rspq_wait() + // exits with all RSP idle. + if (UNLIKELY(*SP_STATUS & SP_STATUS_DMA_BUSY)) { + ACCT_SCOPE(ACCT_CAT_RSPQ) RSP_WAIT_LOOP(200) { + if (!(*SP_STATUS & SP_STATUS_DMA_BUSY)) + break; + } + } } +/// @cond +void rspq_signal(uint32_t signal) +{ + const uint32_t allowed_mask = SP_WSTATUS_CLEAR_SIG0|SP_WSTATUS_SET_SIG0; + assertf((signal & allowed_mask) == signal, "rspq_signal called with a mask that contains bits outside SIG0: %lx", signal); + + rspq_int_write(RSPQ_CMD_WRITE_STATUS, signal); +} +/// @endcond + static void rspq_dma(void *rdram_addr, uint32_t dmem_addr, uint32_t len, uint32_t flags) { rspq_int_write(RSPQ_CMD_DMA, PhysicalAddr(rdram_addr), dmem_addr, len, flags); @@ -1289,17 +1858,9 @@ void rspq_dma_to_dmem(uint32_t dmem_addr, void *rdram_addr, uint32_t len, bool i rspq_dma(rdram_addr, dmem_addr, len - 1, is_async ? 0 : SP_STATUS_DMA_BUSY | SP_STATUS_DMA_FULL); } -/// @cond -void rspq_signal(uint32_t signal) -{ - const uint32_t allowed_mask = SP_WSTATUS_CLEAR_SIG0|SP_WSTATUS_SET_SIG0; - assertf((signal & allowed_mask) == signal, "rspq_signal called with a mask that contains bits outside SIG0: %lx", signal); - - rspq_int_write(RSPQ_CMD_WRITE_STATUS, signal); -} -/// @endcond - /* Extern inline instantiations. */ +extern inline void rspq_block_begin(void); extern inline rspq_write_t rspq_write_begin(uint32_t ovl_id, uint32_t cmd_id, int size); extern inline void rspq_write_arg(rspq_write_t *w, uint32_t value); extern inline void rspq_write_end(rspq_write_t *w); +extern inline void rspq_call_deferred(void (*func)(void *), void *arg); diff --git a/src/rspq/rspq_internal.h b/src/rspq/rspq_internal.h index ef4fd9ecf..c28e19ecc 100644 --- a/src/rspq/rspq_internal.h +++ b/src/rspq/rspq_internal.h @@ -25,11 +25,9 @@ enum { RSPQ_CMD_INVALID = 0x00, /** - * @brief RSPQ command: No-op - * - * This commands does nothing. It can be useful for debugging purposes. + * @brief RSPQ command: Sets a 32-bit word in DMEM */ - RSPQ_CMD_NOOP = 0x01, + RSPQ_CMD_WRITE_WORD = 0x01, /** * @brief RSPQ command: Jump to another buffer @@ -110,18 +108,6 @@ enum { */ RSPQ_CMD_TEST_WRITE_STATUS = 0x08, - /** - * @brief RSPQ command: Wait for RDP to be idle. - * - * This command will let the RSP spin-wait until the RDP is idle (that is, - * the DP_STATUS_BUSY bit in COP0_DP_STATUS goes to 0). Notice that the - * RDP is fully asynchronous, and reading DP_STATUS_BUSY basically makes - * sense only after a RDP SYNC_FULL command (#rdpq_sync_full()), when it - * really does make sure that all previous commands have finished - * running. - */ - RSPQ_CMD_RDP_WAIT_IDLE = 0x09, - /** * @brief RSPQ Command: send a new buffer to RDP and/or configure it for new commands * @@ -133,7 +119,7 @@ enum { * some RDP commands that will be sent to RDP right away. Sentinel is the end of the * buffer. If cur==sentinel, the buffer is full and no more commands will be written to it. */ - RSPQ_CMD_RDP_SET_BUFFER = 0x0A, + RSPQ_CMD_RDP_SET_BUFFER = 0x09, /** * @brief RSPQ Command: send more data to RDP (appended to the end of the current buffer) @@ -141,7 +127,7 @@ enum { * This commands basically just sets DP_END to the specified argument, allowing new * commands appended in the current buffer to be sent to RDP. */ - RSPQ_CMD_RDP_APPEND_BUFFER = 0x0B, + RSPQ_CMD_RDP_APPEND_BUFFER = 0x0A, }; /** @brief Write an internal command to the RSP queue */ @@ -149,8 +135,18 @@ enum { ///@cond typedef struct rdpq_block_s rdpq_block_t; +typedef struct rspq_block_cb_s rspq_block_cb_t; ///@endcond +/** + * @brief Linked list of callbacks for storage inside a rspq block. + */ +typedef struct rspq_block_cb_s { + void (*cb)(void*); ///< The callback function pointer + void* ctx; ///< The context that will be passed into the callback + rspq_block_cb_t *next; ///< Next callback in the linked list +} rspq_block_cb_t; + /** * @brief A rspq block: pre-recorded array of commands * @@ -160,9 +156,12 @@ typedef struct rdpq_block_s rdpq_block_t; * calls (a block can call another block), up to 8 levels deep. */ typedef struct rspq_block_s { - uint32_t nesting_level; ///< Nesting level of the block - rdpq_block_t *rdp_block; ///< Option RDP static buffer (with RDP commands) - uint32_t cmds[]; ///< Block contents (commands) + uint8_t nesting_level; ///< Nesting level of the block + uint8_t min_ph_level; ///< Lowest slot used by any called placeholders, default: max nesting level + uint8_t padding[2]; ///< Padding + rdpq_block_t *rdp_block; ///< Option RDP static buffer (with RDP commands) + rspq_block_cb_t *atexit; ///< List of callbacks to call upon freeing the block + uint32_t cmds[]; ///< Block contents (commands) } rspq_block_t; /** @brief RDP render mode definition @@ -186,48 +185,71 @@ typedef struct __attribute__((packed)) { uint8_t idmap[RSPQ_MAX_OVERLAYS]; ///< Map of overlay IDs to base IDs } rspq_ovl_table_t; +/** + * @brief RSP profiling data for a single overlay. + */ +typedef struct rspq_profile_slot_dmem_s { + uint32_t total_ticks; + uint32_t sample_count; +} rspq_profile_slot_dmem_t; + /** * @brief RSP Queue data in DMEM. * - * This structure is defined by rsp_queue.S, and represents the + * This structure is defined by rsp_queue.inc, and represents the * top portion of DMEM. */ typedef struct rsp_queue_s { + uint8_t shift_consts[8]; ///< Shift constants rspq_ovl_table_t rspq_ovl_table; ///< Overlay table /** @brief Pointer stack used by #RSPQ_CMD_CALL and #RSPQ_CMD_RET. */ uint32_t rspq_pointer_stack[RSPQ_MAX_BLOCK_NESTING_LEVEL]; uint32_t rspq_dram_lowpri_addr; ///< Address of the lowpri queue (special slot in the pointer stack) uint32_t rspq_dram_highpri_addr; ///< Address of the highpri queue (special slot in the pointer stack) uint8_t banner[32]; ///< Banner + uint32_t cmds[RSPQ_DMEM_BUFFER_SIZE/4]; ///< RSPQ command buffer rspq_rdp_mode_t rdp_mode; ///< RDP current render mode definition uint64_t rdp_scissor_rect; ///< Current RDP scissor rectangle uint32_t rspq_rdp_buffers[2]; ///< RDRAM Address of dynamic RDP buffers uint32_t rspq_rdp_current; ///< Current RDP RDRAM write pointer (normally DP_END) uint32_t rspq_rdp_sentinel; ///< Current RDP RDRAM end pointer (when rdp_current reaches this, the buffer is full) uint32_t rdp_fill_color; ///< Current RDP fill color + uint32_t rspq_dram_addr; ///< Current RDRAM address being processed uint8_t rdp_target_bitdepth; ///< Current RDP target buffer bitdepth uint8_t rdp_syncfull_ongoing; ///< True if a SYNC_FULL is currently ongoing - uint8_t rdpq_debug; ///< Debug mode flag - uint8_t padding; ///< Padding - uint32_t rspq_dram_addr; ///< Current RDRAM address being processed - uint16_t current_ovl; ///< Current overlay ID - uint16_t padding2; ///< Padding +#if RSPQ_PROFILE + uint32_t rspq_profile_cur_slot; + uint32_t rspq_profile_start_time; + rspq_profile_slot_dmem_t rspq_profile_cslots[RSPQ_PROFILE_CSLOT_COUNT]; + rspq_profile_slot_dmem_t rspq_profile_builtin_slot; +#endif } __attribute__((aligned(16), packed)) rsp_queue_t; -/** @brief Address of the RSPQ data header in DMEM (see #rsp_queue_t) */ -#define RSPQ_DATA_ADDRESS 8 - /** @brief ID of the last syncpoint reached by RSP (plus padding). */ extern volatile int __rspq_syncpoints_done[4]; /** @brief Registered overlays */ extern rsp_ucode_t *rspq_overlay_ucodes[RSPQ_MAX_OVERLAYS]; -/** @brief True if we are currently building a block. */ -static inline bool rspq_in_block(void) { - extern rspq_block_t *rspq_block; - return rspq_block != NULL; -} +/** @brief Flag to mark deferred calls that needs to wait for RDP SYNC_FULL */ +#define RSPQ_DCF_WAITRDP (1<<0) + +/** @brief A call deferred for execution after RSP reaches a certain syncpoint */ +typedef struct rspq_deferred_call_s { + union { + void (*func)(void *arg); ///< Function to call + uint32_t flags; ///< Flags (see RSPQ_DCF_*) -- used last 2 bits + }; + void *arg; ///< Argument to pass to the function + rspq_syncpoint_t sync; ///< Syncpoint to wait for + void *next; ///< Next deferred call (linked list) +} rspq_deferred_call_t; + +/** @brief Enqueue a new deferred call. */ +rspq_syncpoint_t __rspq_call_deferred(void (*func)(void *), void *arg, bool waitrdp); + +/** @brief Polls the deferred calls list, calling callbacks ready to be called. */ +bool __rspq_deferred_poll(void); /** @brief True if we are currently in highpri mode */ bool rspq_in_highpri(void); diff --git a/src/rspq/rspq_profile.c b/src/rspq/rspq_profile.c new file mode 100644 index 000000000..ce21af7be --- /dev/null +++ b/src/rspq/rspq_profile.c @@ -0,0 +1,182 @@ +/** + * @file rspq_profile.c + * @author Giovanni Bajo + */ +#include "rspq_profile.h" +#include +#include +#include +#include "rsp.h" +#include "rspq.h" +#include "rspq_internal.h" + +#if RSPQ_PROFILE + +DEFINE_RSP_UCODE(rsp_profile); + +#define CMD_PROFILE_FRAME 0x0 + +static rspq_profile_data_t profile_data; +static uint32_t ovl_id; + +typedef struct { + uint32_t cntr1; + uint32_t padding1; + + rspq_profile_slot_dmem_t ovl[RSPQ_MAX_OVERLAYS]; + rspq_profile_slot_dmem_t cmn[RSPQ_PROFILE_CSLOT_COUNT]; + + uint32_t frame_time; + uint32_t busy_time; + uint32_t frame_last; + uint32_t busy_last; + + uint32_t padding2; + uint32_t cntr2; + + uint32_t padding[2]; +} profile_buffer_t; + +_Static_assert(sizeof(profile_buffer_t) % 16 == 0, "profile_buffer_t size mismatch"); + +static volatile profile_buffer_t cur_profile_buffer __attribute__((aligned(16))); +static profile_buffer_t last_profile_buffer __attribute__((aligned(16))); + +#define PROFILE_DATA_DMEM_ADDRESS (RSPQ_DATA_ADDRESS + offsetof(rsp_queue_t, rspq_profile_data)) + +void rspq_profile_reset(void) +{ + memset(&profile_data, 0, sizeof(profile_data)); + + profile_data.slots[0].name = "Builtin cmds"; + for (int i = 1; i < RSPQ_MAX_OVERLAYS; i++) + { + if (!rspq_overlay_ucodes[i]) continue; + if (i == ovl_id >> RSPQ_OVERLAY_ID_SHIFT) continue; + if (i > 1 && rspq_overlay_ucodes[i] == rspq_overlay_ucodes[i-1]) continue; + profile_data.slots[i].name = rspq_overlay_ucodes[i]->name; + } + + profile_data.slots[RSPQ_PROFILE_CSLOT_WAIT_CPU].name = "Wait CPU"; + profile_data.slots[RSPQ_PROFILE_CSLOT_WAIT_RDP].name = "Wait RDP"; + profile_data.slots[RSPQ_PROFILE_CSLOT_WAIT_RDP_SYNCFULL].name = "Wait SYNC_FULL"; + profile_data.slots[RSPQ_PROFILE_CSLOT_WAIT_RDP_SYNCFULL_MULTI].name = "Wait SYNC_FULLx2"; + profile_data.slots[RSPQ_PROFILE_CSLOT_OVL_SWITCH].name = "Ovl Switch"; +} + +void rspq_profile_start() +{ + ovl_id = rspq_overlay_register(&rsp_profile); + rspq_profile_reset(); +} + +void rspq_profile_stop() +{ + rspq_overlay_unregister(ovl_id); +} + +static void rspq_profile_accumulate(void*) +{ + profile_buffer_t buf; + do { + buf = cur_profile_buffer; + } while (buf.cntr1 != buf.cntr2); + data_cache_hit_invalidate((void*)&cur_profile_buffer, sizeof(cur_profile_buffer)); + + for (int i = 0; i < RSPQ_MAX_OVERLAYS; i++) { + if (profile_data.slots[i].name == NULL) continue; + profile_data.slots[i].total_ticks += buf.ovl[i].total_ticks - last_profile_buffer.ovl[i].total_ticks; + profile_data.slots[i].sample_count += buf.ovl[i].sample_count - last_profile_buffer.ovl[i].sample_count; + } + + int cbase = RSPQ_MAX_OVERLAYS; + for (int i = 0; i < RSPQ_PROFILE_CSLOT_COUNT; i++) { + profile_data.slots[cbase+i].total_ticks += buf.cmn[i].total_ticks - last_profile_buffer.cmn[i].total_ticks; + profile_data.slots[cbase+i].sample_count += buf.cmn[i].sample_count - last_profile_buffer.cmn[i].sample_count; + } + + profile_data.total_ticks += buf.frame_time - last_profile_buffer.frame_time; + profile_data.rdp_busy_ticks += buf.busy_time - last_profile_buffer.busy_time; + profile_data.frame_count++; + + last_profile_buffer = buf; +} + +void rspq_profile_next_frame() +{ + rspq_write(ovl_id, CMD_PROFILE_FRAME, PhysicalAddr(&cur_profile_buffer)); + rspq_call_deferred(rspq_profile_accumulate, NULL); +} + +#define RCP_TICKS_TO_USECS(ticks) (((ticks) * 1000000ULL) / RCP_FREQUENCY) +#define PERCENT(fraction, total) ((total) > 0 ? (float)(fraction) * 100.0f / (float)(total) : 0.0f) + +static void rspq_profile_dump_overlay(rspq_profile_slot_t *slot, uint64_t frame_avg) +{ + uint64_t mean = slot->total_ticks / profile_data.frame_count; + uint64_t mean_us = RCP_TICKS_TO_USECS(mean); + float relative = PERCENT(mean, frame_avg); + + char buf[64]; + sprintf(buf, "%3.2f%%", relative); + + debugf("%-25s %10llu %10lluus %10s\n", + slot->name, + slot->sample_count / profile_data.frame_count, + mean_us, + buf); +} + +void rspq_profile_dump() +{ + if (profile_data.frame_count == 0) + return; + + uint64_t frame_avg = profile_data.total_ticks / profile_data.frame_count; + uint64_t frame_avg_us = RCP_TICKS_TO_USECS(frame_avg); + + uint64_t counted_time = 0; + for (size_t i = 0; i < RSPQ_PROFILE_SLOT_COUNT; i++) counted_time += profile_data.slots[i].total_ticks; + + // The counted time could be slightly larger than the total time due to various measurement errors + uint64_t overhead_time = profile_data.total_ticks > counted_time ? profile_data.total_ticks - counted_time : 0; + uint64_t overhead_avg = overhead_time / profile_data.frame_count; + uint64_t overhead_us = RCP_TICKS_TO_USECS(overhead_avg); + + float overhead_relative = PERCENT(overhead_avg, frame_avg); + + uint64_t rdp_busy_avg = profile_data.rdp_busy_ticks / profile_data.frame_count; + uint64_t rdp_busy_us = RCP_TICKS_TO_USECS(rdp_busy_avg); + float rdp_utilisation = PERCENT(rdp_busy_avg, frame_avg); + + debugf("%-25s %10s %12s %10s\n", "Slot", "Cnt/Frame", "Avg/Frame", "Rel/Frame"); + debugf("------------------------------------------------------------\n"); + + for (int i = 0; i < RSPQ_PROFILE_SLOT_COUNT; i++) { + if (!profile_data.slots[i].name) continue; + rspq_profile_dump_overlay(&profile_data.slots[i], frame_avg); + } + + debugf("------------------------------------------------------------\n"); + debugf("Profiled frames: %12lld\n", profile_data.frame_count); + debugf("Frames per second: %12.1f\n", (float)RCP_FREQUENCY/(float)frame_avg); + debugf("Average frame time: %10lldus\n", frame_avg_us); + debugf("RDP busy time: %10lldus (%2.2f%%)\n", rdp_busy_us, rdp_utilisation); + debugf("Unrecorded time: %10lldus (%2.2f%%)\n", overhead_us, overhead_relative); + debugf("\n"); +} + +void rspq_profile_get_data(rspq_profile_data_t *data) +{ + memcpy(data, &profile_data, sizeof(profile_data)); + +} + +#else +void rspq_profile_start(void) { } +void rspq_profile_stop(void) { } +void rspq_profile_reset(void) { } +void rspq_profile_next_frame(void) { } +void rspq_profile_dump(void) { } +void rspq_profile_get_data(rspq_profile_data_t *data) { } +#endif \ No newline at end of file diff --git a/src/rspq/rspq_symbols.h.template b/src/rspq/rspq_symbols.h.template new file mode 100644 index 000000000..2a4554ad3 --- /dev/null +++ b/src/rspq/rspq_symbols.h.template @@ -0,0 +1,6 @@ +#ifndef __RSPQ_SYMBOLS +#define __RSPQ_SYMBOLS + +#define RSPQ_OVL_DATA_ADDR (0x:OVL_DATA_ADDR: & 0xFFF) + +#endif \ No newline at end of file diff --git a/src/rtc.c b/src/rtc.c index 2f17bc80a..8b9534377 100644 --- a/src/rtc.c +++ b/src/rtc.c @@ -5,606 +5,380 @@ * @ingroup rtc */ -#include -#include -#include -#include "system.h" +#include "bb_rtc.h" +#include "dd.h" +#include "debug.h" +#include "joybus/joybus_rtc.h" +#include "libcart/cart.h" #include "n64sys.h" -#include "joybus.h" -#include "timer.h" #include "rtc.h" +#include "rtc_internal.h" +#include "system.h" +#include "timer.h" /** - * @brief Joybus real-time clock identifier. - * - * A magic value in the SI RTC status response that indicates the Joybus RTC - * is present. + * @addtogroup rtc + * @{ */ -#define JOYBUS_RTC_IDENTIFIER 0x1000 -/** - * @brief Duration (in milliseconds) to wait after writing a Joybus RTC block. - * - * The software should wait for the previous RTC write to finish before issuing - * another Joybus RTC command. Ideally, you could read the RTC status byte to - * determine when to proceed, but some RTC reproductions do not correctly - * implement the RTC status response, so a delay is used for compatibility. - */ -#define JOYBUS_RTC_WRITE_BLOCK_DELAY 20 -/** - * @brief Duration (in milliseconds) to wait after setting Joybus RTC time. - * - * 64drive hw2 only updates the RTC readout a few times per second, so it is - * possible to write a new time, then read back the previous time before the - * 64drive clock ticks to update the "shadow interface" that the SI reads from. - * - * Without this delay, the #rtc_is_writable test may fail intermittently on - * 64drive hw2. - */ -#define JOYBUS_RTC_WRITE_FINISHED_DELAY 500 +/** @brief RTC initialization state enumeration */ +typedef enum { + RTC_STATE_INIT = 0, ///< Init has not been called yet + RTC_STATE_JOYBUS_DETECTING, ///< Detecting Joybus RTC + RTC_STATE_JOYBUS_STARTING, ///< Starting Joybus RTC + RTC_STATE_JOYBUS_READING, ///< Reading Joybus RTC + RTC_STATE_READY, ///< RTC subsystem is ready +} rtc_state_t; -/** - * @brief The Joybus RTC is running. - * - * It is safe to read the current time from the RTC. - */ -#define JOYBUS_RTC_STATUS_RUNNING 0x00 -/** - * @brief The Joybus RTC is stopped. - * - * It is safe to write new time data to the RTC. - */ -#define JOYBUS_RTC_STATUS_STOPPED 0x80 +/** @brief Convenience macro to spinlock while waiting for RTC subsystem to initialize. */ +#define WAIT_FOR_RTC_READY() while( rtc_state != RTC_STATE_READY ) { /* Spinlock! */ } -/** - * @brief Control mode for setting the Joybus RTC date/time. - * - * Clears the write-protection bits and sets the stop bit. - */ -#define JOYBUS_RTC_CONTROL_MODE_SET 0x0004 -/** - * @brief Control mode for normal operation of the Joybus RTC. - * - * Clears the stop bit sets the write-protection bits. - * - * 0x0100 is the block 1 write-protection bit. - * 0x0200 is the block 2 write-protection bit. - */ -#define JOYBUS_RTC_CONTROL_MODE_RUN 0x0300 +/** @brief RTC initial timestamp (1996-01-01 00:00:00) */ +#define RTC_CACHE_TIME_INIT 820454400 -/** - * @brief Invalidate the most-recent RTC time cache after 1 second. - */ -#define RTC_GET_CACHE_INVALIDATE_TICKS (TICKS_PER_SECOND) +// MARK: Static variables -/** - * @brief Tick counter state when #rtc_get cache was last updated. - * - * Set to 0 to manually invalidate the #rtc_get cache. - * Cache will invalidate every #RTC_GET_CACHE_INVALIDATE_TICKS. - */ -static int64_t rtc_get_cache_ticks = 0; +static volatile rtc_state_t rtc_state = RTC_STATE_INIT; +static rtc_source_t rtc_source = RTC_SOURCE_NONE; +static int rtc_sync_result = RTC_ESUCCESS; -/** - * @brief Real-time clock detection values. - */ -typedef enum rtc_type_t -{ - /** @brief No RTC detected. */ - RTC_NONE = 0, - /** @brief Joybus RTC detected. */ - RTC_JOYBUS = 1, - /** @brief N64 Disk Drive RTC detected. (Not implemented) */ - RTC_DD = 2, - /** @brief RTC detection has not been occurred yet. */ - RTC_UNKNOWN = -1, -} rtc_type_t; +/** @brief Tick counter state when rtc_cache_time was last updated. */ +static int64_t rtc_cache_ticks = 0; +/** @brief Internal cache of the time from the last sync with the hardware RTC. */ +static time_t rtc_cache_time = RTC_CACHE_TIME_INIT; -/** - * @brief Decode a packed binary-coded decimal number. - * - * @param[in] bcd - * packed binary-coded decimal number - * - * @return the decoded integer - */ -static uint8_t bcd_to_byte(uint8_t bcd) -{ - uint8_t hi = (bcd & 0xF0) >> 4; - uint8_t lo = bcd & 0x0F; - return (hi * 10) + lo; -} +// MARK: Internal functions /** - * @brief Encode a packed binary-coded decimal number. + * @brief Read the current date/time from the real-time clock subsystem. * - * @param[in] byte - * integer to encode + * @param[out] out pointer to the output time_t * - * @return the encoded packed binary-coded decimal number + * @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 clock time is not representable */ -static uint8_t byte_to_bcd(uint8_t byte) +static int rtc_get_time( time_t *out ) { - byte %= 100; - return ((byte / 10) << 4) | (byte % 10); -} - -/** - * @brief Lookup table for number of days in each month. - * - * Used by #rtc_normalize_time to clamp the day-of-month value. - * - * Does not account for leap years in the month of February. - */ -static const uint8_t DAYS_IN_MONTH[] = - {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + // Act like a dumb software clock if not initialized + if( rtc_state != RTC_STATE_INIT ) WAIT_FOR_RTC_READY(); -/** - * @brief Read the status of the Joybus real-time clock. - * - * The RTC status response contains a byte-swapped identifier half-word that - * indicates an RTC is present on the cartridge and a status byte to indicate - * whether the RTC is in the "stopped" state. - * - * This is a low-level utility function that is used by - * #rtc_init and #joybus_rtc_is_stopped. - * - * @return the normalized Joybus real-time clock status response data - */ -static uint32_t joybus_rtc_status( void ) -{ - const uint64_t input[JOYBUS_BLOCK_DWORDS] = + if( rtc_sync_result != RTC_ESUCCESS ) { - 0x00000000ff010306, - 0xfffffffe00000000, - 0, - 0, - 0, - 0, - 0, - 1 - }; - uint64_t output[JOYBUS_BLOCK_DWORDS]; - - joybus_exec( input, output ); + return rtc_sync_result; + } - uint8_t * recv_bytes = (uint8_t *)&output[1]; + long long now_ticks = get_ticks(); + long long seconds_since = (now_ticks - rtc_cache_ticks) / TICKS_PER_SECOND; + time_t new_time = rtc_cache_time + seconds_since; + rtc_range_t range = rtc_get_supported_range(); + // Wrap the time around if it goes out of bounds + while( new_time < range.min ) new_time += range.max - range.min; + while( new_time > range.max ) new_time -= range.max - range.min; - return ( - /* Intentional un-swap of identifier bytes */ - ((uint32_t)recv_bytes[1] << 16) | - ((uint32_t)recv_bytes[0] << 8) | - recv_bytes[2] - ); + *out = new_time; + return RTC_ESUCCESS; } /** - * @brief Read a block of data from the Joybus real-time clock. + * @brief Set a new date/time for the real-time clock subsystem. * - * This is a low-level utility function that is used by - * #joybus_rtc_read_control and #rtc_get. + * @param new_time the new time to set the RTC to * - * @param[in] block - * Which RTC block to read from (0-2) - * - * @param[out] data - * Destination pointer for the RTC block data - * - * @return the status byte from the Joybus real-time clock + * @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 */ -static uint8_t joybus_rtc_read( uint8_t block, uint64_t * data ) +static int rtc_set_time( time_t new_time ) { - assert(block <= 2); + // Act like a dumb software clock if not initialized + if( rtc_state != RTC_STATE_INIT ) WAIT_FOR_RTC_READY(); - uint64_t input[JOYBUS_BLOCK_DWORDS] = + rtc_range_t range = rtc_get_supported_range(); + if( new_time < range.min || new_time > range.max ) { - 0x0000000002090700 | block, - 0xffffffffffffffff, - 0xfffe000000000000, - 0, - 0, - 0, - 0, - 1 - }; - uint64_t output[JOYBUS_BLOCK_DWORDS]; - - joybus_exec( input, output ); + return RTC_EBADTIME; + } - *data = output[1]; + int result = RTC_ESUCCESS; + if( rtc_source == RTC_SOURCE_JOYBUS ) + { + result = joybus_rtc_set_time( new_time ); + } + else if( rtc_source == RTC_SOURCE_DD ) + { + result = dd_rtc_set_time( new_time ); + } + else if( rtc_source == RTC_SOURCE_BB ) + { + result = bb_rtc_set_time( new_time ); + } - return output[2] >> 56; + if( result == RTC_ESUCCESS ) + { + rtc_cache_time = new_time; + rtc_cache_ticks = get_ticks(); + } + return rtc_sync_result = result; } /** - * @brief Write a block of data to the Joybus real-time clock. - * - * This is a low-level utility function that is used by - * #joybus_rtc_write_control and #joybus_rtc_write_time. - * - * @param[in] block - * Which RTC block to write to (0-2) - * - * @param[out] data - * RTC block data to write - * - * @return the status byte from the Joybus real-time clock + * @brief Resynchronize the subsystem's time with the source clock. */ -static uint8_t joybus_rtc_write( uint8_t block, const uint64_t * data ) +static int rtc_resync_time( void ) { - assert(block <= 2); - - uint64_t input[JOYBUS_BLOCK_DWORDS] = + time_t new_time = rtc_cache_time; + int result = RTC_ESUCCESS; + if( rtc_source == RTC_SOURCE_JOYBUS ) { - 0x000000000A010800 | block, - *data, - 0xfffe000000000000, - 0, - 0, - 0, - 0, - 1 - }; - uint64_t output[JOYBUS_BLOCK_DWORDS]; - - joybus_exec( input, output ); + result = joybus_rtc_get_time( &new_time ); + } + else if( rtc_source == RTC_SOURCE_DD ) + { + result = dd_rtc_get_time( &new_time ); + } + else if( rtc_source == RTC_SOURCE_BB ) + { + result = bb_rtc_get_time( &new_time ); + } - return output[2] >> 56; + if( result == RTC_ESUCCESS ) + { + rtc_cache_time = new_time; + rtc_cache_ticks = get_ticks(); + } + return rtc_sync_result = result; } -/** - * @brief Read the status of the Joybus real-time clock. - * - * The RTC should be stopped when the control registers are in - * #JOYBUS_RTC_CONTROL_MODE_SET and the time is ready to be written to. - * - * @return whether the Joybus RTC status is stopped - */ -static bool joybus_rtc_is_stopped( void ) -{ - return (joybus_rtc_status() & 0xFF) == JOYBUS_RTC_STATUS_STOPPED; -} +// MARK: Public functions -/** - * @brief Read the control block from the Joybus real-time clock. - * - * The control registers should generally be one of the pre-defined modes: - * #JOYBUS_RTC_CONTROL_MODE_SET when writing to the RTC. - * #JOYBUS_RTC_CONTROL_MODE_RUN when not writing to the RTC. - * - * The calibration data is an opaque value that should be propagated when - * calling #joybus_rtc_write_control. Emulators and flash carts may return - * zeroes for this data, which is also fine. In the interest of compatibility - * and accuracy, this data should be preserved, but its intended shape and - * purpose is unknown. - * - * This is a low-level function that needs to be used in proper sequence. - * For normal use, call the the high-level #rtc_init and #rtc_set functions - * which handle the RTC control block writes, delays, and status checks. - * - * @param[out] control - * Destination pointer for the RTC control register data. - * - * @param[out] calibration - * Destination pointer for the RTC calibration data. - */ -static void joybus_rtc_read_control( uint16_t * control, uint32_t * calibration ) +static void rtc_init_joybus_read_time_callback( int error, time_t rtc_time ) { - uint64_t data; - joybus_rtc_read( 0, &data ); + // If the state has changed, abort the callback + if( rtc_state != RTC_STATE_JOYBUS_READING ) return; - if( control != NULL ) *control = data >> 48; - if( calibration != NULL ) *calibration = data; -} + if( error == RTC_ESUCCESS ) + { + // Set the RTC subsystem to Joybus RTC time + rtc_cache_time = rtc_time; + rtc_cache_ticks = get_ticks(); + rtc_sync_result = RTC_ESUCCESS; + } + else + { + // Don't initialize into a broken state; try to fall-back to DD! + // DD RTC should already be synced during rtc_init_async + if( sys_dd() && rtc_sync_result == RTC_ESUCCESS ) + { + rtc_source = RTC_SOURCE_DD; + } + else + { + // If DD is unavailable or also broken, fall-back to software RTC + rtc_source = RTC_SOURCE_NONE; + rtc_sync_result = RTC_ESUCCESS; + } + } -/** - * @brief Read the current date/time from the Joybus real-time clock. - * - * The result of calling this function when the Joybus RTC is not - * present is undefined and may not be safe. #rtc_get will not - * call this function if the Joybus RTC was not detected. - * - * @param[out] rtc_time - * Destination pointer for the RTC time data structure - */ -static void joybus_rtc_read_time( rtc_time_t * rtc_time ) -{ - uint64_t data; - joybus_rtc_read( 2, &data ); - - uint8_t * bytes = (uint8_t *)&data; - - rtc_time->sec = bcd_to_byte(bytes[0]); - rtc_time->min = bcd_to_byte(bytes[1]); - rtc_time->hour = bcd_to_byte(bytes[2] - 0x80); - rtc_time->day = bcd_to_byte(bytes[3]); - rtc_time->week_day = bcd_to_byte(bytes[4]); - rtc_time->month = bcd_to_byte(bytes[5]) - 1; - rtc_time->year = bcd_to_byte(bytes[6]); - rtc_time->year += (uint16_t)bcd_to_byte(bytes[7]) * 100; - rtc_time->year += 1900; + rtc_state = RTC_STATE_READY; } -/** - * @brief Write the control block to the Joybus real-time clock. - * - * Typically this would be set to one of two "modes": - * Use #JOYBUS_RTC_CONTROL_MODE_SET before writing to block 2 to stop the RTC. - * Use #JOYBUS_RTC_CONTROL_MODE_RUN after writing to block 2 to resume the RTC. - * - * It may take some time after the SI command responds for the write operation - * to actually complete. It is recommended to add a delay after this function - * using #JOYBUS_RTC_WRITE_BLOCK_DELAY to ensure the control block data was fully - * written. After the delay, check the RTC status byte to confirm that it is - * in the expected state. - * - * It is strongly recommended to propagate the calibration data returned from - * #joybus_rtc_read_control into the calibration parameter of this function. - * If you have any reason to modify or omit the calibration data, please - * update this documentation with an explanation of why and how you would - * want to do this. - * - * Generally, you should not need to call this function directly. Prefer - * calling the high-level #rtc_init and #rtc_set functions which handle - * the necessary delays, status checks, and calibration propagation. - * - * This is a low-level function that needs to be used in proper sequence. - * For normal use, call the the high-level #rtc_init and #rtc_set functions - * which handle the RTC control block writes, delays, and status checks. - * - * @param[in] control - * The control register data - * - * @param[in] calibration - * The opaque calibration data from #joybus_rtc_read_control - */ -static void joybus_rtc_write_control( uint16_t control, uint32_t calibration ) +static void rtc_init_joybus_starting_callback( void ) { - uint64_t data = (uint64_t)control << 48 | calibration; - joybus_rtc_write( 0, &data ); -} + // If the state has changed, abort the callback + if( rtc_state != RTC_STATE_JOYBUS_STARTING ) return; -/** - * @brief Write a new date/time to the Joybus real-time clock. - * - * If writes are not supported by the emulator or flash cart, this function - * will fail silently. It is recommended to call #rtc_is_writable early - * in your program to detect whether the RTC actually supports writes. - * - * This will also fail silently if block 2 of the RTC is write-protected. - * - * The RTC control registers should be set to #JOYBUS_RTC_CONTROL_MODE_SET - * using #joybus_rtc_write_control before calling this function. - * - * The RTC control registers should be set to #JOYBUS_RTC_CONTROL_MODE_RUN - * using #joybus_rtc_write_control after calling this function. - * - * It may take up to 20 milliseconds after this function returns for the - * write to actually complete on real hardware. It is recommended to wait - * using #JOYBUS_RTC_WRITE_BLOCK_DELAY before setting the RTC control registers - * back to #JOYBUS_RTC_CONTROL_MODE_RUN after setting the time. On 64drive - * it is recommended to wait using #JOYBUS_RTC_WRITE_FINISHED_DELAY before - * reading the time to ensure the internal clock has ticked and copied - * the new time data into the "SI shadow interface". - * - * This is a low-level function that needs to be used in proper sequence. - * For normal use, call the the high-level #rtc_set function which handles - * the RTC control block writes, delays, and status checks. - * - * @param[in] rtc_time - * Source pointer for the RTC time data structure - */ -static void joybus_rtc_write_time( const rtc_time_t * rtc_time ) -{ - uint64_t data; - uint8_t * bytes = (uint8_t *)&data; - - int year = rtc_time->year - 1900; - bytes[0] = byte_to_bcd(rtc_time->sec); - bytes[1] = byte_to_bcd(rtc_time->min); - bytes[2] = byte_to_bcd(rtc_time->hour) + 0x80; - bytes[3] = byte_to_bcd(rtc_time->day); - bytes[4] = byte_to_bcd(rtc_time->week_day); - bytes[5] = byte_to_bcd(rtc_time->month + 1); - bytes[6] = byte_to_bcd(year); - bytes[7] = byte_to_bcd(year / 100); - - joybus_rtc_write( 2, &data ); + rtc_state = RTC_STATE_JOYBUS_READING; + joybus_rtc_get_time_async( rtc_init_joybus_read_time_callback ); } -/** - * @brief Determine which RTC type is available (if any). - * - * This function will only actually perform RTC detection the - * first time it is called. All subsequent calls will return - * a cached value. - * - * The RTC type should never change while the N64 is powered-on. - * - * @return #RTC_JOYBUS or #RTC_NONE (#RTC_DD is not supported yet) - */ -static rtc_type_t rtc_present( void ) +static void rtc_init_joybus_detecting_callback( bool detected, joybus_rtc_status_t status ) { - static rtc_type_t rtc_detected = RTC_UNKNOWN; - if( rtc_detected != RTC_UNKNOWN ) - { - return rtc_detected; - } - else if( (joybus_rtc_status() >> 8) == JOYBUS_RTC_IDENTIFIER ) + // If the state has changed, abort the callback + if( rtc_state != RTC_STATE_JOYBUS_DETECTING ) return; + + if( detected && !status.crystal_bad && !status.battery_bad ) { - return rtc_detected = RTC_JOYBUS; + // Always prefer Joybus RTC if it is available and operational + rtc_source = RTC_SOURCE_JOYBUS; + rtc_state = RTC_STATE_JOYBUS_STARTING; + joybus_rtc_set_stopped_async( false, rtc_init_joybus_starting_callback ); } else { - return rtc_detected = RTC_NONE; + rtc_state = RTC_STATE_READY; } } -/** - * @brief Hook function for newlib gettimeofday to get the current date/time. - * - * @return the current timestamp in seconds or -1 if RTC is unavailable. - */ -static time_t newlib_time_hook( void ) +void rtc_init_async( void ) { - static rtc_time_t rtc_time; - if( !rtc_get( &rtc_time ) ) return -1; - - struct tm time; - time.tm_sec = rtc_time.sec; - time.tm_min = rtc_time.min; - time.tm_hour = rtc_time.hour; - time.tm_mday = rtc_time.day; - time.tm_mon = rtc_time.month; - time.tm_year = rtc_time.year - 1900; - time.tm_isdst = -1; /* Auto-detect Daylight Saving Time */ - - return mktime( &time ); + assert( rtc_state == RTC_STATE_INIT ); + + /* Reset subsystem state */ + rtc_source = RTC_SOURCE_NONE; + rtc_cache_time = RTC_CACHE_TIME_INIT; + rtc_cache_ticks = 0; + rtc_sync_result = RTC_ESUCCESS; + + /* Enable newlib time integration */ + rtc_hooks_t hooks = { &rtc_get_time, &rtc_set_time }; + hook_rtc_calls( &hooks ); + + // BBPlayer RTC will never co-exist with other RTCs + if( sys_bbplayer() ) + { + rtc_source = RTC_SOURCE_BB; + if( rtc_resync_time() != RTC_ESUCCESS ) + { + // If BB RTC is broken, fall-back to software RTC + rtc_source = RTC_SOURCE_NONE; + rtc_sync_result = RTC_ESUCCESS; + } + rtc_state = RTC_STATE_READY; + } + else + { + // 64DD RTC may co-exist with the Joybus RTC + if( sys_dd() ) + { + rtc_source = RTC_SOURCE_DD; + rtc_resync_time(); + } + + // Initialize libcart (if necessary) to detect common flashcart types + if( cart_type <= CART_NULL ) cart_init(); + + // Attempt to detect and start the Joybus RTC asynchronously + rtc_state = RTC_STATE_JOYBUS_DETECTING; + joybus_rtc_detect_async( rtc_init_joybus_detecting_callback ); + } } bool rtc_init( void ) { - /* libdragon currently only supports Joybus RTC! */ - if( rtc_present() != RTC_JOYBUS ) return false; - - /* Invalidate the #rtc_get cache */ - rtc_get_cache_ticks = 0; - - /* Read the calibration data from the control block */ - uint32_t calibration; - joybus_rtc_read_control( NULL, &calibration ); + if( rtc_state == RTC_STATE_INIT ) rtc_init_async(); + while( rtc_state != RTC_STATE_READY ) { /* Spinlock! */ } + return rtc_source != RTC_SOURCE_NONE; +} - /* Put the RTC into normal operating mode */ - joybus_rtc_write_control( JOYBUS_RTC_CONTROL_MODE_RUN, calibration ); - wait_ms( JOYBUS_RTC_WRITE_BLOCK_DELAY ); +void rtc_close( void ) +{ + if( rtc_state == RTC_STATE_INIT ) return; + + /* Enable newlib time integration */ + rtc_hooks_t hooks = { &rtc_get_time, &rtc_set_time }; + unhook_rtc_calls( &hooks ); + + /* Reset state and cancel any pending async callbacks */ + rtc_state = RTC_STATE_INIT; + rtc_source = RTC_SOURCE_NONE; + rtc_cache_time = RTC_CACHE_TIME_INIT; + rtc_cache_ticks = 0; + rtc_sync_result = RTC_ESUCCESS; +} - /* Enable newlib `gettimeofday` integration */ - hook_time_call( &newlib_time_hook ); +rtc_source_t rtc_get_source( void ) +{ + assert( rtc_state != RTC_STATE_INIT ); + WAIT_FOR_RTC_READY(); - return true; + return rtc_source; } -void rtc_close( void ) +int rtc_set_source( rtc_source_t source ) { - /* Disable newlib `gettimeofday` integration */ - unhook_time_call( &newlib_time_hook ); - /* Invalidate the #rtc_get cache */ - rtc_get_cache_ticks = 0; + assert( rtc_state != RTC_STATE_INIT ); + WAIT_FOR_RTC_READY(); + + if( rtc_is_source_available( source )) + { + rtc_source = source; + return rtc_resync_time(); + } + return RTC_ENOCLOCK; } -void rtc_normalize_time( rtc_time_t * rtc_time ) +bool rtc_is_source_available( rtc_source_t source ) { - /* Clamp date/time values that have static limits */ - if( rtc_time->year < 1900 ) rtc_time->year = 1900; - if( rtc_time->month > 11 ) rtc_time->month = 11; - if( rtc_time->hour > 23 ) rtc_time->hour = 23; - if( rtc_time->min > 59 ) rtc_time->min = 59; - if( rtc_time->sec > 59 ) rtc_time->sec = 59; - - /* Clamp the day of the month based on the year and month */ - uint16_t year = rtc_time->year; - uint8_t month = rtc_time->month; - uint8_t days_in_month = DAYS_IN_MONTH[month]; - bool leap_year = year % 400 == 0 || (year % 100 != 0 && year % 4 == 0); - if( month == 1 && leap_year ) days_in_month = 29; - if( rtc_time->day > days_in_month ) rtc_time->day = days_in_month; - - /* Recalculate the day of the week based on calendar date */ - month = month + 1; /* Adjust month to 1-indexed */ - if( month < 3 ) + assert( rtc_state != RTC_STATE_INIT ); + WAIT_FOR_RTC_READY(); + + switch( source ) { - month = month + 12; - year = year - 1; + case RTC_SOURCE_NONE: return true; + case RTC_SOURCE_JOYBUS: return joybus_rtc_detect(); + case RTC_SOURCE_DD: return sys_dd(); + case RTC_SOURCE_BB: return sys_bbplayer(); + default: return false; } - rtc_time->week_day = ( - rtc_time->day - + (2 * month) - + (6 * (month + 1) / 10) - + year - + (year / 4) - - (year / 100) - + (year / 400) - + 1 - ) % 7; } -bool rtc_get( rtc_time_t * rtc_time ) +rtc_range_t rtc_get_source_supported_range( rtc_source_t source ) { - /* libdragon currently only supports getting the time for Joybus RTC! */ - if( rtc_present() != RTC_JOYBUS ) return false; - - /* This should be overwritten the first time this function is called */ - static rtc_time_t cache_time = { 2000, 0, 1, 0, 0, 0, 6 }; - - /* Check if the cached time is still valid */ - int64_t now = timer_ticks(); - if( - rtc_get_cache_ticks == 0 || /* cache manually invalidated */ - (now - rtc_get_cache_ticks) > RTC_GET_CACHE_INVALIDATE_TICKS - ) + if( source == RTC_SOURCE_JOYBUS ) { - /* Update the cache */ - joybus_rtc_read_time( &cache_time ); - rtc_get_cache_ticks = now; + return (rtc_range_t){ + .min = JOYBUS_RTC_TIMESTAMP_MIN, + .max = JOYBUS_RTC_TIMESTAMP_MAX, + }; } + if( source == RTC_SOURCE_DD ) + { + return (rtc_range_t){ + .min = DD_RTC_TIMESTAMP_MIN, + .max = DD_RTC_TIMESTAMP_MAX, + }; + } + if( source == RTC_SOURCE_BB ) + { + return (rtc_range_t){ + .min = BB_RTC_TIMESTAMP_MIN, + .max = BB_RTC_TIMESTAMP_MAX, + }; + } + return (rtc_range_t){ + .min = RTC_SOFT_TIMESTAMP_MIN, + .max = RTC_SOFT_TIMESTAMP_MAX, + }; +} - memcpy( rtc_time, &cache_time, sizeof(rtc_time_t) ); +rtc_range_t rtc_get_supported_range( void ) +{ + return rtc_get_source_supported_range( rtc_source ); +} - return true; +const char *rtc_error_str( int error ) +{ + switch( error ) + { + case RTC_ESUCCESS: return "Success"; + case RTC_ENOCLOCK: return "Clock not available"; + case RTC_EBADCLOCK: return "Clock not operational"; + case RTC_EBADTIME: return "Invalid clock time"; + default: return "Unknown clock error"; + } } -bool rtc_set( rtc_time_t * write_time ) +/** @deprecated Use time(NULL) instead. */ +bool rtc_get( rtc_time_t * rtc_time ) { - /* libdragon currently only supports setting the time for Joybus RTC! */ - if( rtc_present() != RTC_JOYBUS ) return false; - - uint32_t calibration; - /* Read the calibration data from the control block */ - joybus_rtc_read_control( NULL, &calibration ); - /* Prepare the RTC to write the time, preserving the calibration data */ - joybus_rtc_write_control( JOYBUS_RTC_CONTROL_MODE_SET, calibration ); - wait_ms( JOYBUS_RTC_WRITE_BLOCK_DELAY ); - /* Check the RTC status to make sure RTC "set mode" is supported */ - if( !joybus_rtc_is_stopped() ) return false; - /* Ensure write_time is a valid RTC date/time */ - rtc_normalize_time( write_time ); - /* Write the updated time to RTC block 2 */ - joybus_rtc_write_time( write_time ); - wait_ms( JOYBUS_RTC_WRITE_BLOCK_DELAY ); - /* Put the RTC back into normal operating mode */ - joybus_rtc_write_control( JOYBUS_RTC_CONTROL_MODE_RUN, calibration ); - wait_ms( JOYBUS_RTC_WRITE_BLOCK_DELAY ); - /* Wait for the RTC to start running */ - while( joybus_rtc_is_stopped() ) { /* Spinloop */ } - wait_ms( JOYBUS_RTC_WRITE_FINISHED_DELAY ); - /* Invalidate the #rtc_get cache */ - rtc_get_cache_ticks = 0; + time_t current_time; + int result = rtc_get_time( ¤t_time ); + if( result != RTC_ESUCCESS ) return false; + struct tm * timeinfo = gmtime( ¤t_time ); + if( timeinfo == NULL ) return false; + *rtc_time = rtc_time_from_tm( timeinfo ); return true; } -bool rtc_is_writable( void ) +/** @deprecated Use settimeofday instead. */ +bool rtc_set( rtc_time_t * write_time ) { - rtc_time_t restore_time; - rtc_time_t verify_time; - /* These values are arbitrary but unlikely to be the current date/time. */ - rtc_time_t write_time = { 2003, 10, 3, 1, 2, 3, 0 }; - - rtc_get( &restore_time ); - - bool written = rtc_set( &write_time ); - if( !written ) return false; - - rtc_get( &verify_time ); - - bool verified = ( - verify_time.year == write_time.year && - verify_time.month == write_time.month && - verify_time.day == write_time.day && - verify_time.hour == write_time.hour && - verify_time.min == write_time.min - ); - - if( verified ) rtc_set( &restore_time ); - - return verified; + struct tm timeinfo = rtc_time_to_tm( write_time ); + time_t new_time = mktime( &timeinfo ); + return rtc_set_time( new_time ) == RTC_ESUCCESS; } + +/** @} */ /* rtc */ diff --git a/src/rtc_internal.c b/src/rtc_internal.c new file mode 100644 index 000000000..f2d7f2f4f --- /dev/null +++ b/src/rtc_internal.c @@ -0,0 +1,64 @@ +/** + * @file rtc_internal.c + * @author Christopher Bonhage + * @brief Real-Time Clock subsystem internal API + * @ingroup rtc + */ + +#include + +#include "rtc.h" +#include "utils.h" + +/** + * @addtogroup rtc + * @{ + */ + +int bcd_decode( uint8_t bcd ) +{ + uint8_t hi = (bcd & 0xF0) >> 4; + uint8_t lo = bcd & 0x0F; + return (hi * 10) + lo; +} + +uint8_t bcd_encode( int value ) +{ + value %= 100; + return ((value / 10) << 4) | (value % 10); +} + +struct tm rtc_time_to_tm( const rtc_time_t * rtc_time ) +{ + struct tm t = { + .tm_sec = CLAMP( rtc_time->sec, 0, 59 ), + .tm_min = CLAMP( rtc_time->min, 0, 59 ), + .tm_hour = CLAMP( rtc_time->hour, 0, 23 ), + .tm_mday = CLAMP( rtc_time->day, 1, 31 ), + .tm_mon = CLAMP( rtc_time->month, 0, 11 ), + .tm_year = CLAMP( rtc_time->year, 1900, 2099 ) - 1900, + }; + time_t timestamp = mktime( &t ); + return *gmtime( ×tamp ); +} + +rtc_time_t rtc_time_from_tm( const struct tm * time ) +{ + return (rtc_time_t){ + .year = CLAMP( time->tm_year + 1900, 1900, 2099 ), + .month = CLAMP( time->tm_mon, 0, 11 ), + .day = CLAMP( time->tm_mday, 1, 31 ), + .hour = CLAMP( time->tm_hour, 0, 23 ), + .min = CLAMP( time->tm_min, 0, 59 ), + .sec = CLAMP( time->tm_sec, 0, 59 ), + .week_day = CLAMP( time->tm_wday, 0, 6 ), + }; +} + +void rtc_normalize_time( rtc_time_t * rtc_time ) +{ + struct tm t = rtc_time_to_tm( rtc_time ); + *rtc_time = rtc_time_from_tm( &t ); +} + +/** @} */ /* rtc_internal */ diff --git a/src/rtc_internal.h b/src/rtc_internal.h new file mode 100644 index 000000000..a5de3fb04 --- /dev/null +++ b/src/rtc_internal.h @@ -0,0 +1,93 @@ +/** + * @file rtc_internal.h + * @author Christopher Bonhage + * @brief Real-Time Clock subsystem internal API + * @ingroup rtc + */ + +#ifndef __LIBDRAGON_RTC_INTERNAL_H +#define __LIBDRAGON_RTC_INTERNAL_H + +#include "rtc.h" + +/** + * @addtogroup rtc + * @{ + */ + +#ifdef __cplusplus +extern "C" { +#endif + + +/** + * @brief Decode a packed binary-coded decimal number. + * + * @param[in] bcd + * packed binary-coded decimal number + * + * @return the decoded integer + */ +int bcd_decode( uint8_t bcd ); + +/** + * @brief Encode a packed binary-coded decimal number. + * + * @param[in] value + * integer to encode + * + * @return the encoded packed binary-coded decimal number + */ +uint8_t bcd_encode( int value ); + +/** + * @brief Convert rtc_time_t into struct tm. + * + * @deprecated use `struct tm` from instead of rtc_time_t + * + * @param[in] rtc_time + * rtc_time_t to convert + * + * @return the converted struct tm + */ +struct tm rtc_time_to_tm( const rtc_time_t * rtc_time ); + +/** + * @brief Convert a struct tm into rtc_time_t. + * + * @deprecated use `struct tm` from instead of rtc_time_t + * + * @param[in] time + * struct tm to convert + * + * @return the converted rtc_time_t + */ +rtc_time_t rtc_time_from_tm( const struct tm * time ); + +/** + * @brief Calculate sane values for arbitrary time inputs. + * + * If your time inputs are already sane, nothing should change. + * This function will clamp date/time values within the expected ranges, + * including the correct day-of-month based on year/month. It will also + * recalculate the day-of-week based on the clamped year/month/day. + * + * This is useful to call while the player is adjusting the time after each + * input to ensure that the date being set always makes sense before they + * actually confirm and commit the updated date/time. The rtctest example + * demonstrates a user-interface for setting the time with live validation. + * + * @deprecated use `struct tm` from instead of rtc_time_t + * + * @param[in,out] rtc_time + * Pointer to the RTC time data structure + */ +void rtc_normalize_time( rtc_time_t * rtc_time ); + +#ifdef __cplusplus +} +#endif + +/** @} */ /* rtc */ + +#endif diff --git a/src/scratch.c b/src/scratch.c new file mode 100644 index 000000000..ad64f775d --- /dev/null +++ b/src/scratch.c @@ -0,0 +1,357 @@ +/** + * @file scratch.c + * @author Giovanni Bajo + * @brief Scratch malloc/free allocator for short-lived allocations + * @ingroup n64sys + * + * This is a small first-fit allocator built on top of sbrk_top(). + * It is intentionally simple: + * - doubly-linked physical block list; + * - arbitrary malloc/free/realloc API; + * - no split and no coalescing. + * + * Address model: + * + * low addresses high addresses + * sh_low sh_high + * | | + * v v + * +--------------------------------------------------------+ + * | [block][block][block]... (used/free mixed) | + * +--------------------------------------------------------+ + * + * The full scratch reservation is [sh_low, sh_high). New memory is reserved + * from sbrk_top(need) and prepended to the block list. + * + * A free block is reused only if it is already big enough. Since blocks are + * never split, this can create internal fragmentation; therefore each block + * stores both: + * - total block size + free flag (size_flags); + * - requested user size (requested), used for stats/realloc/debug. + * + * Freeing a block does not merge neighbors. Memory can be returned to + * sbrk_top() only when free blocks are at the head (lowest-address side) of + * the list, via trim_head(). + * + * Important invariant: all scratch blocks must remain contiguous. When growing, + * the allocator asserts that the new range is exactly adjacent to sh_low + * ("non-contiguous scratch heap"), otherwise internal assumptions break. + * + * Returned pointers are SCRATCH_ALIGN-byte aligned (16 bytes). + */ + +#include +#include +#include +#include +#include +#include "debug.h" +#include "n64sys.h" +#include "scratch.h" +#include "system_internal.h" +#include "utils.h" + +#define SCRATCH_ALIGN 16 ///< Alignment of scratch allocations +#define BLOCK_FREE ((size_t)1) ///< Free block flag +#define BLOCK_SIZE_MASK (~BLOCK_FREE) ///< Mask for the block size +#define MAGIC_USED 0x53435553 ///< Magic value for used blocks +#define MAGIC_FREE 0x53434652 ///< Magic value for free blocks + +/** @brief Block header for the scratch allocator. */ +typedef struct block_s { + size_t size_flags; ///< Total block size + free flag + size_t requested; ///< Requested user size + struct block_s *prev; ///< Previous block in the list + struct block_s *next; ///< Next block in the list + uint32_t magic; ///< Magic value to identify the block type +} block_t; + +/** @brief Lowest address reserved for scratch memory. */ +static uint8_t *sh_low; +/** @brief Highest address (exclusive) reserved for scratch memory. */ +static uint8_t *sh_high; +/** @brief Head of the physical block list. */ +static block_t *sh_head; +/** @brief Sum of requested bytes for all currently live allocations. */ +static size_t sh_live_bytes; +/** @brief Peak value reached by sh_live_bytes since boot. */ +static size_t sh_peak_bytes; +/** @brief Number of currently live allocations. */ +static size_t sh_live_blocks; + +static size_t header_size(void) { return ROUND_UP(sizeof(block_t), SCRATCH_ALIGN); } +static size_t block_size(const block_t *b) { return b->size_flags & BLOCK_SIZE_MASK; } +static bool block_is_free(const block_t *b) { return (b->size_flags & BLOCK_FREE) != 0; } +static size_t alloc_total_size(size_t req) { return ROUND_UP(header_size() + req, SCRATCH_ALIGN); } +static void *block_user_ptr(block_t *b) { return (uint8_t *)b + header_size(); } +static block_t *block_from_user_ptr(void *p) { return (block_t *)((uint8_t *)p - header_size()); } +static bool ptr_in_heap(const void *p) { return sh_low && p >= (void *)sh_low && p < (void *)sh_high; } + +static void block_mark_free(block_t *b) { + b->size_flags |= BLOCK_FREE; + b->requested = 0; + b->magic = MAGIC_FREE; +} + +static void block_mark_used(block_t *b, size_t requested) { + b->size_flags &= ~BLOCK_FREE; + b->requested = requested; + b->magic = MAGIC_USED; +} + +static block_t *find_fit(size_t need) { + for (block_t *b = sh_head; b; b = b->next) + if (block_is_free(b) && block_size(b) >= need) + return b; + return NULL; +} + +static void trim_head(void) { + while (sh_head && block_is_free(sh_head)) { + size_t n = block_size(sh_head); + sh_head = sh_head->next; + if (sh_head) sh_head->prev = NULL; + sh_low += n; + sbrk_top(-(ptrdiff_t)n); + } + if (!sh_head) { + sh_low = NULL; + sh_high = NULL; + } +} + +void *scratch_malloc(size_t size) { + if (!size) size = 1; + + size_t need = alloc_total_size(size); + block_t *b; + if (!(b = find_fit(need))) { + uint8_t *p = sbrk_top((ptrdiff_t)need); + if (p == (void *)-1) return NULL; + assertf(!sh_low || p + need == sh_low, "non-contiguous scratch heap"); + assert((((uintptr_t)p) & (SCRATCH_ALIGN - 1)) == 0); + + b = (block_t *)p; + b->size_flags = need; + b->prev = NULL; + b->next = sh_head; + block_mark_free(b); + + if (sh_head) sh_head->prev = b; + sh_head = b; + sh_low = p; + if (!sh_high) sh_high = p + need; + } + + block_mark_used(b, size); + sh_live_blocks++; + sh_live_bytes += size; + if (sh_live_bytes > sh_peak_bytes) sh_peak_bytes = sh_live_bytes; + return block_user_ptr(b); +} + +/* Release the chunk that ptr is the user-data of. Caller guarantees ptr is + * a valid direct-allocated scratch chunk's user pointer. */ +static void scratch_free_chunk(void *ptr) { + block_t *b = block_from_user_ptr(ptr); + assert(ptr_in_heap(b)); + assert(!block_is_free(b)); + assert(b->magic == MAGIC_USED); + assert(sh_live_blocks > 0); + assert(sh_live_bytes >= b->requested); + + sh_live_blocks--; + sh_live_bytes -= b->requested; + block_mark_free(b); + if (b == sh_head) trim_head(); +} + +static bool block_matches_user_ptr(block_t *b, const void *ptr) { + return ptr_in_heap(b) && !block_is_free(b) && + b->magic == MAGIC_USED && block_user_ptr(b) == ptr; +} + +static void *scratch_resolve_ptr(const void *ptr) { + if (!ptr) return NULL; + + void *cached = CachedAddr(ptr); + if (!ptr_in_heap(cached)) return NULL; + + /* Fast path: normal scratch_malloc/calloc/realloc pointers point exactly + * at the user area that follows the block header. */ + block_t *b = block_from_user_ptr(cached); + if (block_matches_user_ptr(b, cached)) + return cached; + + /* scratch_memalign returns an aligned alias inside a normal scratch block. + * The original user pointer is stored immediately before the alias. */ + void *raw = *(void **)(cached - 4); + b = block_from_user_ptr(raw); + if (!block_matches_user_ptr(b, raw)) + return NULL; + + /* Accept the alias only if it still points inside the raw block payload. */ + if ((uintptr_t)cached < (uintptr_t)raw) + return NULL; + if ((uintptr_t)cached >= (uintptr_t)raw + (block_size(b) - header_size())) + return NULL; + + return raw; +} + +void scratch_free(void *ptr) { + if (!ptr) return; + + /* scratch_free is the single-entry deallocator for any pointer + * returned by any scratch_* allocation. It transparently handles: + * - cached vs uncached views (both refer to the same physical chunk) + * - direct allocations (scratch_malloc / scratch_calloc) where the + * pointer is at user_ptr_offset within the chunk + * - aligned allocations (scratch_memalign) where the pointer is + * offset further into the chunk and the chunk's raw user pointer + * is stored in a back-slot at (ptr - sizeof(void*)) + * Callers don't need to track which allocator produced the pointer or + * which view (cached/uncached) they hold. */ + void *raw = scratch_resolve_ptr(ptr); + if (!raw) { + assertf(false, + "scratch_free: %p is neither a scratch chunk nor a memalign'd alias", ptr); + return; + } + + scratch_free_chunk(raw); +} + +void *scratch_malloc_uncached(size_t size) { + // Round the size up to a full cacheline so the uncached buffer can never + // false-share with a neighbouring allocation (mirrors malloc_uncached). + size = ROUND_UP(size, 16); + void *p = scratch_malloc(size); + if (!p) return NULL; + + // The memory may have been in the data cache from a prior cached use of + // the same block; invalidate so a future writeback can't clobber RSP/RDP + // writes. + data_cache_hit_invalidate(p, size); + return UncachedAddr(p); +} + + +void *scratch_calloc(size_t count, size_t size) { + if (count && size > ((size_t)-1) / count) return NULL; + + size_t n = count * size; + void *p = scratch_malloc(n); + if (p) memset(p, 0, n); + return p; +} + +void *scratch_realloc(void *ptr, size_t size) { + if (!ptr) return scratch_malloc(size); + if (!size) { + scratch_free(ptr); + return NULL; + } + + block_t *b = block_from_user_ptr(ptr); + assertf(ptr_in_heap(b), "pointer not allocated via scratch_malloc"); + assert(!block_is_free(b)); + assert(b->magic == MAGIC_USED); + + size_t old_requested = b->requested; + size_t capacity = block_size(b) - header_size(); + if (size <= capacity) { + sh_live_bytes = sh_live_bytes - old_requested + size; + b->requested = size; + return ptr; + } + + void *q = scratch_malloc(size); + if (!q) return NULL; + memcpy(q, ptr, old_requested < size ? old_requested : size); + scratch_free(ptr); + return q; +} + +void *scratch_memalign(size_t alignment, size_t size) { + if (alignment < SCRATCH_ALIGN) alignment = SCRATCH_ALIGN; + /* Power-of-two check matches POSIX memalign semantics. */ + assertf((alignment & (alignment - 1)) == 0, + "scratch_memalign: alignment must be a power of two"); + + /* Over-allocate by (alignment + sizeof(void*)) so we can both align the + * returned pointer and stash a back-pointer to the raw block in the + * gap before it. Guard the addition against wrap (mirrors scratch_calloc): + * a wrapped size would under-allocate and let the caller overrun the heap. */ + if (size > (size_t)-1 - alignment - sizeof(void *)) return NULL; + size_t padded = size + alignment + sizeof(void *); + void *raw = scratch_malloc(padded); + if (!raw) return NULL; + + uintptr_t aligned_addr = ((uintptr_t)raw + sizeof(void *) + (alignment - 1)) & ~(alignment - 1); + void **back_slot = (void **)(aligned_addr - sizeof(void *)); + *back_slot = raw; + return (void *)aligned_addr; +} + +void scratch_check(void) { + if (!sh_head) { + assert(!sh_low); + assert(!sh_high); + assert(sh_live_blocks == 0); + assert(sh_live_bytes == 0); + return; + } + + assert(sh_low && sh_high); + assert((uint8_t *)sh_head == sh_low); + assert(sh_low < sh_high); + + size_t live_blocks = 0; + size_t live_bytes = 0; + uint8_t *cursor = sh_low; + + for (block_t *b = sh_head; b; b = b->next) { + size_t sz = block_size(b); + assert((uint8_t *)b == cursor); + assert(((uintptr_t)b & (SCRATCH_ALIGN - 1u)) == 0); + assert((sz & (SCRATCH_ALIGN - 1u)) == 0); + assert(sz >= alloc_total_size(1)); + + if (block_is_free(b)) { + assert(b->requested == 0); + assert(b->magic == MAGIC_FREE); + } else { + assert(b->magic == MAGIC_USED); + assert(b->requested <= sz - header_size()); + live_blocks++; + live_bytes += b->requested; + } + + if (b->next) assert(b->next->prev == b); + cursor += sz; + } + + assert(cursor == sh_high); + assert(live_blocks == sh_live_blocks); + assert(live_bytes == sh_live_bytes); + assert(sh_peak_bytes >= sh_live_bytes); +} + +void scratch_get_stats(scratch_stats_t *stats) { + stats->live_bytes = sh_live_bytes; + stats->peak_bytes = sh_peak_bytes; + stats->live_blocks = sh_live_blocks; + stats->reserved_bytes = sh_low && sh_high ? (size_t)(sh_high - sh_low) : 0; +} + +bool scratch_empty(void) { + return sh_live_blocks == 0; +} + +bool scratch_owns(const void *ptr) { + if (!ptr) return false; + /* Accept either the cached or uncached view of a scratch address. */ + const void *cached = CachedAddr(ptr); + return ptr_in_heap(cached); +} diff --git a/src/sprite.c b/src/sprite.c index 7c46bed03..88972851a 100644 --- a/src/sprite.c +++ b/src/sprite.c @@ -1,7 +1,6 @@ /** * @file sprite.c * @author Giovanni Bajo - * @author Dennis Heinze */ #include "sprite.h" #include "n64sys.h" @@ -26,13 +25,17 @@ sprite_ext_t *__sprite_ext(sprite_t *sprite) if (!(sprite->flags & SPRITE_FLAGS_EXT)) return NULL; + // For SPRITE_FLAGS_NODATA, the ext header is immediately after the base sprite header. + // Otherwise, it follows the embedded pixel data. uint8_t *data = (uint8_t*)sprite->data; - tex_format_t format = sprite_get_format(sprite); - data += ROUND_UP(TEX_FORMAT_PIX2BYTES(format, sprite->width) * sprite->height, 8); + if (!(sprite->flags & SPRITE_FLAGS_NODATA)) { + tex_format_t format = sprite_get_format(sprite); + data += ROUND_UP(TEX_FORMAT_PIX2BYTES(format, sprite->width) * sprite->height, 8); + } // Access extended header sprite_ext_t *sx = (sprite_ext_t*)data; - assertf(sx->version == 4, "Invalid sprite version (%d); please regenerate your asset files", sx->version); + assertf(sx->version == SPRITE_EXT_VERSION, "Invalid sprite version (%d); please regenerate your asset files", sx->version); return sx; } @@ -44,6 +47,12 @@ bool __sprite_upgrade(sprite_t *sprite) assertf(memcmp(sprite, ASSET_MAGIC, 3) != 0, "Sprite is compressed: use sprite_load() instead of reading the file manually"); + assertf(memcmp(sprite, H264I_FILE_MAGIC, H264I_FILE_MAGIC_SIZE) != 0, + "Sprite is lossy (Level 3 H264I): call lspr3_init() before sprite_load()"); + + assertf(memcmp(sprite, BC1Q_FILE_MAGIC, BC1Q_FILE_MAGIC_SIZE) != 0, + "Sprite is lossy (Level 1 BC1Q): call lspr1_init() before sprite_load()"); + // Previously, the "format" field of the sprite structure (now renamed "flags") // was unused and always contained 0. Sprites could only be RGBA16 and RGBA32 anyway, // so only a bitdepth field could be used to understand the format. @@ -61,10 +70,46 @@ bool __sprite_upgrade(sprite_t *sprite) return false; } +static struct sprite_decoder_s *sprite_decoders = NULL; + +sprite_decoder_t *sprite_decoder_register(sprite_decodable_fn decodable, sprite_decode_fn decode) +{ + struct sprite_decoder_s *d = malloc(sizeof(struct sprite_decoder_s)); + d->decodable = decodable; + d->decode = decode; + d->next = sprite_decoders; + sprite_decoders = d; + return d; +} + +int sprite_decoder_unregister(sprite_decoder_t *decoder) +{ + struct sprite_decoder_s **d = &sprite_decoders; + while (*d) { + if (*d == decoder) { + struct sprite_decoder_s *to_free = *d; + *d = (*d)->next; + free(to_free); + return 0; + } + d = &(*d)->next; + } + return -1; +} + sprite_t *sprite_load_buf(void *buf, int sz) { - sprite_t *s = buf; + // Attempt to decode the sprite with a custom decoder using on magic header detection. + struct sprite_decoder_s *d = sprite_decoders; + while (d) { + if (d->decodable(buf, sz)) { + return d->decode(buf, sz); + } + d = d->next; + } + // No decoder matched. Try to load as a normal sprite (with possible header upgrade). assertf(sz >= sizeof(sprite_t), "Sprite buffer too small (sz=%d)", sz); + sprite_t *s = buf; __sprite_upgrade(s); (void)__sprite_ext(s); // just check if the sprite is valid (the version is checked in __sprite_ext) data_cache_hit_writeback(s, sz); @@ -76,7 +121,14 @@ sprite_t *sprite_load(const char *fn) int sz; void *buf = asset_load(fn, &sz); sprite_t *s = sprite_load_buf(buf, sz); - s->flags |= SPRITE_FLAGS_OWNEDBUFFER; + if ((void*)s != buf) { + // The decoder allocated a fresh sprite (e.g. LSPR); the source + // buffer is no longer needed. The new sprite already has + // SPRITE_FLAGS_OWNEDBUFFER set internally. + free(buf); + } else { + s->flags |= SPRITE_FLAGS_OWNEDBUFFER; + } return s; } @@ -95,7 +147,12 @@ void sprite_free(sprite_t *s) } surface_t sprite_get_pixels(sprite_t *sprite) { - return surface_make_linear(sprite->data, sprite_get_format(sprite), + void *pixels = sprite->data; + if (sprite->flags & SPRITE_FLAGS_NODATA) { + sprite_ext_t *sx = __sprite_ext(sprite); + pixels = (void*)((uint8_t*)sprite + sx->data_ptr); + } + return surface_make_linear(pixels, sprite_get_format(sprite), sprite->width, sprite->height); } @@ -168,6 +225,11 @@ uint16_t* sprite_get_palette(sprite_t *sprite) { return (void*)sprite + sx->pal_file_pos; } +int sprite_get_palette_used_colors(sprite_t *sprite) { + sprite_ext_t *sx = __sprite_ext(sprite); + return sx->pal_used_colors ? sx->pal_used_colors : 256; +} + surface_t sprite_get_tile(sprite_t *sprite, int h, int v) { static int tile_width = 0, tile_height = 0; @@ -229,4 +291,12 @@ bool sprite_fits_tmem(sprite_t *sprite) return (sx->flags & SPRITE_FLAG_FITS_TMEM) != 0; } +bool sprite_is_shq(sprite_t *sprite) +{ + sprite_ext_t *sx = __sprite_ext(sprite); + if (!sx) + return false; + return (sx->flags & SPRITE_FLAG_SHQ) != 0; +} + extern inline tex_format_t sprite_get_format(sprite_t *sprite); diff --git a/src/sprite_internal.h b/src/sprite_internal.h index 2945a66a0..6f18855e5 100644 --- a/src/sprite_internal.h +++ b/src/sprite_internal.h @@ -1,6 +1,7 @@ /** * @file sprite_internal.h * @author Giovanni Bajo + * @author Christopher Bonhage */ #ifndef __LIBDRAGON_SPRITE_INTERNAL_H #define __LIBDRAGON_SPRITE_INTERNAL_H @@ -8,10 +9,38 @@ #include #include +#define SPRITE_EXT_VERSION 6 ///< Current version of the sprite extended structure + #define SPRITE_FLAG_NUMLODS 0x0007 ///< Number of LODs, including detail texture if any (0 = no LODs) #define SPRITE_FLAG_HAS_TEXPARMS 0x0008 ///< Sprite contains texture parameters #define SPRITE_FLAG_HAS_DETAIL 0x0010 ///< Sprite contains detail texture #define SPRITE_FLAG_FITS_TMEM 0x0020 ///< Set if the sprite does fit TMEM without splitting +#define SPRITE_FLAG_SHQ 0x0040 ///< Sprite is in special SHQ format (2 mipmap levels with subtractive blending) + +/** + * @brief File-start signature of a Lossy-sprite Level 3 (H264I) file (8 bytes). + * + * The leading four zero bytes are a safety pad: if an H264I file reaches + * #sprite_load without lspr3_init() having registered the decoder, + * the buffer is reinterpreted as a #sprite_t — the pad forces width=height=0 + * so the situation is detectable and a clear assertion can fire instead of + * silently producing a corrupt sprite. + */ +#define H264I_FILE_MAGIC "\0\0\0\0H264" +/** @brief Size in bytes of #H264I_FILE_MAGIC (drops the trailing NUL). */ +#define H264I_FILE_MAGIC_SIZE (sizeof(H264I_FILE_MAGIC) - 1) + +/** + * @brief File-start signature of a Lossy-sprite Level 1 (BC1Q) file (8 bytes). + * + * Same safety-pad rationale as #H264I_FILE_MAGIC: the leading four zero bytes + * force width=height=0 if a BC1Q file is reinterpreted as a #sprite_t when + * lspr1_init() has not registered the decoder, so the situation produces + * a clear assertion instead of a silently corrupt sprite. + */ +#define BC1Q_FILE_MAGIC "\0\0\0\0BC1Q" +/** @brief Size in bytes of #BC1Q_FILE_MAGIC (drops the trailing NUL). */ +#define BC1Q_FILE_MAGIC_SIZE (sizeof(BC1Q_FILE_MAGIC) - 1) /** * @brief Internal structure used as additional sprite header @@ -24,7 +53,7 @@ */ typedef struct sprite_ext_s { uint16_t size; ///< Size of the structure itself (for forward compatibility) - uint16_t version; ///< Version of the structure (currently 1) + uint16_t version; ///< Version of the structure (#SPRITE_EXT_VERSION) uint32_t pal_file_pos; ///< Position of the palette in the file /// Information on LODs struct sprite_lod_s { @@ -34,7 +63,8 @@ typedef struct sprite_ext_s { } lods[7]; ///< Information on the available LODs (if detail is present, it's always at position 6) struct { uint16_t flags; ///< Generic Flags for the sprite - uint16_t padding; ///< Padding + uint8_t pal_used_colors; ///< Number of colors actually used in palette + uint8_t padding; ///< Padding }; /// @brief RDP texture parameters struct texparms_s { @@ -53,11 +83,35 @@ typedef struct sprite_ext_s { bool use_main_texture; ///< True if the detail texture is the same as the LOD0 of the main texture uint8_t padding[3]; ///< Padding } detail; ///< Detail texture parameters + + uint32_t data_ptr; ///< Optional offset (from sprite base) to pixel data } sprite_ext_t; -_Static_assert(sizeof(sprite_ext_t) == 124, "invalid sizeof(sprite_ext_t)"); +_Static_assert(sizeof(sprite_ext_t) == 128, "invalid sizeof(sprite_ext_t)"); -/** @brief Convert a sprite from the old format with implicit texture format */ +/** @brief Convert a sprite from the old format with implicit texture format */ bool __sprite_upgrade(sprite_t *sprite); +/** @brief Access the sprite extended structure, or NULL if it does not exist. */ +sprite_ext_t *__sprite_ext(sprite_t *sprite); + +/** @brief Function pointer type for a sprite decodable test function */ +typedef bool (*sprite_decodable_fn)(const void *buf, int sz); + +/** @brief Function pointer type for a sprite decoder function */ +typedef sprite_t *(*sprite_decode_fn)(const void *buf, int sz); + +/** @brief Internal structure for registered sprite decoders */ +typedef struct sprite_decoder_s { + sprite_decodable_fn decodable; + sprite_decode_fn decode; + struct sprite_decoder_s *next; +} sprite_decoder_t; + +/** @brief Register a sprite decoder for a specific magic string. */ +sprite_decoder_t *sprite_decoder_register(sprite_decodable_fn decodable, sprite_decode_fn decode); + +/** @brief Unregister the sprite decoder for a specific magic string. */ +int sprite_decoder_unregister(sprite_decoder_t *decoder); + #endif diff --git a/src/sram.c b/src/sram.c new file mode 100644 index 000000000..cef0d8ceb --- /dev/null +++ b/src/sram.c @@ -0,0 +1,101 @@ +/** + * @file sram.c + * @author thekovic + * @brief SRAM access functions for N64 cartridges + * + */ + +#include "sram.h" +#include "debug.h" +#include "dma.h" +#include "interrupt.h" +#include "n64sys.h" +#include + +/// @cond +#define PI_BSD_DOM2_LAT ((volatile uint32_t*) 0xA4600024) +#define PI_BSD_DOM2_PWD ((volatile uint32_t*) 0xA4600028) +#define PI_BSD_DOM2_PGS ((volatile uint32_t*) 0xA460002C) +#define PI_BSD_DOM2_RLS ((volatile uint32_t*) 0xA4600030) + +// TODO: Replace fixed size with detection of other SRAM sizes. +#define SRAM_SIZE 0x00008000 ///< Size of standard SRAM in commercial cartridges (32 KiB). +/// @endcond + +bool __sram_inited = false; ///< True if sram_init() was called + +void sram_init(void) +{ + if (__sram_inited) + return; + + // Configure PI DOM2 registers to enable access to SRAM + disable_interrupts(); + *PI_BSD_DOM2_LAT = 0x5; + *PI_BSD_DOM2_PWD = 0xc; + *PI_BSD_DOM2_PGS = 0xd; + *PI_BSD_DOM2_RLS = 0x2; + enable_interrupts(); + + __sram_inited = true; +} + +int sram_detect(void) +{ + assertf(__sram_inited, "sram accessed, but sram_init() hasn't been called yet"); + + uint32_t test_pattern = 0x12345678; + + // Test read, save the existing data to restore it later. + uint32_t data_old = io_read(SRAM_ADDRESS); + // Write a test pattern to SRAM and check it. + io_write(SRAM_ADDRESS, test_pattern); + uint32_t data_new = io_read(SRAM_ADDRESS); + bool is_detected = (data_new == test_pattern); + // Restore the old data. + io_write(SRAM_ADDRESS, data_old); + + return (is_detected) ? SRAM_SIZE : 0; +} + +int sram_read(void* dst, size_t offset, size_t len) +{ + assertf(__sram_inited, "sram accessed, but sram_init() hasn't been called yet"); + + // Check if the read operation is within bounds. + if (offset + len > SRAM_SIZE) + { + errno = EINVAL; + return -1; + } + + pi_addr_t sram_address = SRAM_ADDRESS + offset; + // Invalidate (writing back first) so the DMA's data is what the CPU reads + // afterwards, and no stale dirty line is flushed over it mid-transfer. + data_cache_hit_writeback_invalidate(dst, len); + dma_read_raw_async(dst, sram_address, len); + dma_wait(); + + return (int) len; +} + +int sram_write(const void* src, size_t offset, size_t len) +{ + assertf(__sram_inited, "sram accessed, but sram_init() hasn't been called yet"); + + // Check if the write operation is within bounds. + if (offset + len > SRAM_SIZE) + { + errno = EINVAL; + return -1; + } + + pi_addr_t sram_address = SRAM_ADDRESS + offset; + // Write back the source so the DMA reads the CPU's latest data from RDRAM + // rather than a stale copy still sitting in the cache. + data_cache_hit_writeback(src, len); + dma_write_raw_async(src, sram_address, len); + dma_wait(); + + return (int) len; +} diff --git a/src/string_hash.c b/src/string_hash.c new file mode 100644 index 000000000..81993d0bb --- /dev/null +++ b/src/string_hash.c @@ -0,0 +1,18 @@ +/** + * @file string_hash.c + * @author Giovanni Bajo + * @brief Fast string hashing function with compile-time support + */ + +#include "string_hash.h" + +/** @brief Out of line string hash function. */ +uint32_t __string_hash(const char *str) +{ + uint32_t hash = 5381; const uint32_t prime = 33; + + while (*str) { + hash = hash * prime + (*str++); + } + return hash; +} diff --git a/src/surface.c b/src/surface.c index 7ce9918c7..d92c3e452 100644 --- a/src/surface.c +++ b/src/surface.c @@ -1,5 +1,6 @@ /** * @file surface.c + * @author Dennis Heinze * @author Giovanni Bajo * @brief Surface buffers used to draw images * @ingroup graphics diff --git a/src/symtable.c b/src/symtable.c new file mode 100644 index 000000000..aaaa61689 --- /dev/null +++ b/src/symtable.c @@ -0,0 +1,710 @@ +/** + * @file symtable.c + * @author Giovanni Bajo + * @brief SYMT symbol table access + * @ingroup backtrace + * + * # N64 SYMT Format v5 + * + * This document describes version 5 of the Symbol Table format (SYMT) used by Libdragon for + * runtime symbolization and backtracing. + * + * The format is designed to be extremely compact in ROM while requiring a minimal fixed amount of + * RAM (e.g. 512-1024 bytes) to be queried at runtime. It achieves this by using a combination of + * delta encoding, variable-length integers (VarInt), and dictionary-based compression with Front + * Coding and Canonical Huffman Coding. + * + * ## File Structure + * + * The file starts with a global header followed by several variable-length sections. All offsets + * are relative to the beginning of the file. Everything is assumed to be big-endian unless + * specified. + * + * ### Header + * + * ```c + * typedef struct { + * char magic[4]; // "SYMT" + * uint32_t version; // 5 + * uint32_t num_symbols; // Total number of symbols + * uint32_t num_chunks; // Total number of compressed symbol chunks + * + * // Offsets to sections + * uint32_t chunk_idx_off; // Chunk Index + * uint32_t file_tab_off; // File Block Table + * uint32_t func_tab_off; // Func Block Table + * uint32_t huff_tab_off; // Global Huffman Table + * uint32_t file_blob_off; // File String Blob + * uint32_t func_blob_off; // Func String Blob + * uint32_t stream_off; // Compressed Symbol Stream (asset compression level 3) + * uint32_t max_chunk_margin; // Max in-place decompression safety margin + * + * // Size of sections (useful for bounds checking) + * uint32_t num_files; // Total number of file strings + * uint32_t num_funcs; // Total Number of func strings + * uint32_t num_file_blocks; // Number of file blocks + * uint32_t num_func_blocks; // Number of func blocks + * uint32_t huff_tab_size; // Size of Huffman table in bytes + * uint32_t file_blob_size; // Size of file blob in bytes + * uint32_t func_blob_size; // Size of func blob in bytes + * uint32_t stream_size; // Size of compressed stream in bytes + * } symtable_header_t; + * ``` + * + * ## Sections + * + * ### 1. Chunk Index (`chunk_idx_off`) + * + * A sparse index used to perform binary search on the symbols, using their address. This is the + * main entrypoint to lookup a symbol given its address. Symbols are divided into "Chunks" which are + * individually compressed, so the Chunk Index actually identifies the chunk in which a symbol is + * encoded. + * + * The structure is an array of `num_chunks` entries: + * + * ```c + * typedef struct { + * uint32_t start_addr; // The memory address of the first symbol in this chunk + * uint32_t stream_off; // The byte offset of this chunk within the Symbol Stream section + * } chunk_index_entry_t; + * ``` + * + * Since the index is sorted by memory address, it can be queried via bisection, to identify the + * chunk in which the symbol is contained. Then the chunk must be parsed and decompressed linearly + * until the symbol is found. + * + * ### 2. String Indexes (`file_tab_off`, `func_tab_off`) + * + * The SYMT file stores two groups of strings: file names and function names. Within each symbol, + * the filename and the function name are referenced by an integer ID. + * + * The string indexes are sparse indexes used to locate strings in the compressed string blobs. + * Strings are organized in compressed blocks of variable size. To find a string with ID `N`, we + * perform a binary search on this index to find the block containing `N`. + * + * The structure is an array of `num_file_blocks` (or `num_func_blocks`) entries: + * + * ```c + * typedef struct { + * uint32_t start_idx; // The index of the first string in this block + * uint32_t blob_off; // The byte offset of this block within the respective Blob section + * } string_index_entry_t; + * ``` + * + * ### 3. Global Huffman Table (`huff_tab_off`) + * + * Contains the shared Canonical Huffman Table used to decompress string suffixes in both the File + * and Func blobs. The table allows decoding individual characters from the compressed bitstream. + * + * **Format:** + * * `MaxLen` (1 byte): Maximum codeword length. + * * `Padding` (1 byte): Zeros. + * * `LUT` (128 bytes): 64-entry Lookup Table for fast decoding of codes <= 6 bits. Each entry is + * `Symbol` (1 byte) + `Len` (1 byte). + * * `FirstCode` (array of uint16): First canonical code for each length (7..MaxLen). + * * `FirstSymbol` (array of uint16): Index of the first symbol for each length (7..MaxLen), + * relative to the start of the symbols array, plus an extra terminal entry equal to the total + * number of stored symbols (len >= 6). + * * `Symbols` (array of uint8): The alphabet symbols with code length > 6, sorted by code length + * then lexicographically. Codes with length <= 6 are fully covered by the LUT and are not + * stored here. + * + * **Notes:** + * * Arrays `FirstCode` and `FirstSymbol` only exist if `MaxLen >= 7`. The size of these arrays is + * `MaxLen - 6`. + * * The `num_symbols` canonical array is omitted. The decoder derives the number of symbols for + * each length from `FirstSymbol` (or equivalently from successive `FirstCode` values) and from + * the total size of the `Symbols` array. + * * The total size of the Huffman table is stored in the header field `huff_tab_size` and is used + * at runtime to know how many bytes of the trailing `Symbols` array are present after the + * canonical arrays. + * + * ### 4. String Blobs (`file_blob_off`, `func_blob_off`) + * + * Contains the actual string data, compressed using **Front Coding** combined with **Huffman + * Coding**. The data is divided into blocks pointed to by the String Index. Each block is + * independently compressed and fits into a fixed-size runtime buffer (e.g. 512 bytes). + * + * **Block Format:** Strings in the block are stored as delta from the previous string. The common + * prefix length is stored as a **delta** relative to the previous string's common prefix length, + * encoded using **Signed Exp-Golomb (k=0)**. The suffix itself is compressed using the Global + * Huffman Table, including the null terminator (`\0`). + * + * ``` + * [PrefixLenDelta] [HuffmanEncodedSuffix...] + * ``` + * + * * `PrefixLenDelta` (Exp-Golomb): The difference between the current string's common prefix length + * and the *previous* string's common prefix length. + * * `Delta = CurrentCommon - PrevCommon`. + * * For the first string in a block, `PrevCommon` is 0. + * * The delta is mapped to an unsigned integer using ZigZag encoding (`0->0`, `-1->1`, + * `1->2`, `-2->3`...) and then written using Exp-Golomb k=0 codes (interleaved in the + * bitstream). + * * `HuffmanEncodedSuffix`: The suffix characters encoded using the global Huffman table. This is a + * bitstream. The stream continues until the Huffman code for `\0` is encountered. + * + * **Note:** Blocks are padded to 2-byte alignment at the end. + * + * ### 5. Compressed Symbol Stream (`stream_off`) + * + * Contains chunks compressed with asset compression level 3 (currently Shrinkler). + * This is a continuous stream of bytes divided into Chunks + * (as defined in the Chunk Index). Each Chunk can be decompressed independently. + * + * **Chunk Layout (after asset compression level 3 decompression):** + * * `FirstFuncOff` (VarInt): Offset of the first symbol in the chunk relative to the start of its + * function. If 0, the chunk starts exactly at a function boundary or the function base is unknown. + * * Opcodes sequence (see below). + * * No explicit end-of-chunk marker: the chunk ends at the decompressed chunk length. + * + * Inside a Chunk, symbols are stored sequentially using **Delta Encoding**. Address deltas are + * relative to the previous symbol address, while file/function/line deltas are relative to a state + * selected by symbol class (`inline` vs `non-inline`). The state consists of: + * * `CurrentAddress` (Initialized to `chunk.start_addr`) + * * `FileID[2]` (Initialized to 0, index 0=`non-inline`, 1=`inline`) + * * `FuncID[2]` (Initialized to 0, index 0=`non-inline`, 1=`inline`) + * * `Line[2]` (Initialized to 0, index 0=`non-inline`, 1=`inline`) + * + * **Symbol Encoding:** Each symbol starts with a 1-byte **Opcode**: + * + * | Bit | Name | Description | + * | :--- | :--- | :--- | + * | 7 | `New File` | If 1, a signed VarInt follows specifying the delta for `FileID`. | + * | 6 | `New Func` | If 1, a signed VarInt follows specifying the delta for `FuncID`. | + * | 5 | `New Line` | If 1, a signed VarInt follows specifying the delta for `Line`. | + * | 4 | `Is Func` | If 1, this symbol marks the start of a function. | + * | 3 | `Is Inline` | If 1, this symbol is an inlined function instance. | + * | 2-0 | `Addr Delta` | Addr inc.
`0`: 0.
`1..6`: (`V*4`).
`7`: VarInt, Delta = (`V+7`)*4. | + * + * ## Data Types + * + * ### VarInt (Unsigned) + * Variable-length integer encoding (LEB128-style). + * * Read byte. + * * Take low 7 bits. + * * If high bit (0x80) is set, shift and read next byte. + * + * ### Signed VarInt (ZigZag) + * Maps signed integers to unsigned values to efficiently encode small negative numbers: + * * `0` -> `0` + * * `-1` -> `1` + * * `1` -> `2` + * * `-2` -> `3` + * * ... + * * Encoded/Decoded as standard VarInt. + * + * ## Runtime Algorithms + * + * These algorithms are designed to work with a small fixed-size buffer (e.g., 1KB) without dynamic + * allocation. + * + * ### Algorithm 1: Symbol Lookup + * + * **Goal:** Find the symbol containing address `SearchAddr`. + * + * 1. **Binary Search Chunk Index:** + * * Load `chunk_index_t` entries from ROM. + * * Find the chunk `C` such that `C.start_addr <= SearchAddr < (C+1).start_addr`. + * 2. **Load Chunk:** + * * DMA the chunk data from `stream_off + C.stream_off` into the RAM buffer. + * 3. **Scan Stream:** + * * Initialize state: `CurrAddr = C.start_addr`, + * `File[0..1]=0`, `Func[0..1]=0`, `Line[0..1]=0`. + * * Loop through opcodes in the buffer: + * * Decode Address Delta. Update `CurrAddr`. + * * If `CurrAddr > SearchAddr`: Stop. The *previous* valid symbol is the match. + * * Select class state with `sid = IsInline ? 1 : 0`. + * * Update `File[sid]`, `Func[sid]`, `Line[sid]` based on flags and VarInts. + * * Keep track of the "Best Match" seen so far. + * + * ### Algorithm 2: String Fetch + * + * **Goal:** Retrieve string with ID `TargetID` from a Blob. + * + * 1. **Binary Search String Index:** + * * Load `string_index_entry_t` entries from ROM. + * * Find the entry `E` such that `E.start_idx <= TargetID < (E+1).start_idx`. + * 2. **Load Huffman Table:** + * * (Optionally cached or loaded once) Load global Huffman table from `huff_tab_off`. + * 3. **Load Block:** + * * DMA the block from `BlobOffset + E.blob_off` into the RAM buffer. + * 4. **Front Coding + Huffman Decode:** + * * Initialize `CurrentString` (empty). + * * Initialize `PrevCommon` = 0. + * * Calculate `TargetInBlock = TargetID - E.start_idx`. + * * Initialize bit_reader_t on the data. + * * Loop `i` from 0 to `TargetInBlock`: + * * Read `PrefixLenDelta` (Exp-Golomb, ZigZag). + * * `CurrentCommon = PrevCommon + PrefixLenDelta`. + * * `PrevCommon = CurrentCommon`. + * * Truncate `CurrentString` to `CurrentCommon`. + * * **Decode Suffix:** + * * Loop until `\0` decoded: + * * Read next Huffman symbol using Table/LUT. + * * If not `\0`, append char to `CurrentString`. + * * `CurrentString` is now the result. + * + * ### Algorithm 3: Backtrace Symbolization + * + * 1. Call **Symbol Lookup** with the PC address. + * * Result: `SymbolAddr`, `FileID`, `FuncID`, `Line`, `Offset`. + * 2. If `FuncID` is valid: + * * Call **String Fetch** with `FuncID` to get the function name. + * 3. If `FileID` is valid: + * * Call **String Fetch** with `FileID` to get the filename. + */ + +#include +#include +#include +#include +#include +#include +#include "symtable_internal.h" +#include "asset_internal.h" +#include "n64sys.h" +#include "dma.h" +#include "rompak_internal.h" +#include "dlfcn_internal.h" +#include "debug.h" +#include "utils.h" +#include "compress/shrinkler_dec_internal.h" + +/** @brief Buffer size for decompression */ +#define MAX_BUFFER_SIZE 512 + +/** @brief Maximum code length for Huffman decoding */ +#define HUFF_MAX_CODE_LEN 16 + +/** @brief Exception handler (see inthandler.S) */ +extern uint32_t inthandler[]; +/** @brief End of exception handler (see inthandler.S) */ +extern uint32_t inthandler_end[]; + +/** @brief Start of main executable text section */ +extern uint32_t __text_start[]; +/** @brief End of main executable text section */ +extern uint32_t __text_end[]; + +/** @brief Serialized SYMT header size (runtime-only fields excluded). */ +#define SYMT_FILE_HEADER_SIZE ((int)offsetof(symtable_header_t, rom_offset)) + +/** + * Read a 32-bit word from SYMT ROM, tolerating unaligned PI addresses. + * SYMT files for DSOs only have 2-byte aligned offsets. + */ +static uint32_t symt_read_u32(pi_addr_t pi_address) +{ + uint32_t base = pi_address & ~3; + uint32_t off = pi_address & 3; + uint32_t w0 = io_read(base); + if (off == 0) return w0; + uint32_t w1 = io_read(base + 4); + return (w0 << 16) | (w1 >> 16); +} + +/** @brief Placeholder used in frames where symbols are not available */ +const char *UNKNOWN_SYMBOL = "???"; + +/** @brief Module resolver (from backtrace.c or dlfcn.c) */ +extern module_lookup_func __bt_lookup_module; + +/** @brief Lookup table for the Huffman decoder */ +typedef struct { + uint8_t symbol; ///< Symbol value for this entry + uint8_t len; ///< Length of the symbol in bits +} huff_lut_t; + +/** @brief Huffman decoder */ +typedef struct { + huff_lut_t *lut; ///< Lookup table for quick lookup (64 symbols) + uint16_t *first_code; ///< First code for each length (7..max_len) + uint16_t *first_symbol; ///< First symbol for each length (7..max_len) + uint8_t *symbols; ///< Symbols array (length > 6) + int max_len; ///< Maximum length of the symbols + int len_count; ///< Number of lengths (7..max_len) +} huff_decoder_t; + +/** @brief Bit reader for the Huffman bitstream */ +typedef struct { + uint8_t *buf; ///< Pointer to the buffer + uint32_t cache; ///< Cache for the bits + int bits_in_cache; ///< Number of bits in the cache +} bit_reader_t; + +/** @brief Check if addr is inside main executable text section */ +static bool is_main_exe_text_address(uint32_t addr) +{ + return addr >= (uint32_t)__text_start && addr < (uint32_t)__text_end; +} + +symtable_header_t symt_open(void *addr) { + uint32_t sym_rom = 0; + uint32_t addr_base = 0; + if(is_main_exe_text_address((uint32_t)addr)) { + //Open SYMT from rompak + static uint32_t mainexe_symt = 0xFFFFFFFF; + if (mainexe_symt == 0xFFFFFFFF) { + mainexe_symt = rompak_search_ext(".sym", NULL); + if (!mainexe_symt) + debugf("backtrace: no symbol table found in the rompak\n"); + } + sym_rom = mainexe_symt; + } else { + dl_module_t *module = NULL; + if(__bt_lookup_module) { + module = __bt_lookup_module(addr); + } + if(module && module->sym_romofs != 0) { + //Read module SYMT (addresses are ELF-relative; rebase via prog_base) + sym_rom = module->sym_romofs; + addr_base = (uint32_t)module->prog_base; + } else { + sym_rom = 0; + } + } + + if (!sym_rom) { + return (symtable_header_t){0}; + } + + symtable_header_t symt_header; + data_cache_hit_writeback_invalidate(&symt_header, sizeof(symt_header)); + dma_read(&symt_header, sym_rom, SYMT_FILE_HEADER_SIZE); + + if (symt_header.head[0] != 'S' || symt_header.head[1] != 'Y' || symt_header.head[2] != 'M' || symt_header.head[3] != 'T') { + debugf("backtrace: invalid symbol table found at 0x%08lx\n", sym_rom); + return (symtable_header_t){0}; + } + if (symt_header.version != 5) { + debugf("backtrace: unsupported symbol table version %ld -- please update your n64sym tool\n", symt_header.version); + return (symtable_header_t){0}; + } + + symt_header.rom_offset = sym_rom; + symt_header.addr_base = addr_base; + return symt_header; +} + +int symt_find_symbol(symtable_header_t *symt, uint32_t addr, symtable_entry_t *entry, int max_entries) +{ + // Convert runtime PC to ELF-relative address (no-op for main executable) + addr -= symt->addr_base; + + // Binary search in the chunk index + int min = 0; + int max = symt->num_chunks - 1; + + while (min < max) { + int mid = (min + max + 1) / 2; + int chunk_start_addr = symt_read_u32(symt->rom_offset + symt->chunk_idx_off + mid * 8); + if (addr < chunk_start_addr) + max = mid - 1; + else + min = mid; + } + + // Read the chunk entry for the found chunk + int chunk_start_addr = symt_read_u32(symt->rom_offset + symt->chunk_idx_off + min * 8 + 0); + uint32_t chunk_stream_off = symt_read_u32(symt->rom_offset + symt->chunk_idx_off + min * 8 + 4); + uint32_t next_chunk_stream_off = (min + 1 < symt->num_chunks) + ? symt_read_u32(symt->rom_offset + symt->chunk_idx_off + (min + 1) * 8 + 4) + : symt->stream_size; + if (addr < chunk_start_addr) + return false; // Should not happen if address is valid code + uint32_t cmp_size = next_chunk_stream_off - chunk_stream_off; + + // Read + decompress the chunk in-place (same model as asset.c) + uint32_t stream_addr = symt->rom_offset + symt->stream_off + chunk_stream_off; + int cmp_pos = 0; + int work_size = asset_buf_size(MAX_BUFFER_SIZE, cmp_size, symt->max_chunk_margin, &cmp_pos); + uint8_t alignas(16) chunk_buf[work_size]; + data_cache_hit_writeback_invalidate(chunk_buf, work_size); + dma_read(chunk_buf + cmp_pos, stream_addr, cmp_size); + int dec_n = decompress_shrinkler_full_inplace(chunk_buf + cmp_pos, cmp_size, chunk_buf, MAX_BUFFER_SIZE); + + const uint8_t *ptr = chunk_buf; + const uint8_t *end = chunk_buf + dec_n; + // First field: function offset of the first symbol in chunk (VarInt) + uint32_t chunk_func_off = __read_varint_u64(&ptr); + + // Iterate through symbols in the chunk + uint32_t cur_addr = chunk_start_addr; + int cur_file[2] = {0, 0}; // [0]=non-inline, [1]=inline + int cur_func[2] = {0, 0}; // [0]=non-inline, [1]=inline + int cur_line[2] = {0, 0}; // [0]=non-inline, [1]=inline + + uint32_t last_func_addr = chunk_func_off ? (chunk_start_addr - chunk_func_off) : 0; + int found = 0; + + while (ptr < end) { + uint8_t op = *ptr++; + + // Decode deltas + int delta_file = (op & 0x80) ? __read_varint_s64(&ptr) : 0; + int delta_func = (op & 0x40) ? __read_varint_s64(&ptr) : 0; + int delta_line = (op & 0x20) ? __read_varint_s64(&ptr) : 0; + + // Decode address delta + uint32_t delta_addr = 0; + if ((op & 0x07) == 7) { + delta_addr = (__read_varint_u64(&ptr) + 7) * 4; + } else { + delta_addr = (op & 0x07) * 4; + } + + bool is_func = (op & 0x10); + bool is_inline = (op & 0x08); + int sid = is_inline ? 1 : 0; + + cur_file[sid] += delta_file; + cur_func[sid] += delta_func; + cur_line[sid] += delta_line; + uint32_t sym_addr = cur_addr + delta_addr; + + if (is_func) last_func_addr = sym_addr; + + // If this is the function start, record it. In case the exact symbol + // is not found, ee will return this approximation (function start + offset) + if (sym_addr < addr && is_func) { + last_func_addr = sym_addr; + entry[0].func_sidx = cur_func[0]; + entry[0].file_sidx = cur_file[0]; + entry[0].line = 0; + entry[0].func_off = addr - last_func_addr; + entry[0].is_inline = 0; + found = 1; + } + + // Exact match: this is the symbol we were looking for + if (sym_addr == addr) { + if (entry[0].line == 0) found = 0; // Overwrite function-only entry + if (found < max_entries) { + entry[found].func_sidx = cur_func[sid]; + entry[found].file_sidx = cur_file[sid]; + entry[found].line = cur_line[sid]; + entry[found].is_inline = is_inline; + if (last_func_addr) { + entry[found].func_off = addr - last_func_addr; + } else { + entry[found].func_off = 0; + } + } + found++; + // If this is an inline symbol, keep searching for the parent function + if (!is_inline) + break; + } + + // If we are past the address, we return the last found function symbol + if (sym_addr > addr) { + break; + } + + // Keep address for next iteration + cur_addr = sym_addr; + } + + return found; +} + +static void br_fill(bit_reader_t *br) { + while (br->bits_in_cache <= 24) { + br->cache |= (uint32_t)(*br->buf++) << (24 - br->bits_in_cache); + br->bits_in_cache += 8; + } +} + +static void br_init(bit_reader_t *br, uint8_t *buf) { + br->buf = buf; + br->cache = 0; + br->bits_in_cache = 0; + br_fill(br); +} + +static int br_read_bits(bit_reader_t *br, int n) { + if (br->bits_in_cache < n) br_fill(br); + uint32_t val = (br->cache >> (32 - n)); + br->cache <<= n; + br->bits_in_cache -= n; + return val; +} + +static int br_peek_bits(bit_reader_t *br, int n) { + if (br->bits_in_cache < n) br_fill(br); + return (br->cache >> (32 - n)); +} + +static void br_skip_bits(bit_reader_t *br, int n) { + if (br->bits_in_cache < n) br_fill(br); + br->cache <<= n; + br->bits_in_cache -= n; +} + +static int32_t read_exp_golomb_signed(bit_reader_t *br) { + int zero_bits = 0; + while (br_peek_bits(br, 1) == 0) { + br_skip_bits(br, 1); + zero_bits++; + } + + uint32_t val = br_read_bits(br, zero_bits + 1) - 1; + return (val >> 1) ^ -(val & 1); +} + +static void huff_decoder_init(huff_decoder_t *dec, uint8_t *table_buf, uint32_t table_size) { + // Layout: MaxLen(1), Padding(1), LUT(128), CanonicalArrays, Symbols + dec->max_len = table_buf[0]; + dec->len_count = dec->max_len >= 7 ? dec->max_len - 6 : 0; + uint8_t *ptr = table_buf + 2; + dec->lut = (huff_lut_t*)ptr; ptr += 64 * 2; + + if (dec->len_count > 0) { + dec->first_code = (uint16_t*)ptr; ptr += dec->len_count * 2; + // first_symbol has one extra terminal entry containing total symbols + dec->first_symbol = (uint16_t*)ptr; ptr += (dec->len_count + 1) * 2; + } else { + dec->first_code = NULL; + dec->first_symbol = NULL; + } + dec->symbols = ptr; +} + +static int huff_decode_symbol(huff_decoder_t *dec, bit_reader_t *br) { + // Peek 16 bits (enough for max code length) + int peek16 = br_peek_bits(br, 16); + + // Try LUT first (6 bits) + int peek6 = peek16 >> 10; + int len = dec->lut[peek6].len; + if (len > 0) { + br_skip_bits(br, len); + return dec->lut[peek6].symbol; + } + + // Canonical fallback + for (int idx = 0; idx < dec->len_count; idx++) { + len = idx + 7; + int code = peek16 >> (16 - len); + + uint16_t fc = dec->first_code[idx]; + uint16_t fs = dec->first_symbol[idx]; + uint16_t count = dec->first_symbol[idx + 1] - fs; + + if (code < fc + count) { + br_skip_bits(br, len); + return dec->symbols[fs + (code - fc)]; + } + } + + return -1; +} + +/** + * @brief Fetch a string from the string table + * + * @param symt SYMT file + * @param idx Index of the string in the string table (index, not offset) + * @param buf Destination buffer + * @param size Size of the destination buffer + * @param is_func True if fetching a function name, false for file name + * @return Fetched string within the destination buffer + */ +static char* symt_get_string(symtable_header_t *symt, int idx, char *buf, int size, bool is_func) +{ + if (idx < 0) { + snprintf(buf, size, "%s", UNKNOWN_SYMBOL); + return buf; + } + + uint32_t tab_off = is_func ? symt->func_tab_off : symt->file_tab_off; + uint32_t blob_off = is_func ? symt->func_blob_off : symt->file_blob_off; + int num_blocks = is_func ? symt->num_func_blocks : symt->num_file_blocks; + + // Binary search for the block + int min = 0; + int max = num_blocks - 1; + + while (min < max) { + int mid = (min + max + 1) / 2; + uint32_t entry_start_idx = symt_read_u32(symt->rom_offset + tab_off + mid * 8 + 0); + if (idx < entry_start_idx) + max = mid - 1; + else + min = mid; + } + + // Read the block entry + uint32_t entry_start_idx = symt_read_u32(symt->rom_offset + tab_off + min * 8 + 0); + uint32_t entry_blob_off = symt_read_u32(symt->rom_offset + tab_off + min * 8 + 4); + if (idx < entry_start_idx) { + snprintf(buf, size, "%s", UNKNOWN_SYMBOL); + return buf; + } + + // Read Huffman Table (Global) + uint32_t huff_size = MIN(symt->huff_tab_size, MAX_BUFFER_SIZE); + uint8_t alignas(16) huff_tab[huff_size] __attribute__((uninitialized)); + data_cache_hit_writeback_invalidate(huff_tab, huff_size); + dma_read(huff_tab, symt->rom_offset + symt->huff_tab_off, huff_size); + + huff_decoder_t dec; + huff_decoder_init(&dec, huff_tab, huff_size); + + // Read string block + uint8_t alignas(16) str_blob[MAX_BUFFER_SIZE] __attribute__((uninitialized)); + data_cache_hit_writeback_invalidate(str_blob, sizeof(str_blob)); + dma_read(str_blob, symt->rom_offset + blob_off + entry_blob_off, sizeof(str_blob)); + + // Decode strings + bit_reader_t br; + br_init(&br, str_blob); + + int target_in_block = idx - entry_start_idx; + + // Reuse the caller-provided buffer as the rolling front-coding buffer. + buf[0] = 0; + int cur_len = 0; + int prev_prefix_len = 0; + for (int i=0; i <= target_in_block; i++) { + // Decode prefix length delta, and apply it to the current common length + int32_t prefix_delta = read_exp_golomb_signed(&br); + int prefix_len = prev_prefix_len + prefix_delta; + prev_prefix_len = prefix_len; + + // Safety checks. We can't really assert here (as the assert would trigger this code again) + // so better make sure we don't cause buffer overflows in case of corruption. + if (prefix_len > cur_len) prefix_len = cur_len; + if (prefix_len >= size-1) prefix_len = size - 2; + + // Decode suffix characters until \0 (or error) + cur_len = prefix_len; + int sym; + while ((sym = huff_decode_symbol(&dec, &br)) > 0) { + if (cur_len < size - 1) { + buf[cur_len++] = (char)sym; + } + } + buf[cur_len] = 0; + } + + return buf; +} + +char* symt_get_func_name(symtable_header_t *symt, symtable_entry_t *entry, uint32_t addr, char *buf, int size) +{ + if (addr >= (uint32_t)inthandler && addr < (uint32_t)inthandler_end) { + // Special case exception handlers. This is just to show something slightly + // more readable instead of "notcart+0x0" or similar assembly symbols + snprintf(buf, size, ""); + return buf; + } else { + return symt_get_string(symt, entry->func_sidx, buf, size, true); + } +} + +char* symt_get_file_name(symtable_header_t *symt, symtable_entry_t *entry, uint32_t addr, char *buf, int size) +{ + return symt_get_string(symt, entry->file_sidx, buf, size, false); +} diff --git a/src/symtable_internal.h b/src/symtable_internal.h new file mode 100644 index 000000000..a21a2e69c --- /dev/null +++ b/src/symtable_internal.h @@ -0,0 +1,111 @@ +/** + * @file symtable_internal.h + * @author Giovanni Bajo + * @brief Internal API for accessing the SYMT symbol table + * @ingroup backtrace + * + * The SYMT file, normally embedded in the rompak, allows to symbolize addresses + * for debugging purposes. It is mostly used by backtrace.c to create symbolized + * backtraces. The SYMT file is generated by the n64sym tool. + * + * Since it is shipped by default, the format is quite complex as it is designed + * in a way to be directly queried from ROM, while still being as compact as + * possible ot reduce ROM size, and use as little as 512 bytes of RAM at runtime. + * The format is described in symtable.c + */ +#ifndef LIBDRAGON_SYMTABLE_INTERNAL_H +#define LIBDRAGON_SYMTABLE_INTERNAL_H + +#include +#include + +/** @brief Symbol table file header */ +typedef struct { + char head[4]; ///< Magic ID "SYMT" + uint32_t version; ///< Version of the symbol table + uint32_t num_symbols; ///< Total number of symbols + uint32_t num_chunks; ///< Total number of compressed symbol chunks + + // Offsets to sections + uint32_t chunk_idx_off; ///< Offset of chunk_index_t[] + uint32_t file_tab_off; ///< Offset of uint32_t[] (offsets into file_blob) + uint32_t func_tab_off; ///< Offset of uint32_t[] (offsets into func_blob) + uint32_t huff_tab_off; ///< Offset of the Global Huffman Table + uint32_t file_blob_off; ///< Offset of the file string blob + uint32_t func_blob_off; ///< Offset of the func string blob + uint32_t stream_off; ///< Offset of the compressed symbol stream + uint32_t max_chunk_margin; ///< Max in-place decompression safety margin across chunks + + // Size of sections (useful for bounds checking) + uint32_t num_files; ///< Total number of file strings + uint32_t num_funcs; ///< Total Number of func strings + uint32_t num_file_blocks; ///< Number of file blocks + uint32_t num_func_blocks; ///< Number of func blocks + uint32_t huff_tab_size; ///< Size of Huffman table in bytes + uint32_t file_blob_size; ///< Size of file blob in bytes + uint32_t func_blob_size; ///< Size of func blob in bytes + uint32_t stream_size; ///< Size of compressed stream in bytes + + // Runtime-only fields (not serialized in .sym file) + uint32_t rom_offset; ///< ROM offset where this SYMT was loaded from + uint32_t addr_base; ///< Load offset: prog_base for DSOs, 0 for main executable +} __attribute__((aligned(8))) symtable_header_t; + +/** @brief Symbol table entry **/ +typedef struct { + uint32_t func_sidx; ///< Index of the function name in the string table + uint32_t file_sidx; ///< Index of the file name in the string table + uint32_t line; ///< Line number (or 0 if this symbol generically refers to a whole function) + uint32_t func_off; ///< Offset of the symbol within its function + bool is_inline; ///< True if the symbol is an inlined function +} symtable_entry_t; + +/** + * @brief Open the SYMT symbol table in the rompak. + * + * If not found, return a null header. + */ +symtable_header_t symt_open(void *addr); + +/** @brief Placeholder used in frames where symbols are not available */ +extern const char *UNKNOWN_SYMBOL; + +/** + * @brief Find one or more symbols in the SYMT file by address. + * + * There could be multiple symbols for the same address, in case of inlined functions. + * + * @param symt SYMT file header + * @param addr Runtime address to search for + * @param entries Output entry structure array + * @param max_entries Size of the output entry array + * @return Number of entries found (0..max_entries) + */ +int symt_find_symbol(symtable_header_t *symt, uint32_t addr, symtable_entry_t *entries, int max_entries); + +/** + * @brief Fetch the function name of an entry + * + * @param symt SYMT file + * @param entry Symbol entry + * @param addr Address being looked up (for special cases) + * @param buf Destination buffer + * @param size Size of the destination buffer + * @return Fetched string within the destination buffer + */ +char* symt_get_func_name(symtable_header_t *symt, symtable_entry_t *entry, uint32_t addr, char *buf, int size); + +/** + * @brief Fetch the file name of an entry + * + * @param symt SYMT file + * @param entry Symbol entry + * @param addr Address being looked up + * @param buf Destination buffer + * @param size Size of the destination buffer + * @return Fetched string within the destination buffer + */ +char* symt_get_file_name(symtable_header_t *symt, symtable_entry_t *entry, uint32_t addr, char *buf, int size); + +#endif // LIBDRAGON_SYMTABLE_INTERNAL_H + diff --git a/src/system.c b/src/system.c index 4d464e5cc..f690ae2c5 100644 --- a/src/system.c +++ b/src/system.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -22,7 +23,12 @@ #define SYSTEM_NO_DEPRECATED ///@endcond #include "system.h" +#include "kernel.h" +#include "debug.h" +#include "kernel/kernel_internal.h" +#include "rand_internal.h" #include "n64sys.h" +#include "rtc_internal.h" /** * @name STDIN/STDOUT/STDERR definitions from unistd.h @@ -53,11 +59,27 @@ /** Total Size of the heap */ int __heap_total_size = 0; +/** Memory allocated via sbrk_top */ +int __heap_top_allocated_size = 0; /** End of the heap */ char *__heap_end = 0; /** Top of the heap */ char *__heap_top = 0; +/** + * @brief Lazy initialize heap boundaries. + * + * Both sbrk() and sbrk_top() can be the first heap API called at runtime. + */ +static void __heap_init_if_needed(void) +{ + if (__heap_end == 0) { + __heap_end = (char*)HEAP_START_ADDR; + __heap_top = (char*)KSEG0_START_ADDR + __boot_memsize - STACK_SIZE; + __heap_total_size = (int)((unsigned long)__heap_top - (unsigned long)__heap_end); + } +} + /** * @brief Write to the MESS debug register * @@ -99,6 +121,10 @@ typedef struct char *prefix; /** @brief Filesystem callback pointers */ filesystem_t *fs; + /** @brief Mutex used to protect concurrent accesses to the filesystem */ + kmutex_t lock; + /** @brief True if a lock is needed for this filesystem */ + bool need_lock; } fs_mapping_t; /** @brief Extract bits from word */ @@ -149,8 +175,10 @@ static int handle_open_count; static fs_mapping_t filesystems[MAX_FILESYSTEMS] = { { 0 } }; /** @brief Current stdio hook structure */ static stdio_t stdio_hooks = { 0 }; -/** @brief Function to provide the current time */ -time_t (*time_hook)( void ) = NULL; +/** @brief Current time hooks structure */ +static time_hooks_t time_hooks = { 0 }; +/** @brief Current real-time clock hooks structure */ +static rtc_hooks_t rtc_hooks = { 0 }; /* Forward definitions */ int close( int fildes ); @@ -274,34 +302,6 @@ static int __strcmp( const char * const a, const char * const b ) return __strncmp( a, b, -1 ); } -/** - * @brief Simple implementation of rand() - * - * @param state Random state - * @return uint32_t New random value - */ -static uint32_t __rand( uint32_t *state ) -{ - uint32_t x = *state; - x ^= x << 13; - x ^= x >> 7; - x ^= x << 5; - return *state = x; -} - -/** - * @brief Generate a random number in range [0..n[ - * - * @param state Random state - * @param n Upper bound (exclusive) - * @return uint32_t Random number - */ -static inline uint32_t __randn( uint32_t *state, int n ) -{ - if(__builtin_constant_p( n )) return __rand( state ) % n; - return ((uint64_t)__rand( state ) * n) >> 32; -} - int attach_filesystem( const char * const prefix, filesystem_t *filesystem ) { /* Sanity checking */ @@ -359,6 +359,10 @@ int attach_filesystem( const char * const prefix, filesystem_t *filesystem ) /* Attach the inputted filesystem */ filesystems[handle].fs = filesystem; + /* Initialize the mutex */ + kmutex_init(&filesystems[handle].lock, KMUTEX_STANDARD); + filesystems[handle].need_lock = __kernel && !filesystem->thread_safe; + /* All went well */ return 0; } @@ -394,15 +398,45 @@ int detach_filesystem( const char * const prefix ) return -2; } +int detach_filesystem_by_pointer( filesystem_t *filesystem ) +{ + /* Sanity checking */ + if( !filesystem ) + { + errno = EINVAL; + return -1; + } + + for( int i = 0; i < MAX_FILESYSTEMS; i++ ) + { + if( filesystems[i].prefix && filesystems[i].fs == filesystem ) + { + /* Now free the memory associated with the prefix and zero out the filesystem */ + free( filesystems[i].prefix ); + filesystems[i].prefix = 0; + filesystems[i].fs = 0; + + /* All went well */ + return 0; + } + } + + /* Couldn't find the filesystem to free */ + errno = EPERM; + return -2; +} + /** * @brief Allocate a new fileno for the given handle * * @param handle Filesystem handle * @param fs_index Filesystem index - * @return int New fileno, or -1 if it cannot be allocated (errno will be set) + * @return New fileno, or -1 if it cannot be allocated (errno will be set) */ static int __allocate_fileno( void *handle, int fs_index ) { + kthread_lock(); + /* Allocate whenever the handle map is full at 75% to avoid wasting too * much time looking for an empty ID. */ if( !handle_buckets_count || @@ -412,6 +446,7 @@ static int __allocate_fileno( void *handle, int fs_index ) void *mem = calloc( HANDLE_BUCKET_SIZE, sizeof( void* ) ); if( !mem ) { + kthread_unlock(); errno = ENOMEM; return -1; } @@ -419,12 +454,12 @@ static int __allocate_fileno( void *handle, int fs_index ) } /* Select a random bucket and a random initial position. This should - * help finding an empty slot fast enough. Use the handle pointer - * as seed; avoid using C0_COUNT because aggressively changing fileno - * might cause some headaches during debugging sessions. */ - uint32_t rand_state = (uint32_t)handle ^ (uint32_t)fs_index; - uint32_t bkt_idx = handle_buckets_count > 1 ? __randn( &rand_state, handle_buckets_count ) : 0; - uint32_t bkt_pos = __randn( &rand_state, HANDLE_BUCKET_SIZE ); + * help finding an empty slot fast enough. Avoid using C0_COUNT because + * aggressively changing fileno might cause some headaches during debugging + * sessions. */ + uint32_t rn = __rand32(); + uint32_t bkt_pos = rn % HANDLE_BUCKET_SIZE; + uint32_t bkt_idx = handle_buckets_count > 1 ? ( (rn >> 16) % handle_buckets_count ) : 0; /* Go through all buckets and positions and look for an empty slot. */ for (int i=0; ifs->close( handle ); + if (fsm->need_lock) kmutex_lock(&fsm->lock); + int ret = fsm->fs->close( handle ); + if (fsm->need_lock) kmutex_unlock(&fsm->lock); + return ret; } /** @@ -716,7 +757,10 @@ int fstat( int fileno, struct stat *st ) return -1; } - return fsm->fs->fstat( *handle_ptr, st ); + if (fsm->need_lock) kmutex_lock(&fsm->lock); + int ret = fsm->fs->fstat( *handle_ptr, st ); + if (fsm->need_lock) kmutex_unlock(&fsm->lock); + return ret; } } @@ -746,15 +790,92 @@ int getpid( void ) */ int gettimeofday( struct timeval *ptimeval, void *ptimezone ) { - if( time_hook != NULL ) + time_t time; + if( time_hooks.gettime != NULL ) { - time_t time = time_hook(); + time = time_hooks.gettime(); if( time != -1 ) { ptimeval->tv_sec = time; ptimeval->tv_usec = 0; return 0; } + errno = EIO; + return -2; + } + + if( rtc_hooks.gettime != NULL ) + { + switch( rtc_hooks.gettime( &time ) ) + { + case RTC_ESUCCESS: + ptimeval->tv_sec = time; + ptimeval->tv_usec = 0; + return 0; + case RTC_ENOCLOCK: + errno = ENODEV; + return -2; + case RTC_EBADCLOCK: + errno = EIO; + return -2; + case RTC_EBADTIME: + errno = EBADMSG; + return -2; + default: + errno = ENOMSG; + return -1; + } + } + + errno = ENOSYS; + return -1; +} + +/** + * @brief Set the current time + * + * @param[out] ptimeval + * Time structure containing the new current time. + * @param[out] ptimezone + * Timezone information. (Not supported) + * + * @retval 0 Success + * @retval -1 Operation not available (errno is set) + * @retval -2 Operation failed (errno is set) + */ +int settimeofday( const struct timeval *ptimeval, const struct timezone *ptimezone) +{ + time_t time = ptimeval->tv_sec; + if( time_hooks.settime != NULL ) + { + if( time_hooks.settime( time ) ) + { + return 0; + } + + errno = EIO; + return -2; + } + + if( rtc_hooks.settime != NULL ) + { + switch( rtc_hooks.settime( time ) ) + { + case RTC_ESUCCESS: + return 0; + case RTC_ENOCLOCK: + errno = ENODEV; + return -2; + case RTC_EBADCLOCK: + errno = EIO; + return -2; + case RTC_EBADTIME: + errno = EINVAL; + return -2; + default: + errno = ENOMSG; + return -1; + } } errno = ENOSYS; @@ -856,7 +977,10 @@ int lseek( int file, int ptr, int dir ) return -1; } - return fsm->fs->lseek( *handle_ptr, ptr, dir ); + if (fsm->need_lock) kmutex_lock(&fsm->lock); + int ret = fsm->fs->lseek( *handle_ptr, ptr, dir ); + if (fsm->need_lock) kmutex_unlock(&fsm->lock); + return ret; } /** @@ -914,7 +1038,9 @@ int open( const char *file, int flags, ... ) errno = 0; /* Use the old open() call that will cause an additional allocation */ + if (fsm->need_lock) kmutex_lock(&fsm->lock); void *handle = fsm->fs->open( (char *)( file + __strlen( filesystems[fs_index].prefix ) ), flags ); + if (fsm->need_lock) kmutex_unlock(&fsm->lock); if( handle ) { @@ -981,7 +1107,10 @@ int read( int fileno, char *ptr, int len ) return -1; } - return fsm->fs->read( *handle_ptr, (uint8_t *)ptr, len ); + if (fsm->need_lock) kmutex_lock(&fsm->lock); + int ret = fsm->fs->read( *handle_ptr, (uint8_t *)ptr, len ); + if (fsm->need_lock) kmutex_unlock(&fsm->lock); + return ret; } } @@ -1016,40 +1145,80 @@ int readlink( const char *path, char *buf, size_t bufsize ) */ void *sbrk( int incr ) { - char * prev_heap_end; + char *prev_heap_end; + char *new_heap_end; + bool emit_exp_pak_warning = false; disable_interrupts(); - if( __heap_end == 0 ) - { - __heap_end = (char*)HEAP_START_ADDR; - __heap_top = (char*)KSEG0_START_ADDR + __boot_memsize - STACK_SIZE; - __heap_total_size = (int)((unsigned long)__heap_top - (unsigned long)__heap_end); - } + __heap_init_if_needed(); prev_heap_end = __heap_end; - __heap_end += incr; + new_heap_end = __heap_end + incr; - // check if out of memory - if (__heap_end > __heap_top) + // Keep the heap end inside [HEAP_START_ADDR, __heap_top] + if (new_heap_end < (char*)HEAP_START_ADDR || new_heap_end > __heap_top) { - __heap_end -= incr; prev_heap_end = (char *)-1; errno = ENOMEM; + } else { + __heap_end = new_heap_end; } if (__heap_end - (char*)KSEG0_START_ADDR >= 4*1024*1024 - STACK_SIZE && !__expanded_memory_asserted) { - static char warning[] = "WARNING: Allocations beyond 4 MiB: this ROM requires the expansion pak to work properly.\nWARNING: Call assert_memory_expanded() or is_memory_expanded() in main to disable this warning.\n"; - write( STDERR_FILENO, warning, sizeof(warning) - 1 ); __expanded_memory_asserted = true; // only emit the warning once + emit_exp_pak_warning = true; } enable_interrupts(); + if (emit_exp_pak_warning) + { + static char warning[] = "WARNING: Allocations beyond 4 MiB: this ROM requires the expansion pak to work properly.\nWARNING: Call assert_memory_expanded() or is_memory_expanded() in main to disable this warning.\n"; + write( STDERR_FILENO, warning, sizeof(warning) - 1 ); + } + return (void *)prev_heap_end; } +/** + * @brief Allocate static memory from the top of the heap + * + * @param incr + * The amount of memory needed in bytes + * + * @return A pointer to the memory or ((void*)-1) on error allocating. + * + * @note This function is internal, and can only used by the display module, + * to allocate memory from the top memory bank for the Z buffer. + */ +void* sbrk_top( int incr ) +{ + char *heap_top_limit; + char *new_heap_top; + + disable_interrupts(); + + __heap_init_if_needed(); + heap_top_limit = (char*)KSEG0_START_ADDR + __boot_memsize - STACK_SIZE; + new_heap_top = __heap_top - incr; + + if (new_heap_top < __heap_end || new_heap_top > heap_top_limit) + { + errno = ENOMEM; + enable_interrupts(); + return (void *)-1; + } + + __heap_top = new_heap_top; + __heap_top_allocated_size += incr; + + enable_interrupts(); + + return __heap_top; +} + /** * @brief Return file stats based on a file name * @@ -1074,7 +1243,10 @@ int stat( const char *file, struct stat *st ) /* Use stat function when available, and fstat as a fallback */ if( fsm != 0 && mapping >= 0 && fsm->fs->stat ) { - return fsm->fs->stat( (char *)file + __strlen( filesystems[mapping].prefix ) - 1, st ); + if (fsm->need_lock) kmutex_lock(&fsm->lock); + int ret = fsm->fs->stat( (char *)file + __strlen( filesystems[mapping].prefix ) - 1, st ); + if (fsm->need_lock) kmutex_unlock(&fsm->lock); + return ret; } /* Dirty hack, open read only */ @@ -1151,7 +1323,10 @@ int unlink( char *name ) } /* Must offset past the prefix */ - return fsm->fs->unlink( name + __strlen( filesystems[mapping].prefix ) ); + if (fsm->need_lock) kmutex_lock(&fsm->lock); + int ret = fsm->fs->unlink( name + __strlen( filesystems[mapping].prefix ) ); + if (fsm->need_lock) kmutex_unlock(&fsm->lock); + return ret; } /** @@ -1180,13 +1355,16 @@ int ioctl(int fd, unsigned long cmd, void *argp) errno = EBADF; return -1; } - if( fsm->fs->ioctl == 0 ) + if(fsm->fs->ioctl == 0 ) { /* Filesystem doesn't support ioctl */ errno = ENOTTY; return -1; } - return fsm->fs->ioctl(*handle_ptr, cmd, argp); + if (fsm->need_lock) kmutex_lock(&fsm->lock); + int ret = fsm->fs->ioctl(*handle_ptr, cmd, argp); + if (fsm->need_lock) kmutex_unlock(&fsm->lock); + return ret; } /** @@ -1252,7 +1430,10 @@ int write( int file, char *ptr, int len ) return -1; } - return fsm->fs->write( *handle_ptr, (uint8_t *)ptr, len ); + if (fsm->need_lock) kmutex_lock(&fsm->lock); + int ret = fsm->fs->write( *handle_ptr, (uint8_t *)ptr, len ); + if (fsm->need_lock) kmutex_unlock(&fsm->lock); + return ret; } } @@ -1266,7 +1447,7 @@ int write( int file, char *ptr, int len ) * * @param file File handle * @param length New length of the file - * @return int 0 on success, -1 on failure (errno will be set) + * @return 0 on success, -1 on failure (errno will be set) */ int ftruncate( int file, off_t length ) { @@ -1286,7 +1467,10 @@ int ftruncate( int file, off_t length ) return -1; } - return fsm->fs->ftruncate( *handle_ptr, length ); + if (fsm->need_lock) kmutex_lock(&fsm->lock); + int ret = fsm->fs->ftruncate( *handle_ptr, length ); + if (fsm->need_lock) kmutex_unlock(&fsm->lock); + return ret; } /** @@ -1299,7 +1483,7 @@ int ftruncate( int file, off_t length ) * * @param path Path to the file * @param length New length of the file - * @return int 0 on success, -1 on failure (errno will be set) + * @return 0 on success, -1 on failure (errno will be set) */ int truncate( const char *path, off_t length ) { @@ -1320,14 +1504,14 @@ int dir_findfirst( const char * const path, dir_t *dir ) if( fsm == 0 || mapping < 0 || dir == 0 ) { errno = EINVAL; - return -1; + return -2; } if( fsm->fs->findfirst == 0 ) { /* Filesystem doesn't support findfirst */ errno = ENOSYS; - return -1; + return -2; } /* Initialize dir_t structure. Set size to -1 in case the filesystem @@ -1335,7 +1519,10 @@ int dir_findfirst( const char * const path, dir_t *dir ) __builtin_memset( dir, 0, sizeof( dir_t ) ); dir->d_size = -1; - return fsm->fs->findfirst( (char *)path + __strlen( filesystems[mapping].prefix ) - 1, dir ); + if (fsm->need_lock) kmutex_lock(&fsm->lock); + int ret = fsm->fs->findfirst( (char *)path + __strlen( filesystems[mapping].prefix ) - 1, dir ); + if (fsm->need_lock) kmutex_unlock(&fsm->lock); + return ret; } int dir_findnext( const char * const path, dir_t *dir ) @@ -1346,19 +1533,22 @@ int dir_findnext( const char * const path, dir_t *dir ) if( fsm == 0 || dir == 0 ) { errno = EINVAL; - return -1; + return -2; } if( fsm->fs->findnext == 0 && fsm->fs->findnext2 == 0 ) { /* Filesystem doesn't support findnext */ errno = ENOSYS; - return -1; + return -2; } - return fsm->fs->findnext2 ? + if (fsm->need_lock) kmutex_lock(&fsm->lock); + int ret = fsm->fs->findnext2 ? fsm->fs->findnext2( path + __strlen( filesystems[mapping].prefix ) - 1, dir ) : fsm->fs->findnext( dir ); + if (fsm->need_lock) kmutex_unlock(&fsm->lock); + return ret; } /** @@ -1368,7 +1558,7 @@ int dir_findnext( const char * const path, dir_t *dir ) * * @param path Path of the directory to create, relative to the root of the filesystem * @param mode Directory access mode - * @return int 0 on success, -1 on failure (errno will be set) + * @return 0 on success, -1 on failure (errno will be set) */ int mkdir( const char * path, mode_t mode ) { @@ -1388,7 +1578,74 @@ int mkdir( const char * path, mode_t mode ) return -1; } - return fsm->fs->mkdir( (char *)path + __strlen( filesystems[mapping].prefix ) - 1, mode ); + if (fsm->need_lock) kmutex_lock(&fsm->lock); + int ret = fsm->fs->mkdir( (char *)path + __strlen( filesystems[mapping].prefix ) - 1, mode ); + if (fsm->need_lock) kmutex_unlock(&fsm->lock); + return ret; +} + +/** + * @brief Update the access and modification times of a file. + * + * @param path Path to the file (with filesystem prefix) + * @param times New access and modification times. If NULL, use the current time. + * @return 0 on success, -1 on failure (errno will be set) + */ +int utimes(const char *path, const struct timeval times[2]) +{ + fs_mapping_t *fsm = __get_fs_pointer_by_name( path ); + int mapping = __get_fs_link_by_name( path ); + + if( fsm == 0 || mapping < 0 ) + { + errno = EINVAL; + return -1; + } + + if( fsm->fs->utimes == 0 ) + { + /* Filesystem doesn't support utimes */ + errno = ENOSYS; + return -1; + } + + // If times is NULL, use the current time + struct timeval now[2]; + if (times == NULL) { + if (gettimeofday(&now[0], NULL) != 0) { + return -1; + } + now[1] = now[0]; + times = now; + } + + if (fsm->need_lock) kmutex_lock(&fsm->lock); + int ret = fsm->fs->utimes( (char *)path + __strlen( filesystems[mapping].prefix ) - 1, times ); + if (fsm->need_lock) kmutex_unlock(&fsm->lock); + return ret; +} + +/** + * @brief Update the access and modification times of a file. + * + * @param path Path to the file (with filesystem prefix) + * @param times New access and modification times. If NULL, use the current time. + * @return 0 on success, -1 on failure (errno will be set) + */ +int utime(const char *path, const struct utimbuf *times) +{ + if (times) + { + struct timeval tv[2] = { + { .tv_sec = times->actime, .tv_usec = 0 }, + { .tv_sec = times->modtime, .tv_usec = 0 } + }; + return utimes(path, tv); + } + else + { + return utimes(path, NULL); + } } int hook_stdio_calls( stdio_t *stdio_calls ) @@ -1425,28 +1682,62 @@ int unhook_stdio_calls( stdio_t *stdio_calls ) return 0; } -int hook_time_call( time_t (*time_fn)( void ) ) +int hook_rtc_calls( rtc_hooks_t *hooks ) { - if( time_fn == NULL ) - { - return -1; - } + if( hooks == NULL ) return -1; - time_hook = time_fn; + rtc_hooks.gettime = hooks->gettime; + rtc_hooks.settime = hooks->settime; return 0; } -int unhook_time_call( time_t (*time_fn)( void ) ) +int unhook_rtc_calls( rtc_hooks_t *hooks ) { - if( time_hook == time_fn ) - { - time_hook = NULL; - } + if( hooks == NULL ) return -1; + + if( rtc_hooks.gettime == hooks->gettime ) rtc_hooks.gettime = NULL; + if( rtc_hooks.settime == hooks->settime ) rtc_hooks.settime = NULL; + + return 0; +} + +/** @deprecated Use #hook_rtc_calls instead. */ +int hook_time_calls( time_hooks_t *hooks ) +{ + if( hooks == NULL ) return -1; + + time_hooks.gettime = hooks->gettime; + time_hooks.settime = hooks->settime; return 0; } +/** @deprecated Use #unhook_rtc_calls instead. */ +int unhook_time_calls( time_hooks_t *hooks ) +{ + if( hooks == NULL ) return -1; + + if( time_hooks.gettime == hooks->gettime ) time_hooks.gettime = NULL; + if( time_hooks.settime == hooks->settime ) time_hooks.settime = NULL; + + return 0; +} + +/** @deprecated Use #hook_time_calls instead. */ +int hook_time_call( time_t (*time_fn)( void ) ) +{ + time_hooks_t hooks = { time_fn, NULL }; + return hook_time_calls( &hooks ); +} + +/** @deprecated Use #unhook_time_calls instead. */ +int unhook_time_call( time_t (*time_fn)( void ) ) +{ + time_hooks_t hooks = { time_fn, NULL }; + return unhook_time_calls( &hooks ); +} + /** * @brief Implement _flush_cache as required by GCC for nested functions. * @@ -1474,3 +1765,7 @@ void __assert_func(const char *file, int line, const char *func, const char *fai __assert_func_ptr(file, line, func, failedexpr); abort(); } + +#ifdef __NEWLIB__ +#include "system_newlib_locks.c" +#endif diff --git a/src/system_internal.h b/src/system_internal.h new file mode 100644 index 000000000..96623bfe8 --- /dev/null +++ b/src/system_internal.h @@ -0,0 +1,10 @@ +/** + * @file system_internal.h + * @author Giovanni Bajo + */ +#ifndef LIBDRAGON_SYSTEM_INTERNAL_H +#define LIBDRAGON_SYSTEM_INTERNAL_H + +void* sbrk_top( int incr ); + +#endif diff --git a/src/system_newlib_locks.c b/src/system_newlib_locks.c new file mode 100644 index 000000000..91eaa3186 --- /dev/null +++ b/src/system_newlib_locks.c @@ -0,0 +1,122 @@ +/** + * @file system_newlib_locks.c + * @author Giovanni Bajo + * @brief newlib lock implementation + * @ingroup system + * + * This file contains the lock implementation used in libc (newlib). They are + * used to guarantee thread safety in the C library functions like malloc + * or file descriptors. + * + * All functions in this file must be defined, so that it takes precedence + * over libc's default implementation (which is empty). + */ +#include "kernel.h" +#include + +// Toolchains since Sept 2024 build with threads and retargetable locks enabled, +// both of which are required for libc to be thread safe and thus the kernel +// to work correctly (otherwise, kernel_init() will assert). +// We want to be able to build libdragon with old toolchains as well, obviously +// for applications not using the kernel. This file wouldn't complete there +// because on those toolchains, LOCK_T is a typedef to int. So we just disable it. +#ifdef _RETARGETABLE_LOCKING + +/** Lock structure: use a kmutex from our kernel */ +struct __lock { + kmutex_t mut; +}; + +///@cond +struct __lock __lock___sfp_recursive_mutex; +struct __lock __lock___atexit_recursive_mutex; +struct __lock __lock___at_quick_exit_mutex; +struct __lock __lock___malloc_recursive_mutex; +struct __lock __lock___env_recursive_mutex; +struct __lock __lock___tz_mutex; +struct __lock __lock___dd_hash_mutex; +struct __lock __lock___arc4random_mutex; +///@endcond + +/** Pool of dynamically allocated lists */ +static struct __lock __libc_mutexes[64]; +static uint64_t __libc_mutexes_bitmap = 0; +extern bool __kernel; + +/** Alloca a dynamic lock from our static pool */ +static struct __lock* __alloc_libc_mutex(void) { + for (int i = 0; i < 64; i++) { + if (!(__libc_mutexes_bitmap & (1ull << i))) { + __libc_mutexes_bitmap |= (1ull << i); + return &__libc_mutexes[i]; + } + } + assert(0); + return NULL; +} + +/** Free a dynamic lock from our static pool */ +static void __free_libc_mutex(struct __lock* lock) { + int i = lock - __libc_mutexes; + __libc_mutexes_bitmap &= ~(1ull << i); +} + +///@cond +// Not part of libdragon public API, no need to document these + +void __retarget_lock_init(_LOCK_T *lock) { + // Allow to allocate the locks even if the kernel is not initialized. + // For instance, this will happen for stderr. If later the kernel is initialized, + // the mutex will start working, otherwise they are nops. + *lock = __alloc_libc_mutex(); + kmutex_init(&(*lock)->mut, KMUTEX_STANDARD); +} +void __retarget_lock_init_recursive(_LOCK_T *lock) { + *lock = __alloc_libc_mutex(); + kmutex_init(&(*lock)->mut, KMUTEX_RECURSIVE); +} + +void __retarget_lock_close(_LOCK_T lock) { + kmutex_destroy(&lock->mut); + __free_libc_mutex(lock); +} +void __retarget_lock_close_recursive(_LOCK_T lock) { + kmutex_destroy(&lock->mut); + __free_libc_mutex(lock); +} + +void __retarget_lock_acquire(_LOCK_T lock) { + if (!__kernel) return; + kmutex_lock(&lock->mut); +} +void __retarget_lock_acquire_recursive(_LOCK_T lock) { + if (!__kernel) return; + // HACK: some libc locks (eg: malloc) are recursive, but they are statically + // defined and are designed to be zero-initialized. Since in our implementation + // zero means non-recursive, we need to set the flag here. + lock->mut.flags |= KMUTEX_RECURSIVE; + kmutex_lock(&lock->mut); +} + +int __retarget_lock_try_acquire(_LOCK_T lock) { + if (!__kernel) return 1; + return kmutex_try_lock(&lock->mut, 0); +} +int __retarget_lock_try_acquire_recursive(_LOCK_T lock) { + if (!__kernel) return 1; + lock->mut.flags |= KMUTEX_RECURSIVE; + return kmutex_try_lock(&lock->mut, 0); +} + +void __retarget_lock_release(_LOCK_T lock) { + if (!__kernel) return; + kmutex_unlock(&lock->mut); +} +void __retarget_lock_release_recursive (_LOCK_T lock) { + if (!__kernel) return; + kmutex_unlock(&lock->mut); +} + +///@endcond + +#endif \ No newline at end of file diff --git a/src/timer.c b/src/timer.c index 45b37d13c..ff1c70cca 100644 --- a/src/timer.c +++ b/src/timer.c @@ -160,27 +160,25 @@ timer_link_t *new_timer(int ticks, int flags, timer_callback1_t callback) { assertf(timer_init_refcount > 0, "timer module not initialized"); timer_link_t *timer = malloc(sizeof(timer_link_t)); - if (timer) - { - disable_interrupts(); - - uint32_t now = TICKS_READ(); - timer->left = now + (int32_t)ticks; - timer->set = ticks; - timer->flags = flags; - timer->callback = callback; - timer->ctx = NULL; + assertf(timer, "Out of memory"); + disable_interrupts(); - if (!(flags & TF_DISABLED)) - { - timer->next = TI_timers; - TI_timers = timer; - timer_update_compare(TI_timers, now); - timer_poll(); - } + uint32_t now = TICKS_READ(); + timer->left = now + (int32_t)ticks; + timer->set = ticks; + timer->flags = flags; + timer->callback = callback; + timer->ctx = NULL; - enable_interrupts(); + if (!(flags & TF_DISABLED)) + { + timer->next = TI_timers; + TI_timers = timer; + timer_update_compare(TI_timers, now); + timer_poll(); } + + enable_interrupts(); return timer; } @@ -188,27 +186,25 @@ timer_link_t *new_timer_context(int ticks, int flags, timer_callback2_t callback { assertf(timer_init_refcount > 0, "timer module not initialized"); timer_link_t *timer = malloc(sizeof(timer_link_t)); - if (timer) - { - disable_interrupts(); - - uint32_t now = TICKS_READ(); - timer->left = now + (int32_t)ticks; - timer->set = ticks; - timer->flags = flags | TF_CONTEXT; - timer->callback_with_context = callback; - timer->ctx = ctx; + assertf(timer, "Out of memory"); + disable_interrupts(); - if (!(flags & TF_DISABLED)) - { - timer->next = TI_timers; - TI_timers = timer; - timer_update_compare(TI_timers, now); - timer_poll(); - } + uint32_t now = TICKS_READ(); + timer->left = now + (int32_t)ticks; + timer->set = ticks; + timer->flags = flags | TF_CONTEXT; + timer->callback_with_context = callback; + timer->ctx = ctx; - enable_interrupts(); + if (!(flags & TF_DISABLED)) + { + timer->next = TI_timers; + TI_timers = timer; + timer_update_compare(TI_timers, now); + timer_poll(); } + + enable_interrupts(); return timer; } diff --git a/src/ucontext.c b/src/ucontext.c new file mode 100644 index 000000000..1cf1bc1fc --- /dev/null +++ b/src/ucontext.c @@ -0,0 +1,25 @@ +/** + * @file ucontext.c + * @author Max Bebök + * @author Giovanni Bajo + * @brief ucontext implementation + * @ingroup lowlevel + */ + +#include "ucontext.h" +#include +#include + +void makecontext( + ucontext_t *ctx, + void (*entry)(void), + int argc, + uint32_t arg0 +) { + assert(argc <= 1); + + memset(&ctx->uc_mcontext, 0, sizeof(ctx->uc_mcontext)); + ctx->uc_mcontext.ra = (uint32_t)entry; + ctx->uc_stack.ss_sp = (char*)ctx->uc_stack.ss_sp + ctx->uc_stack.ss_size; + ctx->uc_mcontext.a[0] = (uint32_t)arg0; +} diff --git a/src/ucontext_asm.S b/src/ucontext_asm.S new file mode 100644 index 000000000..5ee30ee63 --- /dev/null +++ b/src/ucontext_asm.S @@ -0,0 +1,75 @@ +.section .text.swapcontext +.p2align 5 +.globl swapcontext +.func swapcontext +.set noat +.set noreorder + +#define OFFSET_STACK 4 +#define OFFSET_REGS 16 +#define OFFSET_FPR OFFSET_REGS+80 + +swapcontext: + # Save callee-saved registers + sd $s0, OFFSET_REGS + 0*8($a0) + sd $s1, OFFSET_REGS + 1*8($a0) + sd $s2, OFFSET_REGS + 2*8($a0) + sd $s3, OFFSET_REGS + 3*8($a0) + sd $s4, OFFSET_REGS + 4*8($a0) + sd $s5, OFFSET_REGS + 5*8($a0) + sd $s6, OFFSET_REGS + 6*8($a0) + sd $s7, OFFSET_REGS + 7*8($a0) + sd $fp, OFFSET_REGS + 8*8($a0) + sw $ra, OFFSET_REGS + 9*8($a0) + sw $sp, OFFSET_STACK($a0) + + # FPU + cfc1 $1, $f31 + sdc1 $f20,(OFFSET_FPR + 0*8)($a0) + sdc1 $f21,(OFFSET_FPR + 1*8)($a0) + sdc1 $f22,(OFFSET_FPR + 2*8)($a0) + sdc1 $f23,(OFFSET_FPR + 3*8)($a0) + sdc1 $f24,(OFFSET_FPR + 4*8)($a0) + sdc1 $f25,(OFFSET_FPR + 5*8)($a0) + sdc1 $f26,(OFFSET_FPR + 6*8)($a0) + sdc1 $f27,(OFFSET_FPR + 7*8)($a0) + sdc1 $f28,(OFFSET_FPR + 8*8)($a0) + sdc1 $f29,(OFFSET_FPR + 9*8)($a0) + sdc1 $f30,(OFFSET_FPR + 10*8)($a0) + sdc1 $f31,(OFFSET_FPR + 11*8)($a0) + sw $1, (OFFSET_FPR + 12*8)($a0) + + # Restore new context + ld $s0, OFFSET_REGS + 0*8($a1) + ld $s1, OFFSET_REGS + 1*8($a1) + ld $s2, OFFSET_REGS + 2*8($a1) + ld $s3, OFFSET_REGS + 3*8($a1) + ld $s4, OFFSET_REGS + 4*8($a1) + ld $s5, OFFSET_REGS + 5*8($a1) + ld $s6, OFFSET_REGS + 6*8($a1) + ld $s7, OFFSET_REGS + 7*8($a1) + ld $fp, OFFSET_REGS + 8*8($a1) + lw $ra, OFFSET_REGS + 9*8($a1) + lw $sp, OFFSET_STACK($a1) + + # FPU + ldc1 $f20,(OFFSET_FPR + 0*8)($a1) + ldc1 $f21,(OFFSET_FPR + 1*8)($a1) + ldc1 $f22,(OFFSET_FPR + 2*8)($a1) + ldc1 $f23,(OFFSET_FPR + 3*8)($a1) + ldc1 $f24,(OFFSET_FPR + 4*8)($a1) + ldc1 $f25,(OFFSET_FPR + 5*8)($a1) + ldc1 $f26,(OFFSET_FPR + 6*8)($a1) + ldc1 $f27,(OFFSET_FPR + 7*8)($a1) + ldc1 $f28,(OFFSET_FPR + 8*8)($a1) + ldc1 $f29,(OFFSET_FPR + 9*8)($a1) + ldc1 $f30,(OFFSET_FPR + 10*8)($a1) + ldc1 $f31,(OFFSET_FPR + 11*8)($a1) + + lw $at, (OFFSET_FPR + 12*8)($a1) + ctc1 $at, $f31 + + jr $ra + lw $a0, OFFSET_REGS + 9*8 + 4($a1) + nop +.endfunc diff --git a/src/usb.c b/src/usb.c index 5614c71c2..ef91a31b9 100644 --- a/src/usb.c +++ b/src/usb.c @@ -10,9 +10,7 @@ using UNFLoader. #ifndef LIBDRAGON #include #else - #include - #include - #include + #include #endif #include @@ -81,15 +79,16 @@ using UNFLoader. #define D64_BASE 0x10000000 #define D64_REGS_BASE 0x18000000 +#define D64_REGS_BASE_EXTENDED 0x1F800000 -#define D64_REG_STATUS (D64_REGS_BASE + 0x0200) -#define D64_REG_COMMAND (D64_REGS_BASE + 0x0208) +#define D64_REG_STATUS (usb_64drive_get_baseaddr() + 0x0200) +#define D64_REG_COMMAND (usb_64drive_get_baseaddr() + 0x0208) -#define D64_REG_MAGIC (D64_REGS_BASE + 0x02EC) +#define D64_REG_MAGIC (usb_64drive_get_baseaddr() + 0x02EC) -#define D64_REG_USBCOMSTAT (D64_REGS_BASE + 0x0400) -#define D64_REG_USBP0R0 (D64_REGS_BASE + 0x0404) -#define D64_REG_USBP1R1 (D64_REGS_BASE + 0x0408) +#define D64_REG_USBCOMSTAT (usb_64drive_get_baseaddr() + 0x0400) +#define D64_REG_USBP0R0 (usb_64drive_get_baseaddr() + 0x0404) +#define D64_REG_USBP1R1 (usb_64drive_get_baseaddr() + 0x0408) #define D64_CI_BUSY 0x1000 @@ -98,13 +97,18 @@ using UNFLoader. #define D64_CI_ENABLE_ROMWR 0xF0 #define D64_CI_DISABLE_ROMWR 0xF1 +#define D64_CI_ENABLE_EXTADDR 0xF8 +#define D64_CI_DISABLE_EXTADDR 0xF9 + #define D64_CUI_ARM 0x0A #define D64_CUI_DISARM 0x0F #define D64_CUI_WRITE 0x08 #define D64_CUI_ARM_MASK 0x0F #define D64_CUI_ARM_IDLE 0x00 +#define D64_CUI_ARM_ARMED 0x01 #define D64_CUI_ARM_UNARMED_DATA 0x02 +#define D64_CUI_ARM_BUSY 0x0F #define D64_CUI_WRITE_MASK 0xF0 #define D64_CUI_WRITE_IDLE 0x00 @@ -215,16 +219,19 @@ using UNFLoader. *********************************/ static void usb_findcart(void); +static u32 usb_getaddr(); -static void usb_64drive_write(int datatype, const void* data, int size); +static s8 usb_64drive_write(int datatype, const void* data, int size); static u32 usb_64drive_poll(void); static void usb_64drive_read(void); +static void usb_64drive_set_extendedaddress(u8 enable); +static u32 usb_64drive_get_baseaddr(); -static void usb_everdrive_write(int datatype, const void* data, int size); +static s8 usb_everdrive_write(int datatype, const void* data, int size); static u32 usb_everdrive_poll(void); static void usb_everdrive_read(void); -static void usb_sc64_write(int datatype, const void* data, int size); +static s8 usb_sc64_write(int datatype, const void* data, int size); static u32 usb_sc64_poll(void); static void usb_sc64_read(void); @@ -234,7 +241,7 @@ static void usb_sc64_read(void); *********************************/ // Function pointers -void (*funcPointer_write)(int datatype, const void* data, int size); +s8 (*funcPointer_write)(int datatype, const void* data, int size); u32 (*funcPointer_poll)(void); void (*funcPointer_read)(void); @@ -248,6 +255,10 @@ static int usb_datasize = 0; static int usb_dataleft = 0; static int usb_readblock = -1; +// Cart specific globals +static vu8 d64_wasarmed = FALSE; +static u8 d64_extendedaddr = FALSE; + #ifndef LIBDRAGON // Message globals #if !USE_OSRAW @@ -363,7 +374,7 @@ static inline void usb_dma_write(void *ram_address, u32 pi_address, size_t size) osPiRawStartDma(OS_WRITE, pi_address, ram_address, size); #else osPiStartDma(&dmaIOMessageBuf, OS_MESG_PRI_NORMAL, OS_WRITE, pi_address, ram_address, size, &dmaMessageQ); - osRecvMesg(&dmaMessageQ, NULL, OS_MESG_BLOCK); + while (osRecvMesg(&dmaMessageQ, NULL, OS_MESG_NOBLOCK) != 0); #endif #else data_cache_hit_writeback(ram_address, size); @@ -424,7 +435,7 @@ static char usb_timeout_check(u32 start_ticks, u32 duration) /*============================== usb_initialize Initializes the USB buffers and pointers - @returns 1 if the USB initialization was successful, 0 if not + @return 1 if the USB initialization was successful, 0 if not ==============================*/ char usb_initialize(void) @@ -562,27 +573,43 @@ char usb_getcart(void) } +/*============================== + usb_getaddr + Gets the base address for the USB data to be stored in + @return The base data address +==============================*/ + +u32 usb_getaddr() +{ + if (usb_cart == CART_64DRIVE && d64_extendedaddr) + return 0x10000000 - DEBUG_ADDRESS_SIZE; + else + return DEBUG_ADDRESS; +} + + /*============================== usb_write Writes data to the USB. Will not write if there is data to read from USB - @param The DATATYPE that is being sent - @param A buffer with the data to send - @param The size of the data being sent + @param The DATATYPE that is being sent + @param A buffer with the data to send + @param The size of the data being sent + @return 1 on success, 0 on fail, -1 on timeout ==============================*/ -void usb_write(int datatype, const void* data, int size) +char usb_write(int datatype, const void* data, int size) { // If no debug cart exists, stop if (usb_cart == CART_NONE) - return; + return 0; // If there's data to read first, stop if (usb_dataleft != 0) - return; + return 0; // Call the correct write function - funcPointer_write(datatype, data, size); + return funcPointer_write(datatype, data, size); } @@ -668,6 +695,10 @@ void usb_read(void* buffer, int nbytes) block = BUFFER_SIZE; copystart = 0; } + + // Due to hardware issues, we should re-poll the 64Drive for data (which will unarm the buffer if there really isn't any more data) + if (usb_dataleft == 0 && usb_cart == CART_64DRIVE) + usb_64drive_poll(); } @@ -682,7 +713,13 @@ void usb_skip(int nbytes) // Subtract the amount of bytes to skip to the data pointers usb_dataleft -= nbytes; if (usb_dataleft < 0) + { usb_dataleft = 0; + + // Due to hardware issues, we should re-poll the 64Drive for data (which will unarm the buffer if there really isn't any more data) + if (usb_cart == CART_64DRIVE) + usb_64drive_poll(); + } } @@ -712,6 +749,10 @@ void usb_purge(void) usb_datatype = 0; usb_datasize = 0; usb_readblock = -1; + + // Due to hardware issues, we should re-poll the 64Drive for data (which will unarm the buffer if there really isn't any more data) + if (usb_cart == CART_64DRIVE) + usb_64drive_poll(); } @@ -809,17 +850,103 @@ static void usb_64drive_set_writable(u32 enable) } +/*============================== + usb_64drive_get_baseaddr + Gets the 64Drive's base address for CI commands + @return The CI base address +==============================*/ + +static u32 usb_64drive_get_baseaddr() +{ + return d64_extendedaddr ? D64_REGS_BASE_EXTENDED : D64_REGS_BASE; +} + + +/*============================== + usb_64drive_set_extendedaddress + Enables or disables 64Drive's extended address mode + @param Enables/disables extended address mode +==============================*/ + +__attribute__((used)) +static void usb_64drive_set_extendedaddress(u8 enable) +{ + // Wait until CI is not busy + usb_64drive_wait(); + + // Send enable extended address command + usb_io_write(D64_REG_COMMAND, enable ? D64_CI_ENABLE_EXTADDR : D64_CI_DISABLE_EXTADDR); + d64_extendedaddr = enable; + + // Wait until operation is finished + usb_64drive_wait(); +} + + +/*============================== + usb_64drive_cui_arm + Arms the 64Drive's USB with the guarantee that we will receive data + @param The address to put the data into + @param The size of the buffer +==============================*/ + +static void usb_64drive_cui_arm(u32 offset, u32 size) +{ + usb_io_write(D64_REG_USBP0R0, offset >> 1); + usb_io_write(D64_REG_USBP1R1, size & 0x00FFFFFF); + usb_io_write(D64_REG_USBCOMSTAT, D64_CUI_ARM); + while ((usb_io_read(D64_REG_USBCOMSTAT) & D64_CUI_ARM_MASK) != D64_CUI_ARM_UNARMED_DATA) + ; +} + + +/*============================== + usb_64drive_cui_armcheck + Arms the 64Drive's USB to check if we have data or not + @param The address to put the data into + @param The size of the buffer +==============================*/ + +static void usb_64drive_cui_armcheck(u32 offset, u32 size) +{ + usb_io_write(D64_REG_USBP0R0, offset >> 1); + usb_io_write(D64_REG_USBP1R1, size & 0x00FFFFFF); + usb_io_write(D64_REG_USBCOMSTAT, D64_CUI_ARM); + while ((usb_io_read(D64_REG_USBCOMSTAT) & D64_CUI_ARM_MASK) == D64_CUI_ARM_BUSY) + ; +} + + +/*============================== + usb_64drive_cui_disarm + Disarms the 64Drive's USB +==============================*/ + +static void usb_64drive_cui_disarm() +{ + usb_io_write(D64_REG_USBCOMSTAT, D64_CUI_DISARM); + while ((usb_io_read(D64_REG_USBCOMSTAT) & D64_CUI_ARM_MASK) != D64_CUI_ARM_IDLE) + ; +} + + /*============================== usb_64drive_cui_write Writes data from buffer in the 64drive through USB - @param Data type - @param Offset in CARTROM memory space - @param Transfer size + @param Data type + @param Offset in CARTROM memory space + @param Transfer size + @return 1 on success, 0 on fail, -1 on timeout ==============================*/ -static void usb_64drive_cui_write(u8 datatype, u32 offset, u32 size) +static s8 usb_64drive_cui_write(u8 datatype, u32 offset, u32 size) { u32 timeout; + u32 comstat = usb_io_read(D64_REG_USBCOMSTAT); + + // Check the arm status. If it's not idle, then we have to bail + if ((comstat & D64_CUI_ARM_MASK) != D64_CUI_ARM_IDLE) + return 0; // Start USB write usb_io_write(D64_REG_USBP0R0, offset >> 1); @@ -834,86 +961,71 @@ static void usb_64drive_cui_write(u8 datatype, u32 offset, u32 size) if (usb_timeout_check(timeout, D64_WRITE_TIMEOUT)) { usb_didtimeout = TRUE; - return; + return -1; } } while((usb_io_read(D64_REG_USBCOMSTAT) & D64_CUI_WRITE_MASK) != D64_CUI_WRITE_IDLE); -} - - -/*============================== - usb_64drive_cui_poll - Checks if there is data waiting to be read from USB FIFO - @return TRUE if data is waiting, FALSE if otherwise -==============================*/ - -static char usb_64drive_cui_poll(void) -{ - // Check if we have data waiting in buffer - if ((usb_io_read(D64_REG_USBCOMSTAT) & D64_CUI_ARM_MASK) == D64_CUI_ARM_UNARMED_DATA) - return TRUE; - return FALSE; + usb_didtimeout = FALSE; + return 1; } /*============================== usb_64drive_cui_read - Reads data from USB FIFO to buffer in the 64drive + Reads data from USB FIFO to buffer in the 64Drive + This code is structured a bit differently from the + instructions in the hardware spec sheet. Reasons for + this are complicated and are down to a firmware bug + that I wasted an entire week figuring out. + More info about this bug here: + https://github.com/buu342/N64-UNFLoader/wiki/3)-The-64Drive#pc---n64-usb-communication @param Offset in CARTROM memory space @return USB header (datatype + size) ==============================*/ static u32 usb_64drive_cui_read(u32 offset) { - u32 header; - u32 left; u32 datatype; - u32 size; - - // Arm USB FIFO with 8 byte sized transfer - usb_io_write(D64_REG_USBP0R0, offset >> 1); - usb_io_write(D64_REG_USBP1R1, 8); - usb_io_write(D64_REG_USBCOMSTAT, D64_CUI_ARM); - - // Wait until data is received - while ((usb_io_read(D64_REG_USBCOMSTAT) & D64_CUI_ARM_MASK) != D64_CUI_ARM_UNARMED_DATA) - ; - - // Get datatype and bytes remaining - header = usb_io_read(D64_REG_USBP0R0); - left = usb_io_read(D64_REG_USBP1R1) & 0x00FFFFFF; - datatype = header & 0xFF000000; - size = header & 0x00FFFFFF; - - // Determine if we need to read more data - if (left > 0) + u32 size = 0; + u32 left = 0; + + // Arm the USB to check if we have data to read + do { - // Arm USB FIFO with known transfer size - usb_io_write(D64_REG_USBP0R0, (offset + 8) >> 1); - usb_io_write(D64_REG_USBP1R1, left); - usb_io_write(D64_REG_USBCOMSTAT, D64_CUI_ARM); - - // Wait until data is received - while ((usb_io_read(D64_REG_USBCOMSTAT) & D64_CUI_ARM_MASK) != D64_CUI_ARM_UNARMED_DATA) - ; - - // Calculate total transfer length - size += left; - } - - // Disarm USB FIFO - usb_io_write(D64_REG_USBCOMSTAT, D64_CUI_DISARM); - - // Wait until USB FIFO is disarmed - while ((usb_io_read(D64_REG_USBCOMSTAT) & D64_CUI_ARM_MASK) != D64_CUI_ARM_IDLE) - ; + // Arm the USB to take the data + if (!d64_wasarmed || left > 0) + { + d64_wasarmed = TRUE; + usb_64drive_cui_arm(offset + size, DEBUG_ADDRESS_SIZE - size); + } + else // We recently armed and took data, arm again but check if we have more to serve + { + usb_64drive_cui_armcheck(offset, DEBUG_ADDRESS_SIZE); + + // No data, disarm + if ((usb_io_read(D64_REG_USBCOMSTAT) & D64_CUI_ARM_MASK) == D64_CUI_ARM_ARMED) + { + usb_64drive_cui_disarm(); + d64_wasarmed = FALSE; + return 0; + } + + // Otherwise, we have more data to read + } - // Due to a 64drive bug, we need to ignore the last 512 bytes of the transfer if it's larger than 512 bytes + // Read the result + datatype = usb_io_read(D64_REG_USBP0R0); + size += (datatype & 0x00FFFFFF); + left = (usb_io_read(D64_REG_USBP1R1) & 0x00FFFFFF); + } + while (left > 0); + + // Due to a 64Drive bug, we need to ignore the last 512 bytes of the transfer if it's larger than 512 bytes if (size > 512) size -= 512; // Return data header (datatype and size) - return (datatype | size); + return ((datatype & 0xFF000000) | size); } @@ -924,18 +1036,24 @@ static u32 usb_64drive_cui_read(u32 offset) @param The DATATYPE that is being sent @param A buffer with the data to send @param The size of the data being sent + @return 1 on success, 0 on fail, -1 on timeout ==============================*/ -static void usb_64drive_write(int datatype, const void* data, int size) +static s8 usb_64drive_write(int datatype, const void* data, int size) { s32 left = size; - u32 pi_address = D64_BASE + DEBUG_ADDRESS; + u32 pi_address = D64_BASE + usb_getaddr(); + u32 comstat = usb_io_read(D64_REG_USBCOMSTAT); + + // Check the arm status. If there's data, for some reason, we have to bail + if ((comstat & D64_CUI_ARM_MASK) != D64_CUI_ARM_IDLE) + return 0; // Return if previous transfer timed out - if ((usb_io_read(D64_REG_USBCOMSTAT) & D64_CUI_WRITE_MASK) == D64_CUI_WRITE_BUSY) + if ((comstat & D64_CUI_WRITE_MASK) == D64_CUI_WRITE_BUSY) { usb_didtimeout = TRUE; - return; + return -1; } // Set the cartridge to write mode @@ -967,8 +1085,7 @@ static void usb_64drive_write(int datatype, const void* data, int size) usb_64drive_set_writable(FALSE); // Send the data through USB - usb_64drive_cui_write(datatype, DEBUG_ADDRESS, size); - usb_didtimeout = FALSE; + return usb_64drive_cui_write(datatype, usb_getaddr(), size); } @@ -982,19 +1099,23 @@ static void usb_64drive_write(int datatype, const void* data, int size) static u32 usb_64drive_poll(void) { u32 header; - - // If there's data to service - if (usb_64drive_cui_poll()) + + // Check if the CUI is telling us to read data + if ((usb_io_read(D64_REG_USBCOMSTAT) & D64_CUI_ARM_MASK) == D64_CUI_ARM_UNARMED_DATA) { // Read data to the buffer in 64drive SDRAM memory - header = usb_64drive_cui_read(DEBUG_ADDRESS); - + header = usb_64drive_cui_read(usb_getaddr()); + + // Check if we actually had data + if (header == 0) + return 0; + // Get the data header usb_datatype = USBHEADER_GETTYPE(header); usb_dataleft = USBHEADER_GETSIZE(header); usb_datasize = usb_dataleft; usb_readblock = -1; - + // Return the data header return USBHEADER_CREATE(usb_datatype, usb_datasize); } @@ -1012,7 +1133,7 @@ static u32 usb_64drive_poll(void) static void usb_64drive_read(void) { // Set up DMA transfer between RDRAM and the PI - usb_dma_read(usb_buffer, D64_BASE + DEBUG_ADDRESS + usb_readblock, BUFFER_SIZE); + usb_dma_read(usb_buffer, D64_BASE + usb_getaddr() + usb_readblock, BUFFER_SIZE); } @@ -1103,12 +1224,13 @@ static void usb_everdrive_readusb(void* buffer, int size) usb_everdrive_write Sends data through USB from the EverDrive Will not write if there is data to read from USB - @param The DATATYPE that is being sent - @param A buffer with the data to send - @param The size of the data being sent + @param The DATATYPE that is being sent + @param A buffer with the data to send + @param The size of the data being sent + @return 1 on success, 0 on fail, -1 on timeout ==============================*/ -static void usb_everdrive_write(int datatype, const void* data, int size) +static s8 usb_everdrive_write(int datatype, const void* data, int size) { char wrotecmp = 0; char cmp[] = {'C', 'M', 'P', 'H'}; @@ -1162,7 +1284,7 @@ static void usb_everdrive_write(int datatype, const void* data, int size) if (usb_everdrive_usbbusy()) { usb_didtimeout = TRUE; - return; + return -1; } // Keep track of what we've read so far @@ -1171,6 +1293,7 @@ static void usb_everdrive_write(int datatype, const void* data, int size) offset = 0; } usb_didtimeout = FALSE; + return 1; } @@ -1221,7 +1344,7 @@ static u32 usb_everdrive_poll(void) usb_everdrive_readusb(usb_buffer, bytes_do); // Copy received block to ROM - usb_dma_write(usb_buffer, ED_BASE + DEBUG_ADDRESS + offset, bytes_do); + usb_dma_write(usb_buffer, ED_BASE + usb_getaddr() + offset, bytes_do); offset += bytes_do; len -= bytes_do; } @@ -1253,7 +1376,7 @@ static u32 usb_everdrive_poll(void) static void usb_everdrive_read(void) { // Set up DMA transfer between RDRAM and the PI - usb_dma_read(usb_buffer, ED_BASE + DEBUG_ADDRESS + usb_readblock, BUFFER_SIZE); + usb_dma_read(usb_buffer, ED_BASE + usb_getaddr() + usb_readblock, BUFFER_SIZE); } @@ -1333,15 +1456,16 @@ static u32 usb_sc64_set_writable(u32 enable) /*============================== usb_sc64_write Sends data through USB from the SC64 - @param The DATATYPE that is being sent - @param A buffer with the data to send - @param The size of the data being sent + @param The DATATYPE that is being sent + @param A buffer with the data to send + @param The size of the data being sent + @return 1 on success, 0 on fail, -1 on timeout ==============================*/ -static void usb_sc64_write(int datatype, const void* data, int size) +static s8 usb_sc64_write(int datatype, const void* data, int size) { u32 left = size; - u32 pi_address = SC64_BASE + DEBUG_ADDRESS; + u32 pi_address = SC64_BASE + usb_getaddr(); u32 writable_restore; u32 timeout; u32 args[2]; @@ -1352,7 +1476,7 @@ static void usb_sc64_write(int datatype, const void* data, int size) if (result[0] & SC64_USB_WRITE_STATUS_BUSY) { usb_didtimeout = TRUE; - return; + return -1; } // Enable SDRAM writes and get previous setting @@ -1379,12 +1503,12 @@ static void usb_sc64_write(int datatype, const void* data, int size) usb_sc64_set_writable(writable_restore); // Start sending data from buffer in SDRAM - args[0] = SC64_BASE + DEBUG_ADDRESS; + args[0] = SC64_BASE + usb_getaddr(); args[1] = USBHEADER_CREATE(datatype, size); if (usb_sc64_execute_cmd(SC64_CMD_USB_WRITE, args, NULL)) { usb_didtimeout = TRUE; - return; // Return if USB write was unsuccessful + return 0; // Return if USB write was unsuccessful } // Wait for transfer to end @@ -1395,12 +1519,13 @@ static void usb_sc64_write(int datatype, const void* data, int size) if (usb_timeout_check(timeout, SC64_WRITE_TIMEOUT)) { usb_didtimeout = TRUE; - return; + return -1; } usb_sc64_execute_cmd(SC64_CMD_USB_WRITE_STATUS, NULL, result); } while (result[0] & SC64_USB_WRITE_STATUS_BUSY); usb_didtimeout = FALSE; + return 1; } @@ -1434,7 +1559,7 @@ static u32 usb_sc64_poll(void) usb_readblock = -1; // Start receiving data to buffer in SDRAM - args[0] = SC64_BASE + DEBUG_ADDRESS; + args[0] = SC64_BASE + usb_getaddr(); args[1] = size; if (usb_sc64_execute_cmd(SC64_CMD_USB_READ, args, NULL)) return 0; // Return 0 if USB read was unsuccessful @@ -1459,5 +1584,5 @@ static u32 usb_sc64_poll(void) static void usb_sc64_read(void) { // Set up DMA transfer between RDRAM and the PI - usb_dma_read(usb_buffer, SC64_BASE + DEBUG_ADDRESS + usb_readblock, BUFFER_SIZE); + usb_dma_read(usb_buffer, SC64_BASE + usb_getaddr() + usb_readblock, BUFFER_SIZE); } diff --git a/src/utils.c b/src/utils.c new file mode 100644 index 000000000..51c256095 --- /dev/null +++ b/src/utils.c @@ -0,0 +1,69 @@ +/** + * @file utils.c + * @author Giovanni Bajo + * @brief Misc utilities functions and macros + */ + +#include "utils.h" +#include "debug.h" + +uint32_t __utf8_decode(const char **str) +{ + const uint8_t *s = (const uint8_t*)*str; + uint32_t c = *s++; + if (c < 0x80) { + *str = (const char*)s; + return c; + } + if (c < 0xC0) { + *str = (const char*)s; + return 0xFFFD; + } + if (c < 0xE0) { + c = ((c & 0x1F) << 6) | (*s++ & 0x3F); + *str = (const char*)s; + return c; + } + if (c < 0xF0) { + c = ((c & 0x0F) << 12); c |= ((*s++ & 0x3F) << 6); c |= (*s++ & 0x3F); + *str = (const char*)s; + return c; + } + if (c < 0xF8) { + c = ((c & 0x07) << 18); c |= ((*s++ & 0x3F) << 12); c |= ((*s++ & 0x3F) << 6); c |= (*s++ & 0x3F); + *str = (const char*)s; + return c; + } + *str = (const char*)s; + return 0xFFFD; +} + +uint64_t __read_varint_u64(const uint8_t **ptr) +{ + uint64_t val = 0; + int shift = 0; + while (1) { + uint8_t byte = *(*ptr)++; + val |= (uint64_t)(byte & 0x7F) << shift; + if (!(byte & 0x80)) break; + shift += 7; + assertf(shift < 64, "Varint overflow"); + } + return val; +} + +int64_t __read_varint_s64(const uint8_t **ptr) +{ + uint64_t val = __read_varint_u64(ptr); + return (val >> 1) ^ -(val & 1); +} + +uint64_t __peek_varint_u64(const uint8_t *ptr) +{ + return __read_varint_u64(&ptr); +} + +int64_t __peek_varint_s64(const uint8_t *ptr) +{ + return __read_varint_s64(&ptr); +} diff --git a/src/utils.h b/src/utils.h index 9f8f7a095..f24fde883 100644 --- a/src/utils.h +++ b/src/utils.h @@ -7,14 +7,24 @@ #define __LIBDRAGON_UTILS_H #include // memcpy +#include /** @brief Swap two values */ #define SWAP(a, b) ({ typeof(a) t = a; a = b; b = t; }) +// MAX/MIN/ABS are guarded to avoid conflicts with other headers +#ifndef MAX /** @brief Return the maximum of two values */ #define MAX(a,b) ({ typeof(a) _a = a; typeof(b) _b = b; _a > _b ? _a : _b; }) +#endif +#ifndef MIN /** @brief Return the minimum of two values */ #define MIN(a,b) ({ typeof(a) _a = a; typeof(b) _b = b; _a < _b ? _a : _b; }) +#endif +#ifndef ABS +/** @brief Absolute number */ +#define ABS(x) ({ typeof(x) _x = x; (_x < 0 ? -_x : _x); }) +#endif /** @brief Clamp a value between min and max */ #define CLAMP(x, min, max) (MIN(MAX((x), (min)), (max))) @@ -36,14 +46,12 @@ ((_n) + (_d) - 1) / (_d); \ }) -/** @brief Absolute number */ -#define ABS(x) ({ \ - typeof(x) _x = x; \ - (_x < 0 ? -_x : _x); \ -}) - /** @brief Type-safe bitcast from float to integer */ -#define F2I(f) ({ uint32_t __i; memcpy(&__i, &(f), 4); __i; }) +#define F2I(f) ({ \ + _Static_assert(__builtin_types_compatible_p(typeof(f), float) || \ + __builtin_types_compatible_p(typeof(f), const float), \ + "F2I requires a float argument"); \ + uint32_t __i; memcpy(&__i, &(f), 4); __i; }) /** @brief Type-safe bitcast from integer to float */ #define I2F(i) ({ float __f; memcpy(&__f, &(i), 4); __f; }) @@ -53,7 +61,36 @@ /** @brief Hint for the compiler that the condition is unlikely to happen */ #define UNLIKELY(cond) __builtin_expect(!!(cond), 0) -/** @brief UTF-8 decoding */ +/** + * @brief Decode the next UTF-8 codepoint from a string + * + * @param str Pointer to the UTF-8 string pointer. The pointer will be + * advanced to the next character. + * @return The decoded Unicode codepoint + */ uint32_t __utf8_decode(const char **str); +/** + * @brief Read a variable-length unsigned integer in LEB128 format + * + * @param ptr Pointer to the buffer pointer. The pointer will be + * advanced past the read integer. + */ +uint64_t __read_varint_u64(const uint8_t **ptr); + +/** + * @brief Read a variable-length signed integer in LEB128 format + * + * @param ptr Pointer to the buffer pointer. The pointer will be + * advanced past the read integer. + */ +int64_t __read_varint_s64(const uint8_t **ptr); + +/** @brief Like __read_varint_u64 but does not advance the pointer */ +uint64_t __peek_varint_u64(const uint8_t *ptr); + +/** @brief Like __read_varint_s64 but does not advance the pointer */ +int64_t __peek_varint_s64(const uint8_t *ptr); + + #endif diff --git a/src/vaddr64.c b/src/vaddr64.c new file mode 100644 index 000000000..da0bcee19 --- /dev/null +++ b/src/vaddr64.c @@ -0,0 +1,20 @@ +/** + * @file vaddr64.c + * @author Giovanni Bajo + * @brief 64-bit virtual address space access functions + * @ingroup n64sys + */ + +#include "vaddr64.h" + +/* Inline instantiations */ +/// @cond +extern inline uint8_t sys_vaddr_read8(uint64_t vaddr); +extern inline uint16_t sys_vaddr_read16(uint64_t vaddr); +extern inline uint32_t sys_vaddr_read32(uint64_t vaddr); +extern inline uint64_t sys_vaddr_read64(uint64_t vaddr); +extern inline void sys_vaddr_write8(uint64_t vaddr, uint8_t value); +extern inline void sys_vaddr_write16(uint64_t vaddr, uint16_t value); +extern inline void sys_vaddr_write32(uint64_t vaddr, uint32_t value); +extern inline void sys_vaddr_write64(uint64_t vaddr, uint64_t value); +/// @endcond diff --git a/src/vi.c b/src/vi.c new file mode 100644 index 000000000..51b6992e1 --- /dev/null +++ b/src/vi.c @@ -0,0 +1,968 @@ +/** + * @file vi.c + * @author Giovanni Bajo + * @brief Video Interface Subsystem + * @ingroup display + */ +#include "vi.h" +#include "vi_internal.h" +#include "interrupt.h" +#include "interrupt_internal.h" +#include "surface.h" +#include "n64sys.h" +#include "utils.h" +#include "debug.h" +#include "kernel/kernel_internal.h" +#include "kirq.h" +#include "mi.h" +#include "accounting_internal.h" +#include +#include + +/** @brief Hardware clock values (in Hz) */ +static const int vi_clocks[3] = { + [TV_NTSC] = 48681818, + [TV_PAL] = 49656530, + [TV_MPAL] = 48628322, +}; + +const vi_timing_preset_t VI_TIMING_NTSC = { + .vi_h_total = VI_H_TOTAL_SET(0b00000, 773.5), + .vi_h_total_leap = VI_H_TOTAL_LEAP_SET(773.5, 773.5), + .vi_v_total = VI_V_TOTAL_SET(526), + .vi_burst = VI_BURST_SET(62, 5, 34, 57), + .vi_v_burst = VI_V_BURST_SET(14, 516), + .display = { + .x0 = 108, .y0 = 35, + .width = 640, .height = 480, + }, +}; + +const vi_timing_preset_t VI_TIMING_PAL = { + .vi_h_total = VI_H_TOTAL_SET(0b10101, 794.5), + .vi_h_total_leap = VI_H_TOTAL_LEAP_SET(796.0, 795.75), + .vi_v_total = VI_V_TOTAL_SET(626), + .vi_burst = VI_BURST_SET(64, 4, 35, 58), + .vi_v_burst = VI_V_BURST_SET(9, 619), + .display = { + .x0 = 128, .y0 = 45, + .width = 640, .height = 576, + }, +}; + +const vi_timing_preset_t VI_TIMING_MPAL = { + .vi_h_total = VI_H_TOTAL_SET(0b00000, 772.75), + .vi_h_total_leap = VI_H_TOTAL_LEAP_SET(774.50, 774.50), ///< (774.00, 774.00) is slightly more stable for interlaced over composite + .vi_v_total = VI_V_TOTAL_SET(526), + .vi_burst = VI_BURST_SET(70, 5, 30, 57), + .vi_v_burst = VI_V_BURST_SET(14, 516), + .display = { + .x0 = 108, .y0 = 35, + .width = 640, .height = 480, + }, +}; + +const vi_timing_preset_t VI_TIMING_PAL60 = { + .vi_h_total = VI_H_TOTAL_SET(0b00000, 789), + .vi_h_total_leap = VI_H_TOTAL_LEAP_SET(789, 789), + .vi_v_total = VI_V_TOTAL_SET(526), + .vi_burst = VI_BURST_SET(62, 5, 34, 57), + .vi_v_burst = VI_V_BURST_SET(14, 516), + .display = { + .x0 = 115, .y0 = 35, + .width = 640, .height = 480, + }, +}; + +static const vi_timing_preset_t *default_presets[3] = { + [TV_NTSC] = &VI_TIMING_NTSC, + [TV_PAL] = &VI_TIMING_PAL, + [TV_MPAL] = &VI_TIMING_MPAL, +}; + +/** @brief Line interrupt callbacks */ +typedef struct { + int line; ///< Line number + void (*handler)(void*); ///< Callback function + void *arg; ///< Argument to pass to the callback +} line_irqs_t; + +#define MAX_VBLANK_HANDLERS 4 ///< Maximum number of vblank handlers +#define MAX_LINE_IRQS 16 ///< Maximum number of line interrupts +static line_irqs_t line_irqs[MAX_LINE_IRQS] = {0}; ///< Line interrupt callbacks +static line_irqs_t new_line_irqs[MAX_LINE_IRQS] = {0}; ///< New line interrupt callbacks + +static int8_t vi_initialized = 0; ///< True if the VI subsystem has been initialized +uint32_t __vi_cfg[VI_REGISTERS_COUNT]; ///< Current VI configuration +static const vi_timing_preset_t *preset; ///< Active TV preset +static uint16_t cfg_pending; ///< Pending register changes (1 bit per each VI register) +static uint16_t cfg_raster; ///< Raster register changes (1 bit per each VI register) +static uint16_t cfg_to_validate; ///< Config settings that must be validated +static bool cfg_pending_lineirqs; ///< True if line IRQs have been changed +static volatile int cfg_refcount; ///< Number of active write transactions +static bool blank_mode; ///< True if blank mode is active +static line_irqs_t *cur_line_irq; ///< Current line IRQ pointer +static int interlacing_parms[2]; ///< Interlaced parameters (offsets for ORIGIN, YSCALE) +static line_irqs_t vblank_handlers[MAX_VBLANK_HANDLERS]; ///< Vertical blank handlers + +static void __vi_validate_config(void) +{ + #ifdef NDEBUG + // Disable validation in NDEBUG builds + return; + #endif + + // Accumulate the pending changes to validate + cfg_to_validate |= cfg_pending; + + // If we are blanking or the framebuffer is not set, don't validate the configuration, + // as technically there's nothing wrong right now. + if (blank_mode) + return; + + // Check for a not fully understood bug (see issue #759) + if (cfg_to_validate & (1 << VI_TO_INDEX(VI_WIDTH))) { + uint32_t width = vi_read(VI_WIDTH); + if (width < 8) { + debugf("VI WARNING: setting VI_WIDTH < 8 is known to sometimes crash the VI\n"); + } + } + + // Check for some common mistakes in VI configuration. Since they are based + // on VI_CTRL, VI_X_SCALE and VI_H_VIDEO, do that only if they have been changed. + if ((cfg_to_validate & ((1 << VI_TO_INDEX(VI_CTRL)) | + (1 << VI_TO_INDEX(VI_X_SCALE)) | + (1 << VI_TO_INDEX(VI_H_VIDEO))))) { + + uint32_t ctrl = vi_read(VI_CTRL); + uint32_t xscale = vi_read(VI_X_SCALE); + uint32_t hstart = vi_read(VI_H_VIDEO) >> 16; + bool bpp16 = (ctrl & VI_CTRL_TYPE) == VI_CTRL_TYPE_16_BPP; + bool dedither = ctrl & VI_DEDITHER_FILTER_ENABLE; + bool divot = ctrl & VI_DIVOT_ENABLE; + int mode = ctrl & VI_AA_MODE_MASK; + + switch (mode) { + case VI_AA_MODE_NONE: + if (xscale <= 0x200 && bpp16 && hstart < 128) { + debugf("VI WARNING: setting VI_AA_MODE_NONE with 16 bpp, X_SCALE <= 0x200 and H_START < 128 can cause visual artifacts\n"); + debugf("A common scenario where this happens: NTSC units, with default output area, and 320x240 framebuffer.\n"); + debugf("Possible workarounds: activate resampling with VI_AA_MODE_RESAMPLE, increase X_SCALE\n"); + } + if (divot) + debugf("VI WARNING: divot filter is only useful when the AA filter is enabled\n"); + break; + + case VI_AA_MODE_RESAMPLE: + if (dedither) + debugf("VI WARNING: setting VI_AA_MODE_RESAMPLE with dedithering can cause artifacts\n"); + if (divot) + debugf("VI WARNING: divot filter is only useful when the AA filter is enabled\n"); + break; + + case VI_AA_MODE_RESAMPLE_FETCH_NEEDED: + if (xscale > 0x280) + debugf("VI WARNING: setting VI_AA_MODE_RESAMPLE_FETCH_NEEDED with VI_X_SCALE >= 0x280 (aka: framebuffer widths > 400) can cause artifacts\n"); + break; + + case VI_AA_MODE_RESAMPLE_FETCH_ALWAYS: + if (!bpp16) + debugf("VI WARNING: setting VI_AA_MODE_RESAMPLE_FETCH_ALWAYS with 32 bpp can often cause image corruption\n"); + break; + } + + if (dedither) { + if (!bpp16) + debugf("VI WARNING: dedithering is only useful with 16 bpp\n"); + if (mode != VI_AA_MODE_RESAMPLE_FETCH_ALWAYS && mode != VI_AA_MODE_NONE) + debugf("VI WARNING: dedithering requires VI_AA_MODE_NONE or VI_AA_MODE_RESAMPLE_FETCH_ALWAYS\n"); + } + } + + cfg_to_validate = 0; +} + +static void __vblank_interrupt(void*) +{ + // Call vblank handlers that might change some configuration + for (int i=0; i> 16) & 0x3FF; + int yscale = Y_SCALE & 0xFFFF; + int origin_offset = 0; + + // Serration moves the odd field (field==0) by 1/2 line down. We + // want to counter-adjust this movement by moving the field up. + // We can use the Y_OFFSET to do so and the correct amount is the + // one framebuffer Y advanceent offset, which is YSCALE. So we + // adjust Y_OFFSET by Y_SCALE / 2 + yoffset += yscale >> 1; + + // Y_OFFSET is 0.10 so it can't represent values above 0x3FF (0.9999). + // Luckily, for whole scanlines, we can just move the framebuffer origin. + if (yoffset > 0x3FF) { + int num_lines = yoffset >> 10; + int bpp_shift = (vi_read(VI_CTRL) & VI_CTRL_TYPE) - 1; + origin_offset = (vi_read(VI_WIDTH) * num_lines) << bpp_shift; + yoffset &= 0x3FF; + } + + int new_yscale = (yscale & 0xFFFF) | (yoffset << 16); + interlacing_parms[0] = origin_offset; + interlacing_parms[1] = new_yscale - Y_SCALE; + } + + // Apply interlacing adjustments during field 0. The parameters + // were already calculated so we just need to adjust VI_ORIGIN / VI_YSCALE + uint32_t origin = vi_read(VI_ORIGIN); + uint32_t yscale = vi_read(VI_Y_SCALE); + if (field == 0) { + origin += interlacing_parms[0]; + yscale += interlacing_parms[1]; + } + *VI_ORIGIN = origin; + *VI_Y_SCALE = yscale; + + // Workaround for a PAL-M VI bug on old boards like NUS-CPU-02. + // On those consoles, V_BURST must be changed every field, + // otherwise the image seems garbled at the top. + // It is probably a bug in old revisions of the VI chip, + // since the problem doesn't exist on newer boards. + if (UNLIKELY(get_tv_type() == TV_MPAL)) { + static const uint32_t v_burst_mpal_values[2] = { + VI_V_BURST_SET(11, 514), VI_V_BURST_SET(14, 516), + }; + *VI_V_BURST = v_burst_mpal_values[field]; + } + } + +#ifndef NDEBUG + int line = vi_get_scanline(NULL); + if (line > VI_V_CURRENT_VBLANK + 8) { + debugf("VI WARNING: __vblank_interrupt outside of vblank period: %d\n", line); + } +#endif +} + +/** @brief VI interrupt handler for line interrupts */ +void __vi_interrupt(void) +{ + // Check if there are pending line interrupts to install on this frame. + // We do this only on the beginning of a frame, and only if cfg_refcount + // is 0, so that we are synchronized with other VI changes and we respect + // vi_write_begin(). + if (UNLIKELY(cfg_pending_lineirqs) && cfg_refcount == 0) { + for (int i=0; ihandler; + void *arg = cur_line_irq->arg; + cur_line_irq++; + if (cur_line_irq->line == 0) + cur_line_irq = line_irqs; + *VI_V_INTR = cur_line_irq->line; + handler(arg); +} + +void vi_write_begin(void) +{ + cfg_refcount++; +} + +static void vi_write_maybe_flush(void) +{ + if (cfg_refcount > 0) return; + + // Validate the configuration and emit warnings if needed + __vi_validate_config(); + + // Check if VI is disabled now, and if so, we can apply the changes + // immediately. Notice that this is not just a latency optimization: + // it is mandatory when VI is disabled (VI_CTRL=0, which makes VI_V_CURRENT=0), + // because the VI does not generate interrupts in that case. + uint32_t sr = __disable_interrupts(); + if ((*VI_CTRL & VI_CTRL_TYPE) == VI_CTRL_TYPE_OFF) + __vblank_interrupt(NULL); + __enable_interrupts(sr); +} + +void vi_write_end(void) +{ + assertf(cfg_refcount > 0, "vi_write_end without matching begin"); + --cfg_refcount; + vi_write_maybe_flush(); +} + +void vi_write_end_forced(void) +{ + if (cfg_refcount > 0) { + cfg_refcount = 0; + vi_write_maybe_flush(); + } +} + + +static void vi_write_idx_masked(int reg_idx, uint32_t wmask, uint32_t value) +{ + assert(reg_idx >= 0 && reg_idx < VI_REGISTERS_COUNT); + assert((value & ~wmask) == 0); + disable_interrupts(); + __vi_cfg[reg_idx] = (__vi_cfg[reg_idx] & ~wmask) | value; + cfg_pending |= 1 << reg_idx; + vi_write_maybe_flush(); + enable_interrupts(); +} + +void vi_write_masked(volatile uint32_t *reg, uint32_t wmask, uint32_t value) +{ + vi_write_idx_masked(VI_TO_INDEX(reg), wmask, value); +} + +static void __get_output(int *x0, int *y0, int *x1, int *y1) +{ + uint32_t h_video = vi_read(VI_H_VIDEO); + uint32_t v_video = vi_read(VI_V_VIDEO); + + *x0 = h_video >> 16; *x1 = h_video & 0xFFFF; + *y0 = v_video >> 16; *y1 = v_video & 0xFFFF; +} + +static void __set_output(int x0, int y0, int x1, int y1) +{ + vi_write_begin(); + vi_write(VI_H_VIDEO, VI_H_VIDEO_SET(x0, x1)); + vi_write(VI_V_VIDEO, VI_V_VIDEO_SET(y0, y1)); + vi_write_end(); +} + +void vi_set_origin(void *buffer, int width, int bpp) +{ + assertf(PhysicalAddr(buffer) % 8 == 0, "Invalid VI buffer alignment: %p", buffer); + assertf(bpp == 16 || bpp == 32, "Invalid VI buffer bpp: %d (must be 16 or 32)", bpp); + vi_write_begin(); + vi_write(VI_ORIGIN, PhysicalAddr(buffer)); + vi_write(VI_WIDTH, width); + vi_write_masked(VI_CTRL, VI_CTRL_TYPE, bpp == 16 ? VI_CTRL_TYPE_16_BPP : VI_CTRL_TYPE_32_BPP); + vi_write_end(); +} + +int vi_get_bpp(void) +{ + uint32_t ctrl = vi_read(VI_CTRL) & VI_CTRL_TYPE; + if (ctrl == 2) return 16; + if (ctrl == 3) return 32; + return 0; +} + +void vi_set_xscale(float fb_width) +{ + int x0, y0, x1, y1; + __get_output(&x0, &y0, &x1, &y1); + vi_write_masked(VI_X_SCALE, 0xFFF, VI_X_SCALE_SET(fb_width, x1-x0)); +} + +void vi_set_yscale(float fb_height) +{ + int x0, y0, x1, y1; + __get_output(&x0, &y0, &x1, &y1); + vi_write_masked(VI_Y_SCALE, 0xFFF, VI_Y_SCALE_SET(fb_height, (y1-y0)/2)); +} + +void vi_set_xscale_factor(float xfactor) +{ + int xscale = (int)((xfactor * 1024.0f) + 0.5f); + assertf(xscale >= 0 && xscale <= 0xFFF, "xfactor out of range: %f", xfactor); + vi_write_masked(VI_X_SCALE, 0xFFF, (int)((xfactor * 1024.0f) + 0.5f)); +} + +void vi_set_yscale_factor(float yfactor) +{ + int yscale = (int)((yfactor * 1024.0f) + 0.5f); + assertf(yscale >= 0 && yscale <= 0xFFF, "yfactor out of range: %f", yfactor); + vi_write_masked(VI_Y_SCALE, 0xFFF, yscale); +} + +void vi_show(surface_t *fb) +{ + if (!fb) { + vi_write_begin(); + vi_blank(true); + vi_write(VI_ORIGIN, 0); + vi_write(VI_WIDTH, 8); + vi_write_end(); + return; + } + + tex_format_t fmt = surface_get_format(fb); + assert(fmt == FMT_RGBA16 || fmt == FMT_RGBA32); + int bpp_shift = fmt == FMT_RGBA16 ? 1 : 2; + int bpp = 1 << bpp_shift; + assert(PhysicalAddr(fb->buffer) % 8 == 0); + assert(fb->stride % 8 == 0); + vi_write_begin(); + vi_blank(false); + vi_set_origin(fb->buffer, fb->stride >> bpp_shift, bpp * 8); + vi_set_xscale(fb->width); + vi_set_yscale(fb->height); + vi_write_end(); +} + +void vi_set_interlaced(bool interlaced) +{ + vi_write_begin(); + vi_write_masked(VI_CTRL, VI_CTRL_SERRATE, interlaced ? VI_CTRL_SERRATE : 0); + // Interlaced mode has one additional vertical half-line. Notice that V_TOTAL + // is the total number of scanlines **minus one**, so the logic is reversed. + vi_write_masked(VI_V_TOTAL, 0x1, interlaced ? 0 : 1); + vi_write_end(); +} + +void vi_set_aa_mode(vi_aa_mode_t mode) +{ + vi_write_masked(VI_CTRL, VI_AA_MODE_MASK, mode); +} + +vi_aa_mode_t vi_get_aa_mode(void) +{ + return (vi_aa_mode_t)(vi_read(VI_CTRL) & VI_AA_MODE_MASK); +} + +void vi_set_divot(bool enable) +{ + vi_write_masked(VI_CTRL, VI_DIVOT_ENABLE, enable ? VI_DIVOT_ENABLE : 0); +} + +void vi_set_dedither(bool enable) +{ + vi_write_masked(VI_CTRL, VI_DEDITHER_FILTER_ENABLE, enable ? VI_DEDITHER_FILTER_ENABLE : 0); +} + +bool vi_get_dedither(void) +{ + return (vi_read(VI_CTRL) & VI_DEDITHER_FILTER_ENABLE) != 0; +} + +void vi_set_gamma(vi_gamma_t gamma) +{ + vi_write_masked(VI_CTRL, VI_GAMMA_DITHER_ENABLE, gamma); +} + +float vi_get_refresh_rate(void) +{ + int clock = vi_clocks[get_tv_type()]; + uint32_t HTOTAL = vi_read(VI_H_TOTAL); + uint32_t VTOTAL = vi_read(VI_V_TOTAL); + uint32_t HTOTAL_LEAP = vi_read(VI_H_TOTAL_LEAP); + + int h_total = (HTOTAL & 0xFFF) + 1; + int v_total = (VTOTAL & 0x3FF) + 1; + int h_total_leap_b = (HTOTAL_LEAP >> 0) & 0xFFF; + int h_total_leap_a = (HTOTAL_LEAP >> 16) & 0xFFF; + int leap_bitcount = 0; + int mask = 1<<16; + for (int i=0; i<5; i++) { + if (HTOTAL & mask) leap_bitcount++; + mask <<= 1; + } + int h_total_leap_avg = (h_total_leap_a * leap_bitcount + h_total_leap_b * (5 - leap_bitcount)) / 5; + + return (float)clock / ((h_total * (v_total - 2) / 2 + h_total_leap_avg)); +} + +void vi_get_output(int *x0, int *y0, int *x1, int *y1) +{ + __get_output(x0, y0, x1, y1); +} + +void vi_get_output_bounds(int *x0, int *y0, int *x1, int *y1) +{ + uint32_t H_TOTAL = vi_read(VI_H_TOTAL); + uint32_t V_TOTAL = vi_read(VI_V_TOTAL); + uint32_t BURST = vi_read(VI_BURST); + int burst_start = (BURST >> 20) & 0x3FF; + int vsync_height = (BURST >> 16) & 0xF; + int burst_width = (BURST >> 8) & 0xFF; + int htotal = ((H_TOTAL & 0xFFF) + 1) / 4; + int vtotal = (V_TOTAL & 0x3FF) + 1; + + // On the left, the display area is bound by the color burst. If the display + // area is configured to begin before the color burst is finished, it will + // interrupt it, causing chrominance artifacts. + *x0 = burst_start + burst_width; + + // On the top, the display area is bound by the initial vsync period. Notice + // that the vertical color burst enable/disable + *y0 = vsync_height; + + // On the right/bottom, the display area is bound by the total picture length. + // The bounds are exclusive but V_VIDEO is compared to V_CURRENT to decide + // when to statp the display, and V_CURRENT will never be V_TOTAL (it'll wrap + // around). This means that it's impossible to draw on the last line. + *x1 = htotal; + *y1 = vtotal - 1; +} + +void vi_set_output(int x0, int y0, int x1, int y1) +{ + int bounds[4]; + vi_get_output_bounds(&bounds[0], &bounds[2], &bounds[1], &bounds[3]); + + if (x0 > bounds[1] || x1 < bounds[0] || y0 > bounds[3] || y1 < bounds[2]) { + // If the image is totally out of bounds, just force zero display are + // so that nothing is displayed. + x0 = x1 = y0 = y1 = 0; + } else { + // If the image is partially out of bounds, crop it. + if (x0 < bounds[0]) { + x1 += bounds[0] - x0; + x0 = bounds[0]; + } + if (x1 > bounds[1]) { + x0 -= x1 - bounds[1]; + x1 = bounds[1]; + } + if (y0 < bounds[2]) { + y1 += bounds[2] - y0; + y0 = bounds[2]; + } + if (y1 > bounds[3]) { + y0 -= y1 - bounds[3]; + y1 = bounds[3]; + } + } + + __set_output(x0, y0, x1, y1); +} + +void vi_set_borders(vi_borders_t b) +{ + int x0 = preset->display.x0, x1 = x0 + preset->display.width; + int y0 = preset->display.y0, y1 = y0 + preset->display.height; + + x0 += b.left; + x1 -= b.right; + y0 += b.up; + y1 -= b.down; + + vi_set_output(x0, y0, x1, y1); +} + +vi_borders_t vi_get_borders(void) +{ + int x0, y0, x1, y1; + __get_output(&x0, &y0, &x1, &y1); + + vi_borders_t b; + b.left = +(x0 - preset->display.x0); + b.right = -(x1 - (preset->display.x0 + preset->display.width)); + b.up = +(y0 - preset->display.y0); + b.down = -(y1 - (preset->display.y0 + preset->display.height)); + return b; +} + +float vi_get_aspect_ratio(void) +{ + int vi_width = preset->display.width; + int vi_height = preset->display.height; + + int x0, y0, x1, y1; + __get_output(&x0, &y0, &x1, &y1); + int vis_w = x1 - x0; + int vis_h = y1 - y0; + + float correction = (float)vis_w / (float)vis_h; + const float vi_dar = 4.0f / 3.0f; + float vi_par = (float)vi_width / (float)vi_height; + return correction * vi_dar / vi_par; +} + +vi_borders_t vi_calc_borders(float aspect_ratio, float overscan_margin) +{ + const int vi_width = preset->display.width; + const int vi_height = preset->display.height; + const float vi_par = (float)vi_width / vi_height; + const float vi_dar = 4.0f / 3.0f; + float correction = (aspect_ratio / vi_dar) * vi_par; + + vi_borders_t b; + b.left = b.right = vi_width * overscan_margin; + b.up = b.down = vi_height * overscan_margin; + + int width = vi_width - b.left - b.right; + int height = vi_height - b.up - b.down; + + if (correction > 1) { + int vborders = (int)(height - width / correction + 0.5f); + b.up += vborders / 2; + b.down += vborders / 2; + } else { + int hborders = (int)(width - height * correction + 0.5f); + b.left += hborders / 2; + b.right += hborders / 2; + } + + return b; +} + +void vi_move_output(int x, int y) +{ + int x0, y0, x1, y1; + __get_output(&x0, &y0, &x1, &y1); + + int w = x1 - x0, h = y1 - y0; + vi_write_begin(); + vi_set_output(x, y, x + w, y + h); + vi_write_end(); +} + +void vi_scroll_output(int dx, int dy) +{ + int x0, y0, x1, y1; + __get_output(&x0, &y0, &x1, &y1); + + vi_write_begin(); + x0 += dx; x1 += dx; + y0 += dy; y1 += dy; + vi_set_output(x0, y0, x1, y1); + vi_write_end(); +} + +void vi_blank(bool set_blank) +{ + disable_interrupts(); + blank_mode = set_blank; + if (!blank_mode) { + vi_write(VI_H_VIDEO, vi_read(VI_H_VIDEO)); + vi_write(VI_CTRL, vi_read(VI_CTRL)); + } + enable_interrupts(); +} + +void vi_wait_vblank(void) +{ + // If VI is turned off, just return immediately as there is no proper vblank + uint32_t ctrl = vi_read(VI_CTRL); + if ((ctrl & VI_CTRL_TYPE) == VI_CTRL_TYPE_OFF) + return; + + // For the kernel we can avoid spin-waiting by using 'kirq_wait'. + // However, if VI interrupts are disabled we would wait forever if no other thread turns it on again. + // To avoid hanging up here, fallback to the regular logic + if (__kernel && (*MI_MASK & MI_MASK_VI)) { + kirq_wait_t w = kirq_begin_wait_vi(); + kirq_wait(&w); + return; + } else { + // We define the vblank as the 0->2 current line transition. We can't + // just check if line 2 is current (VI_V_CURRENT_VBLANK) because it + // would cause multiple subsequent calls not to wait the next vblank. + ACCT_SCOPE(ACCT_CAT_VI) { + while (vi_get_scanline(NULL) != VI_V_CURRENT_VBLANK-2) {} + while (vi_get_scanline(NULL) != VI_V_CURRENT_VBLANK) {} + + // To support operating the VI with interrupts disabled, manually call + // our vblank handler here + uint32_t c0_status = C0_STATUS(); + if ((c0_status & C0_STATUS_IE) == 0 || ((c0_status & (C0_STATUS_EXL|C0_STATUS_ERL)) != 0)) { + __vblank_interrupt(NULL); + } + + assert(cfg_pending == 0); + } + } +} + +void vi_stabilize(volatile uint32_t *reg, bool enable) +{ + int idx = VI_TO_INDEX(reg); + if (enable) + cfg_raster |= 1 << idx; + else + cfg_raster &= ~(1 << idx); +} + +void vi_debug_dump(int verbose) +{ + debugf("CTRL:0x%08lx ORIGIN:0x%06lx WIDTH:0x%08lx V_INTR:0x%lx V_CURRENT:0x%lx\n", + vi_read(VI_CTRL), vi_read(VI_ORIGIN), vi_read(VI_WIDTH), vi_read(VI_V_INTR), *VI_V_CURRENT); + debugf("BURST:0x%08lx V_BURST:0x%08lx H_TOTAL:0x%08lx H_TOTAL_LEAP:0x%08lx V_TOTAL:0x%08lx\n", + vi_read(VI_BURST), vi_read(VI_V_BURST), vi_read(VI_H_TOTAL), vi_read(VI_H_TOTAL_LEAP), vi_read(VI_V_TOTAL)); + debugf("H_VIDEO:0x%08lx, V_VIDEO:0x%08lx X_SCALE:0x%08lx Y_SCALE:0x%08lx\n", + vi_read(VI_H_VIDEO), vi_read(VI_V_VIDEO), vi_read(VI_X_SCALE), vi_read(VI_Y_SCALE)); + + debugf("cfg: pending=%x raster=%x to_validate=%x refcount=%x\n", cfg_pending, cfg_raster, cfg_to_validate, cfg_refcount); + + if (verbose == 0) + return; + + uint32_t ctrl = vi_read(VI_CTRL); + const char *ctrl_type[4] = { "off", "invalid", "16-bit", "32-bit"}; + const char *ctrl_aa[4] = { "resample-fetch-always", "resample-fetch-needed", "resample","none" }; + debugf("CTRL: type=%s serrate=%s divot=%s dedither=%s aa=%s\n", + ctrl_type[ctrl & VI_CTRL_TYPE], + ctrl & VI_CTRL_SERRATE ? "on" : "off", + ctrl & VI_DIVOT_ENABLE ? "on" : "off", + ctrl & VI_DEDITHER_FILTER_ENABLE ? "on" : "off", + ctrl_aa[(ctrl & VI_AA_MODE_MASK) >> 8]); + + debugf("VIDEO: H:%ld-%ld V:%ld-%ld\n", vi_read(VI_H_VIDEO) >> 16, vi_read(VI_H_VIDEO) & 0xFFFF, vi_read(VI_V_VIDEO) >> 16, vi_read(VI_V_VIDEO) & 0xFFFF); + + uint32_t x_scale = vi_read(VI_X_SCALE); + uint32_t y_scale = vi_read(VI_Y_SCALE); + debugf("SCALE: X=%.5f (0x%03lx) Y=%.5f (0x%03lx)\n", + (x_scale & 0xFFF) / 1024.0f, x_scale & 0xFFF, + (y_scale & 0xFFF) / 1024.0f, y_scale & 0xFFF); + debugf("OFFSET: X=%.5f (0x%03lx) Y=%0.5f (0x%03lx)\n", + ((x_scale >> 16) & 0xFFF) / 1024.0f, (x_scale >> 16) & 0xFFF, + ((y_scale >> 16) & 0x3FF) / 1024.0f, (y_scale >> 16) & 0x3FF); +} + +void vi_install_vblank_handler(void (*handler)(void *), void *arg) +{ + disable_interrupts(); + for (int i=0; i line) + break; + } + assertf(i < MAX_LINE_IRQS, "Too many line interrupts"); + for (int j=MAX_LINE_IRQS-1; j>i; j--) + new_line_irqs[j] = new_line_irqs[j-1]; + new_line_irqs[i].line = line; + new_line_irqs[i].handler = handler; + new_line_irqs[i].arg = arg; + } else { + // Remove the line interrupt from the interrupt list. + bool removed = false; + for (int i=0; i 0); + assertf(p->vi_v_total % 2 == 1, "invalid VI preset: VI_V_TOTAL must be odd (for progressive scan)"); + + vi_write_begin(); + + preset = p; + + // Configure the timing registers from the preset. These will not change + // at runtime as they are fixed by the TV standard. + vi_write(VI_H_TOTAL, preset->vi_h_total); + vi_write(VI_H_TOTAL_LEAP, preset->vi_h_total_leap); + vi_write(VI_BURST, preset->vi_burst); + vi_write(VI_V_BURST, preset->vi_v_burst); + + // Configure the total number of scanlines, but keep existing bit 0, + // which is the interlaced mode toggle (see #vi_set_interlaced). + vi_write_masked(VI_V_TOTAL, 0xFFFFFFFE, preset->vi_v_total & ~1); + + // Configure the default display area from the preset. + __set_output(preset->display.x0, preset->display.y0, + preset->display.x0 + preset->display.width, + preset->display.y0 + preset->display.height); + + vi_write_end(); +} + +void vi_reset(void) +{ + assert(vi_initialized > 0); + vi_write_begin(); + + // Set the pending mask to all registers, so that the whole + // VI will be programmed at next vblank. + cfg_pending = (1 << VI_REGISTERS_COUNT) - 1; + + // Set the vblank interrupt to the beginning of the vblank period + vi_write(VI_V_INTR, VI_V_CURRENT_VBLANK); + + // Set the timing preset according to the current TV type + vi_set_timing_preset(default_presets[get_tv_type()]); + + // Start in progressive scan mode + vi_set_interlaced(false); + + // Turn on blank mode (disable framebuffer sampling) + vi_write(VI_ORIGIN, 0); + vi_write(VI_WIDTH, 8); + vi_blank(true); + + uint32_t ctrl = 0; + ctrl |= !sys_bbplayer() ? VI_PIXEL_ADVANCE_DEFAULT : VI_PIXEL_ADVANCE_BBPLAYER; + ctrl |= VI_AA_MODE_RESAMPLE; + ctrl |= VI_CTRL_TYPE_16_BPP; // Turn on the VI sync (we're in blank mode) + vi_write(VI_CTRL, ctrl); + + vi_write_end(); +} + +void vi_init(void) +{ + if (vi_initialized++ > 0) { return; } + + memset(&__vi_cfg, 0, sizeof(__vi_cfg)); + cfg_pending = cfg_raster = cfg_to_validate = 0; + cfg_refcount = 0; + blank_mode = false; + cur_line_irq = line_irqs; + + memset(line_irqs, 0, sizeof(line_irqs)); + line_irqs[0].line = VI_V_CURRENT_VBLANK; + line_irqs[0].handler = __vblank_interrupt; + + // Reset the VI to its default state. + vi_reset(); + + disable_interrupts(); + register_VI_handler(__vi_interrupt); + set_VI_interrupt(1, vi_read(VI_V_INTR)); + enable_interrupts(); +} + +void vi_close(void) +{ + assert(vi_initialized > 0); + if (--vi_initialized > 0) { return; } + + // Wait until vblank, then disable interrupts. +wait_vblank: + vi_wait_vblank(); + disable_interrupts(); + + // Make sure we're still in vblank, in the unlikely event we were + // preempted before disabling interrupts. + if (UNLIKELY(vi_get_scanline(NULL) != VI_V_CURRENT_VBLANK)) { + enable_interrupts(); + goto wait_vblank; + } + + // Shut down VI by writing 0 to all registers. VI_CTRL=0 is what really + // turns off the VI: + for (int i=0; i - * @brief Video Interface Subsystem - * @ingroup display - */ -#ifndef __LIBDRAGON_VI_H -#define __LIBDRAGON_VI_H - -#include -#include -#include "regsinternal.h" -#include "n64sys.h" - -/** - * @addtogroup display - * @{ - */ - -/** @brief Register uncached location in memory of VI */ -#define VI_REGISTERS_ADDR 0xA4400000 -/** @brief Number of useful 32-bit registers at the register base */ -#define VI_REGISTERS_COUNT 14 - -/** - * @brief Video Interface register structure - * - * Whenever trying to configure VI registers, - * this struct and its index definitions below can be very useful - * in writing comprehensive and verbose code. - */ -typedef struct vi_config_s{ - uint32_t regs[VI_REGISTERS_COUNT]; -} vi_config_t; - -/** @brief Base pointer to hardware Video interface registers that control various aspects of VI configuration. - * Shouldn't be used by itself, use VI_ registers to get/set their values. */ -#define VI_REGISTERS ((volatile uint32_t*)VI_REGISTERS_ADDR) -/** @brief VI Index register of controlling general display filters/bitdepth configuration */ -#define VI_CTRL (&VI_REGISTERS[0]) -/** @brief VI Index register of RDRAM base address of the video output Frame Buffer. This can be changed as needed to implement double or triple buffering. */ -#define VI_ORIGIN (&VI_REGISTERS[1]) -/** @brief VI Index register of width in pixels of the frame buffer. */ -#define VI_WIDTH (&VI_REGISTERS[2]) -/** @brief VI Index register of vertical interrupt. */ -#define VI_V_INTR (&VI_REGISTERS[3]) -/** @brief VI Index register of the current half line, sampled once per line. */ -#define VI_V_CURRENT (&VI_REGISTERS[4]) -/** @brief VI Index register of sync/burst values */ -#define VI_BURST (&VI_REGISTERS[5]) -/** @brief VI Index register of total visible and non-visible lines. - * This should match either NTSC (non-interlaced: 0x20D, interlaced: 0x20C) or PAL (non-interlaced: 0x271, interlaced: 0x270) */ -#define VI_V_SYNC (&VI_REGISTERS[6]) -/** @brief VI Index register of total width of a line */ -#define VI_H_SYNC (&VI_REGISTERS[7]) -/** @brief VI Index register of an alternate scanline length for one scanline during vsync. */ -#define VI_H_SYNC_LEAP (&VI_REGISTERS[8]) -/** @brief VI Index register of start/end of the active video image, in screen pixels */ -#define VI_H_VIDEO (&VI_REGISTERS[9]) -/** @brief VI Index register of start/end of the active video image, in screen half-lines. */ -#define VI_V_VIDEO (&VI_REGISTERS[10]) -/** @brief VI Index register of start/end of the color burst enable, in half-lines. */ -#define VI_V_BURST (&VI_REGISTERS[11]) -/** @brief VI Index register of horizontal subpixel offset and 1/horizontal scale up factor. */ -#define VI_X_SCALE (&VI_REGISTERS[12]) -/** @brief VI Index register of vertical subpixel offset and 1/vertical scale up factor. */ -#define VI_Y_SCALE (&VI_REGISTERS[13]) - -/** @brief VI register by index (0-13)*/ -#define VI_TO_REGISTER(index) (((index) >= 0 && (index) <= VI_REGISTERS_COUNT)? &VI_REGISTERS[index] : NULL) - -/** @brief VI index from register */ -#define VI_TO_INDEX(reg) ((reg) - VI_REGISTERS) - -/** - * @name Video Mode Register Presets - * @brief Presets to begin with when setting a particular video mode - * @{ - */ -static const vi_config_t vi_ntsc_p = {.regs = { - 0x00000000, 0x00000000, 0x00000000, 0x00000002, - 0x00000000, 0x03e52239, 0x0000020d, 0x00000c15, - 0x0c150c15, 0x006c02ec, 0x002501ff, 0x000e0204, - 0x00000000, 0x00000000 }}; -static const vi_config_t vi_pal_p = {.regs = { - 0x00000000, 0x00000000, 0x00000000, 0x00000002, - 0x00000000, 0x0404233a, 0x00000271, 0x00150c69, - 0x0c6f0c6e, 0x00800300, 0x005f0239, 0x0009026b, - 0x00000000, 0x00000000 }}; -static const vi_config_t vi_mpal_p = {.regs = { - 0x00000000, 0x00000000, 0x00000000, 0x00000002, - 0x00000000, 0x04651e39, 0x0000020d, 0x00040c11, - 0x0c190c1a, 0x006c02ec, 0x002501ff, 0x000e0204, - 0x00000000, 0x00000000 }}; -static const vi_config_t vi_ntsc_i = {.regs = { - 0x00000000, 0x00000000, 0x00000000, 0x00000002, - 0x00000000, 0x03e52239, 0x0000020c, 0x00000c15, - 0x0c150c15, 0x006c02ec, 0x002301fd, 0x000e0204, - 0x00000000, 0x00000000 }}; -static const vi_config_t vi_pal_i = {.regs = { - 0x00000000, 0x00000000, 0x00000000, 0x00000002, - 0x00000000, 0x0404233a, 0x00000270, 0x00150c69, - 0x0c6f0c6e, 0x00800300, 0x005d0237, 0x0009026b, - 0x00000000, 0x00000000 }}; -static const vi_config_t vi_mpal_i = {.regs = { - 0x00000000, 0x00000000, 0x00000000, 0x00000002, - 0x00000000, 0x04651e39, 0x0000020c, 0x00000c10, - 0x0c1c0c1c, 0x006c02ec, 0x002301fd, 0x000b0202, - 0x00000000, 0x00000000 }}; -/** @} */ - -/** @brief Register initial value array */ -static const vi_config_t vi_config_presets[2][3] = { - {vi_pal_p, vi_ntsc_p, vi_mpal_p}, - {vi_pal_i, vi_ntsc_i, vi_mpal_i} -}; - -/** Under VI_CTRL */ - -/** @brief VI_CTRL Register setting: enable dedither filter. */ -#define VI_DEDITHER_FILTER_ENABLE (1<<16) -/** @brief VI_CTRL Register setting: default value for pixel advance. */ -#define VI_PIXEL_ADVANCE_DEFAULT (0b0011 << 12) -/** @brief VI_CTRL Register setting: default value for pixel advance on iQue. */ -#define VI_PIXEL_ADVANCE_BBPLAYER (0b0001 << 12) -/** @brief VI_CTRL Register setting: disable AA / resamp. */ -#define VI_AA_MODE_NONE (0b11 << 8) -/** @brief VI_CTRL Register setting: disable AA / enable resamp. */ -#define VI_AA_MODE_RESAMPLE (0b10 << 8) -/** @brief VI_CTRL Register setting: enable AA / enable resamp, fetch pixels when needed. */ -#define VI_AA_MODE_RESAMPLE_FETCH_NEEDED (0b01 << 8) -/** @brief VI_CTRL Register setting: enable AA / enable resamp, fetch pixels always. */ -#define VI_AA_MODE_RESAMPLE_FETCH_ALWAYS (0b00 << 8) -/** @brief VI_CTRL Register setting: enable interlaced output. */ -#define VI_CTRL_SERRATE (1<<6) -/** @brief VI_CTRL Register setting: enable divot filter (fixes 1 pixel holes after AA). */ -#define VI_DIVOT_ENABLE (1<<4) -/** @brief VI_CTRL Register setting: enable gamma correction filter. */ -#define VI_GAMMA_ENABLE (1<<3) -/** @brief VI_CTRL Register setting: enable gamma correction filter and hardware dither the least significant color bit on output. */ -#define VI_GAMMA_DITHER_ENABLE (1<<2) -/** @brief VI_CTRL Register setting: framebuffer source format */ -#define VI_CTRL_TYPE (0b11) -/** @brief VI_CTRL Register setting: set the framebuffer source as 32-bit. */ -#define VI_CTRL_TYPE_32_BPP (0b11) -/** @brief VI_CTRL Register setting: set the framebuffer source as 16-bit (5-5-5-3). */ -#define VI_CTRL_TYPE_16_BPP (0b10) -/** @brief VI_CTRL Register setting: set the framebuffer source as blank (no data and no sync, TV screens will either show static or nothing). */ -#define VI_CTRL_TYPE_BLANK (0b00) - -/** Under VI_ORIGIN */ -/** @brief VI_ORIGIN Register: set the address of a framebuffer. */ -#define VI_ORIGIN_SET(value) ((value & 0xFFFFFF) << 0) - -/** Under VI_WIDTH */ -/** @brief VI_ORIGIN Register: set the width of a framebuffer. */ -#define VI_WIDTH_SET(value) ((value & 0xFFF) << 0) - -/** Under VI_V_CURRENT */ -/** @brief VI_V_CURRENT Register: default value for vblank begin line. */ -#define VI_V_CURRENT_VBLANK 2 - -/** Under VI_V_INTR */ -/** @brief VI_V_INTR Register: set value for vertical interrupt. */ -#define VI_V_INTR_SET(value) ((value & 0x3FF) << 0) -/** @brief VI_V_INTR Register: default value for vertical interrupt. */ -#define VI_V_INTR_DEFAULT 0x3FF - -/** Under VI_BURST */ -/** @brief VI_BURST Register: set start of color burst in pixels from hsync. */ -#define VI_BURST_START(value) ((value & 0x3F) << 20) -/** @brief VI_BURST Register: set vertical sync width in half lines. */ -#define VI_VSYNC_WIDTH(value) ((value & 0x7) << 16) -/** @brief VI_BURST Register: set color burst width in pixels. */ -#define VI_BURST_WIDTH(value) ((value & 0xFF) << 8) -/** @brief VI_BURST Register: set horizontal sync width in pixels. */ -#define VI_HSYNC_WIDTH(value) ((value & 0xFF) << 0) - -/** @brief VI_BURST Register: NTSC default start of color burst in pixels from hsync. */ -#define VI_BURST_START_NTSC 62 -/** @brief VI_BURST Register: NTSC default vertical sync width in half lines. */ -#define VI_VSYNC_WIDTH_NTSC 5 -/** @brief VI_BURST Register: NTSC default color burst width in pixels. */ -#define VI_BURST_WIDTH_NTSC 34 -/** @brief VI_BURST Register: NTSC default horizontal sync width in pixels. */ -#define VI_HSYNC_WIDTH_NTSC 57 - -/** @brief VI_BURST Register: PAL default start of color burst in pixels from hsync. */ -#define VI_BURST_START_PAL 64 -/** @brief VI_BURST Register: PAL default vertical sync width in half lines. */ -#define VI_VSYNC_WIDTH_PAL 4 -/** @brief VI_BURST Register: PAL default color burst width in pixels. */ -#define VI_BURST_WIDTH_PAL 35 -/** @brief VI_BURST Register: PAL default horizontal sync width in pixels. */ -#define VI_HSYNC_WIDTH_PAL 58 - -/** Under VI_X_SCALE */ -/** @brief VI_X_SCALE Register: set 1/horizontal scale up factor (value is converted to 2.10 format) */ -#define VI_X_SCALE_SET(value) (( 1024*(value) + 320 ) / 640) - -/** Under VI_Y_SCALE */ -/** @brief VI_Y_SCALE Register: set 1/vertical scale up factor (value is converted to 2.10 format) */ -#define VI_Y_SCALE_SET(value) (( 1024*(value) + 120 ) / 240) - -/** - * @brief Write a set of video registers to the VI - * - * @param[in] reg - * A pointer to a register to be written - * @param[in] value - * Value to be written to the register - */ -inline void vi_write_safe(volatile uint32_t *reg, uint32_t value){ - assert(reg); /* This should never happen */ - *reg = value; - MEMORY_BARRIER(); -} - -/** - * @brief Write a set of video registers to the VI - * - * @param[in] config - * A pointer to a set of register values to be written - */ -inline void vi_write_config(const vi_config_t* config) -{ - /* This should never happen */ - assert(config); - - /* Just straight copy */ - for( int i = 0; i < VI_REGISTERS_COUNT; i++ ) - { - /* Don't clear interrupts */ - if( i == 3 ) { continue; } - if( i == 4 ) { continue; } - - *VI_TO_REGISTER(i) = config->regs[i]; - MEMORY_BARRIER(); - } -} - -/** - * @brief Update the framebuffer pointer in the VI - * - * @param[in] dram_val - * The new framebuffer to use for display. Should be aligned and uncached. - */ -static inline void vi_write_dram_register( void const * const dram_val ) -{ - *VI_ORIGIN = VI_ORIGIN_SET(PhysicalAddr(dram_val)); - MEMORY_BARRIER(); -} - -/** @brief Wait until entering the vblank period */ -static inline void vi_wait_for_vblank() -{ - while(*VI_V_CURRENT != VI_V_CURRENT_VBLANK ) { } -} - -/** @brief Return true if VI is sending a video signal (16-bit or 32-bit color set) */ -static inline bool vi_is_active() -{ - return (*VI_CTRL & VI_CTRL_TYPE) != VI_CTRL_TYPE_BLANK; -} - -/** @brief Set active image width to 0, which keeps VI signal active but only sending a blank image */ -static inline void vi_set_blank_image() -{ - vi_write_safe(VI_H_VIDEO, 0); -} - - -#ifdef __cplusplus -extern "C" { -#endif - - -#ifdef __cplusplus -} -#endif - -/** @} */ /* vi */ - -#endif diff --git a/src/vi_internal.h b/src/vi_internal.h new file mode 100644 index 000000000..186aaaebe --- /dev/null +++ b/src/vi_internal.h @@ -0,0 +1,64 @@ +/** + * @file vi_internal.h + * @author SpookyIluha + * @author Giovanni Bajo + */ +#ifndef __LIBDRAGON_VI_INTERNAL_H +#define __LIBDRAGON_VI_INTERNAL_H + +#include "vi.h" + +/** + * @brief Install a custom line handler + * + * This is an advanced function that installs a custom callback that will be + * run at the beginning of a specific screen line. + * + * The line number is sometimes called "half-line": it is a *even* value between + * 0 and 524 (or 624 on PAL), which can be thought of as two times the actual TV + * scanline (which is 262 or 312 on PAL). If you pass an odd value, it'll be + * rounded to even automatically. It is the same reference system used by other + * VI registers and functions such as #vi_set_output or #vi_get_scanline. + * + * The main usage of this function is to implement raster effects, that is, + * changing VI configuration at the beginning of a specific raster line. This + * is extremely finnicky and timing sensitive: you need to be sure to modify + * the VI before the horizontal blank period is finished, and that period + * is extremely short. So normally, you'll want to install the handler the + * line before, spin-wait in the handler until the next line begins, and + * only then directly write to the VI registers. In this case, you can also use + * #vi_stabilize to ensure that registers are automatically reset at the + * beginning of a frame, if needed. + * + * @note Do *NOT* use this function to implement a frame buffer switch. If you + * just want to switch framebuffers at the beginning of a frame, use + * #vi_install_vblank_handler instead, as it is far less time-sensitive, + * and also more efficient. + * + * @param line Line number to install the handler for. Use 0 for vblank. + * Use -1 to remove the couple handler+arg from all lines + * they were installed for. + * @param handler Function to call at the beginning of the line + * @param arg Argument to pass to the handler + */ +void vi_set_line_interrupt(int line, void (*handler)(void*), void *arg); + +/** + * @brief Force the end of any batched VI calls + * + * This can be useful in emergency situations to unblock the VI. For instance, + * an assertion that triggers within vi_write_begin() can be unblocked by this + * call. + */ +void vi_write_end_forced(void); + +#ifdef __cplusplus +extern "C" { +#endif + + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/video/fastcache.h b/src/video/fastcache.h new file mode 100644 index 000000000..bae656ceb --- /dev/null +++ b/src/video/fastcache.h @@ -0,0 +1,55 @@ +/** + * @file fastcache.h + * @author Giovanni Bajo + */ +#ifndef FASTCACHE_H +#define FASTCACHE_H + +// Fast cache flush/invalidate ops. + +// This is similar to libdragon's functions, but it generates very fast +// inlined code, with a noticeable performance difference especially +// when the size of the data to flush is known at compile-time. + +// Notice that, in general, these functions try to be cautious, assume +// unaligned addresses, and make sure to touch enough cachelines to honor +// the request under all circumstances. For instance, when asking to flush +// 2 bytes, 2 cachelines will be flushed, because in theory the two bytes could +// sit in two different cachelines. + +#define cache_op(op, addr_, length_) ({ \ + void *addr=(void*)(((unsigned long)(addr_)&(~3))); \ + int length = (length_)+16; \ + for (;length>64;length-=64,addr+=64) { \ + asm ("\tcache %0,0(%1)\n"::"i" (op), "r" (addr)); \ + asm ("\tcache %0,16(%1)\n"::"i" (op), "r" (addr)); \ + asm ("\tcache %0,32(%1)\n"::"i" (op), "r" (addr)); \ + asm ("\tcache %0,48(%1)\n"::"i" (op), "r" (addr)); \ + } \ + /* In case length is know at compile-time and there are no more than 4 */ \ + /* cachelines to flush, generate manually-unrolled cache instructions. */ \ + if (__builtin_constant_p(length_)) { \ + if (length > 0 ) asm ("\tcache %0, 0(%1)\n"::"i" (op), "r" (addr)); \ + if (length > 16) asm ("\tcache %0,16(%1)\n"::"i" (op), "r" (addr)); \ + if (length > 32) asm ("\tcache %0,32(%1)\n"::"i" (op), "r" (addr)); \ + if (length > 48) asm ("\tcache %0,48(%1)\n"::"i" (op), "r" (addr)); \ + } else { \ + for (;length>=0;length-=16,addr+=16) { \ + asm ("\tcache %0,0(%1)\n"::"i" (op), "r" (addr)); \ + } \ + } \ +}) + +#define fast_data_cache_hit_writeback(mem, len) ({ \ + cache_op(0x19, mem, len); \ +}) + +#define fast_data_cache_hit_writeback_invalidate(mem, len) ({ \ + cache_op(0x15, mem, len); \ +}) + +#define fast_data_cache_hit_invalidate(mem, len) ({ \ + cache_op(0x11, mem, len); \ +}) + +#endif diff --git a/src/video/fmv.c b/src/video/fmv.c new file mode 100644 index 000000000..a98516ba3 --- /dev/null +++ b/src/video/fmv.c @@ -0,0 +1,276 @@ +/** + * @file fmv.c + * @author Giovanni Bajo + * @brief Full-motion video playback (high-level API) + */ + +#include "fmv.h" +#include "n64sys.h" +#include "video.h" +#include "video_sync.h" +#include "yuv.h" +#include "display.h" +#include "vi.h" +#include "wav64.h" +#include "mixer.h" +#include "subtitles.h" +#include "graphics.h" +#include "rdpq.h" +#include "rdpq_attach.h" +#include + +void fmv_play(const char *video_fn, const fmv_parms_t *parms) +{ + int buffered_pics = (parms && parms->decode_ahead_pics) + ? parms->decode_ahead_pics + : (is_memory_expanded() ? 8 : 4); + video_t *video = video_open(video_fn, &(video_parms_t){ .buffered_pics = buffered_pics }); + video_info_t info = video_get_info(video); + if (!parms) { + parms = alloca(sizeof(fmv_parms_t)); + memset((void*)parms, 0, sizeof(fmv_parms_t)); + } + + if (!parms->disable_display_init) { + display_init((resolution_t){ + .width = info.width, + .height = info.height, + .aspect_ratio = info.aspect_ratio, + .overscan_margin = parms->crt_margin ? VI_CRT_MARGIN : 0, + }, DEPTH_32_BPP, 2, GAMMA_NONE, FILTERS_RESAMPLE); + } + + bool vi_16bpp = display_get_bitdepth() == 2; + + yuv_init(); + yuv_blitter_t yuv; + if (parms->create_yuv_blitter) { + yuv = parms->create_yuv_blitter(parms->osd_ctx, &info); + } else { + yuv = yuv_blitter_new_fmv( + info.width, info.height, + display_get_width(), display_get_height(), + &(yuv_fmv_parms_t){ + .cs = &info.colorspace, + .video_aspect_ratio = info.aspect_ratio, + .display_aspect_ratio = vi_get_aspect_ratio(), + .enable_dithering = vi_16bpp, // YUV blitter will dither in 16bpp mode + } + ); + } + + // Engage the fps limiter to ensure proper video pacing. + display_set_fps_limit(info.framerate); + + // Open audio if not disabled + wav64_t* audio = NULL; + if (!parms->disable_audio) { + const char *audio_fn = parms->audio_fn; + if (!audio_fn) { + // Try to find audio file with same name but .wav64 extension + const char *ext = strrchr(video_fn, '.'); + assertf(ext, "Audio filename not specified for video playback"); + + // Create audio filename + audio_fn = alloca(strlen(video_fn) + 7); + size_t base_len = ext - video_fn; + strncpy((char*)audio_fn, video_fn, base_len); + strcpy((char*)audio_fn + base_len, ".wav64"); + } + + // Load audio + struct stat st; + if (stat(audio_fn, &st) == 0) { + audio = wav64_load(audio_fn, NULL); + } + } + + subtitles_t* subs = NULL; + subrenderer_t* subrenderer = parms->sub_renderer; + if (!parms->disable_subtitles) { + // Open subtitles + const char *subs_fn = parms->subtitles_fn; + if (!subs_fn) { + // Try to find subtitle file with same name but .sub64 extension + const char *ext = strrchr(video_fn, '.'); + assertf(ext, "Subtitle filename not specified for video playback"); + + // Create subtitle filename + subs_fn = alloca(strlen(video_fn) + 7); + size_t base_len = ext - video_fn; + strncpy((char*)subs_fn, video_fn, base_len); + strcpy((char*)subs_fn + base_len, ".sub64"); + } + + struct stat st; + if (stat(subs_fn, &st) == 0) { + // Load subtitles + subs = subtitles_load(subs_fn); + + // Create a default subtitle renderer if none provided + if (!subrenderer) + subrenderer = subrenderer_create_rdpq( + &(subrenderer_rdpq_parms_t){ + .bkg_color = RGBA32(0,0,0,128) + } + ); + } + } + + int frame_idx = 0; + bool paused = false; + bool abort = false; + video_sync_t *vsync = NULL; + + void ctrl_pause(fmv_control_t *ctrl, bool pause) { + paused = pause; + } + void ctrl_stop(fmv_control_t *ctrl) { + abort = true; + } + int ctrl_seek_frame(fmv_control_t *ctrl, int idx, bool exact) { + int new_idx = video_seek(video, idx); + if (new_idx < 0) return -1; // seeking not supported + frame_idx = new_idx; + if (exact) { + // Decode frames until we reach the exact frame + while (frame_idx < idx) { + if (!video_next_frame(video)) break; + frame_idx++; + } + } + // Sync also audio + if (audio) { + double time_sec = (double)frame_idx / (double)info.framerate; + wav64_seek(audio, parms->audio_mixer_channel, time_sec); + } + if (subs) { + subtitles_seek(subs, frame_idx); + } + if (vsync) { + video_sync_reset(vsync, frame_idx); + } + return frame_idx; + } + float ctrl_seek_time(fmv_control_t *ctrl, float time_sec, bool exact) { + int frame_idx = (int)(time_sec * info.framerate); + frame_idx = ctrl_seek_frame(ctrl, frame_idx, exact); + if (frame_idx < 0) return -1; // seeking not supported + return (float)frame_idx / info.framerate; + } + + // Prepare control structure for OSD callback + fmv_control_t ctrl = { + .video = video, + .audio = audio, + .subs = subs, + .pause = ctrl_pause, + .stop = ctrl_stop, + .seek_frame = ctrl_seek_frame, + .seek_time = ctrl_seek_time, + }; + + if (audio) { + mixer_ch_play(parms->audio_mixer_channel, &audio->wave); + + if (!parms->disable_frame_skipping) { + vsync = video_sync_create(video, NULL); + } + } + + while (!abort) { + bool skip_render = false; + + mixer_try_play(); + + if (!paused) { + if (vsync && mixer_ch_playing(parms->audio_mixer_channel)) { + // Decide what to do next based on the sync controller + double master_time_sec = mixer_ch_get_pos(parms->audio_mixer_channel) / (double)audio->wave.frequency; + video_sync_action_t a = video_sync_step(vsync, master_time_sec, frame_idx); + + if (a.kind == VIDEO_SYNC_SKIP_NEXT) { + // Skip rendering this frame + skip_render = true; + } else if (a.kind == VIDEO_SYNC_SEEK_AND_RENDER) { + // Seek to the target frame and render it. + // Also seek subtitles to the target frame. + int new_idx = video_seek(video, a.seek_frame); + if (new_idx >= 0) { + frame_idx = new_idx; + if (subs) subtitles_seek(subs, frame_idx); + } + } + } + + if (!video_next_frame(video)) { + if (parms->loop) { + frame_idx = 0; + video_rewind(video); + if (audio) wav64_seek(audio, parms->audio_mixer_channel, 0.0); + if (subs) subtitles_seek(subs, 0); + if (vsync) video_sync_reset(vsync, frame_idx); + continue; + } else { + break; + } + } + + if (subs) subtitles_next_frame(subs); + } + + mixer_try_play(); + + if (!skip_render) { + surface_t *disp = display_try_get(); + while (disp == NULL) { + if (!paused && !video_poll(video)) { + disp = display_get(); + break; + } + disp = display_try_get(); + } + + rdpq_attach(disp, NULL); + + // Get the next video frame and feed it into our previously set up blitter. + yuv_frame_t frame = video_get_frame(video); + yuv_blitter_run(&yuv, &frame); + + // Draw subtitles + if (subs) { + subtitle_cue_t cues[3]; + int num_cues = subtitles_get_current_cues(subs, cues, 3); + subrenderer_render(subrenderer, cues, num_cues); + } + + // Call OSD callback if provided + if (parms->osd_callback) { + float time_sec = frame_idx / info.framerate; + parms->osd_callback(parms->osd_ctx, frame_idx, time_sec, &ctrl); + } + + rdpq_detach_show(); + } + + if (!paused) + frame_idx++; + } + + rspq_wait(); + + if (subrenderer) subrenderer_free(subrenderer); + if (audio) { + mixer_ch_stop(parms->audio_mixer_channel); + wav64_close(audio); + } + + yuv_blitter_free(&yuv); + yuv_close(); + if (vsync) video_sync_destroy(vsync); + video_close(video); + display_set_fps_limit(0); + + if (!parms->disable_display_init) + display_close(); +} diff --git a/src/video/h264.c b/src/video/h264.c new file mode 100644 index 000000000..1ef3031d6 --- /dev/null +++ b/src/video/h264.c @@ -0,0 +1,487 @@ +/** + * @file h264.c + * @author Giovanni Bajo + * @brief H264 video player using RSP acceleration + */ + +#include "h264.h" +#include "h264_decoder.h" +#include "rsph264_internal.h" +#include "asset_internal.h" +#include "video_internal.h" +#include "n64sys.h" +#include "profile.h" +#include "yuv.h" +#include "surface.h" +#include "rspq.h" +#include "debug.h" +#include "utils.h" +#include +#include + +// The size of the internal I/O buffer in the H264 decoder. This must be +// at least the maximum slice size advertised by videoconv64 metadata. +#define H264_BUF_SIZE (64*1024) + +#define H264BSD_EOF (-1) + +// Per-frame log of the RSP DMA traffic and of the overlay switches emitted by +// the H264 command stream, enabled by RSPH264_PROFILE (see rsph264_internal.h). +// The DMA figures need an EMUX-capable emulator; the command counters work +// anywhere. +#if RSPH264_PROFILE +#include "emux.h" + +static struct { + bool available; ///< Whether the emux profiler answered + int frame; ///< Number of frames logged so far + uint64_t dma_bytes; ///< Counters sampled at the previous frame boundary + uint64_t rsp_cycles; + uint64_t rsp_idle; +} bwprof; + +static void bwprof_sample(uint64_t *dma, uint64_t *cycles, uint64_t *idle) +{ + if (!bwprof.available) { + *dma = *cycles = *idle = 0; + return; + } + *dma = emux_prof_read(-1, EMUX_PROF_RAM_RSPDMA_BYTES); + *cycles = emux_prof_read(-1, EMUX_PROF_RSP_CYCLES); + *idle = emux_prof_read(-1, EMUX_PROF_RSP_IDLE); +} + +static void bwprof_init(void) +{ + bwprof.available = (emux_detect(1) & EMUX_FEAT1_PROFILER) != 0; + debugf("h264: bandwidth profiling on (emux profiler: %s)\n", + bwprof.available ? "yes" : "NO, DMA figures unavailable"); + debugf("h264: overlay switch costs %lu bytes of DMA\n", + rsph264_measure_overlay_switch_cost()); + bwprof_sample(&bwprof.dma_bytes, &bwprof.rsp_cycles, &bwprof.rsp_idle); + rsph264_stats_reset(); +} + +static void bwprof_frame_end(void) +{ + // The RSP runs behind the CPU, so drain it before sampling: otherwise this + // frame's DMA traffic would land in the next frame's window. + rspq_wait(); + + uint64_t dma, cycles, idle; + bwprof_sample(&dma, &cycles, &idle); + + rsph264_stats_t st; + rsph264_stats_get(&st); + + uint64_t elapsed = cycles - bwprof.rsp_cycles; + uint64_t busy = elapsed - (idle - bwprof.rsp_idle); + + debugf("h264 frame %d: cmds %lu=%lu+%lu, sw %lu=%lu+%lu, rspdma %u B, rsp busy %u/%u kcyc\n", + bwprof.frame, + st.commands, st.cmds[RSPH264_STAT_INTER], st.cmds[RSPH264_STAT_INTRA], + st.ovl_switches, + st.switches_to[RSPH264_STAT_INTER], st.switches_to[RSPH264_STAT_INTRA], + (unsigned)(dma - bwprof.dma_bytes), + (unsigned)(busy / 1000), (unsigned)(elapsed / 1000)); + + bwprof.frame++; + bwprof.dma_bytes = dma; + bwprof.rsp_cycles = cycles; + bwprof.rsp_idle = idle; + rsph264_stats_reset(); +} +#else +#define bwprof_init() ((void)0) +#define bwprof_frame_end() ((void)0) +#endif + +static const uint8_t H264_LD_BUFFER_UUID[16] = { + 'L', 'I', 'B', 'D', 'R', 'A', 'G', 'O', 'N', 0, 0, 0, 0, 0, 0, 0, +}; + +typedef struct h264_s { + video_t video; ///< Video handle + uint8_t *pic; ///< Pointer to the decoded picture data + int idx; ///< Current index in the buffer + int buf_len; ///< Number of valid bytes in the buffer + int fd; ///< File descriptor of the opened H264 file + bool in_frame_decoding; ///< True if we have partially decoded a frame + uint32_t max_slice_size; ///< Maximum size of a slice seen so far + bool has_slice_metadata; ///< True if the stream advertised max_slice_size + int max_buffered_pics; ///< Runtime-configured buffered pictures + storage_t s; ///< H264 decoder main structure + uint8_t buf[]; ///< Internal buffered data +} h264_t; + +static void h264_sei_callback(void *ctx, u32 payload_type, const u8 *payload, u32 payload_size) { + h264_t *player = (h264_t*)ctx; + if (payload_type != 5 || payload_size < 25) + return; + if (memcmp(payload, H264_LD_BUFFER_UUID, sizeof(H264_LD_BUFFER_UUID)) != 0 || + memcmp(payload + 16, "LDSZ", 4) != 0) + return; + assertf(payload[20] == 1, "Invalid SEI LDSZ payload version %d", payload[20]); + + uint32_t max_slice_size; + memcpy(&max_slice_size, payload + 21, sizeof(uint32_t)); + assertf(max_slice_size > 0 && max_slice_size <= H264_BUF_SIZE, + "Invalid max_slice_size: %ld", max_slice_size); + + player->max_slice_size = max_slice_size; + player->has_slice_metadata = true; +} + +static void release_current_picture(h264_t *player) { + if (player->pic) { + /* The DPB keeps output pictures locked until the client releases them, + so that background decoding cannot overwrite the current picture. */ + h264bsdDpbReleasePicture(player->s.dpb, player->pic); + player->pic = NULL; + } +} + +static int decode_next_slice(h264_t *player, u32 mb_budget) { + int left = player->buf_len - player->idx; + + // Mid-slice resume keeps absolute pointers into player->buf; do not + // compact/move the buffer while a slice is pending. + if (!h264bsdIsSlicePending(&player->s) && left <= (int)player->max_slice_size) { + memmove(player->buf, player->buf+player->idx, left); + int n = read(player->fd, player->buf+left, H264_BUF_SIZE-left); + if (n < 0) n = 0; + player->buf_len = left + n; + player->idx = 0; + + if (player->buf_len == 0) + return H264BSD_EOF; + } + + // Do the actual decoding. mb_budget 0 = unlimited (finish the slice). + unsigned int np; + PROFILE_START(PS_H264); + int status = h264bsdDecodePartial(&player->s, player->buf+player->idx, + player->buf_len-player->idx, 0, &np, mb_budget); + PROFILE_STOP(PS_H264); + + player->idx += np; + if (!player->has_slice_metadata && np) + player->max_slice_size = MAX(player->max_slice_size, np*1.3f); + + return status; +} + +static bool fetch_picture(h264_t *player) { + unsigned int picId=0, isIdrPic=0, numErrs=0; + player->pic = h264bsdNextOutputPicture(&player->s, &picId, &isIdrPic, &numErrs); + assert(numErrs == 0); + return player->pic != NULL; +} + + +static float get_framerate(h264_t *player) { + if (player->s.activeSps && + player->s.activeSps->vuiParametersPresentFlag && + player->s.activeSps->vuiParameters && + player->s.activeSps->vuiParameters->timingInfoPresentFlag) { + + uint32_t timeScale = player->s.activeSps->vuiParameters->timeScale; + uint32_t numUnitsInTick = player->s.activeSps->vuiParameters->numUnitsInTick; + + if (numUnitsInTick > 0) { + return (float)timeScale / (2.0f * numUnitsInTick); + } + } + // Default to 20 fps if not specified. This is totally arbitrary, but + // better than no playback... + return 20.0f; +} + +static float get_aspect_ratio(h264_t *player) { + // Prefer VUI SAR if present; otherwise assume square pixels. + u32 sarW = 1, sarH = 1; + h264bsdSampleAspectRatio(&player->s, &sarW, &sarH); + if (sarW == 0 || sarH == 0) { + sarW = 1; + sarH = 1; + } + return ((float)player->video.info.width * (float)sarW) / + ((float)player->video.info.height * (float)sarH); +} + +static yuv_colorspace_t get_colorspace(h264_t *player) { + + // H.264 VUI: + // - matrix_coefficients (if present; otherwise "unspecified" = 2) + // - video_full_range_flag (0 => limited/TV, 1 => full/PC) + const u32 full = h264bsdVideoRange(&player->s); + const u32 m = h264bsdMatrixCoefficients(&player->s); + + // matrix_coefficients values (H.264): 1=BT.709, 5=BT.470BG, 6=SMPTE170M, 2=unspecified. + const bool is_bt709 = (m == 1); + // Treat unspecified and BT.601 family as BT.601. + if (is_bt709) { + return full ? YUV_BT709_FULL : YUV_BT709_TV; + } else { + return full ? YUV_BT601_FULL : YUV_BT601_TV; + } +} + +static void h264_rewind(video_t *v) { + h264_t *player = (h264_t*)v; + + release_current_picture(player); + lseek(player->fd, 0, SEEK_SET); + player->idx = 0; + player->buf_len = 0; + player->in_frame_decoding = false; + + if (player->s.dpb->buffer) { + // Rewind: reset decoder state, keep existing allocations + h264bsdRewindStorage(&player->s); + } else { + // First open: initialize from scratch + h264bsdInit(&player->s, 0); + h264bsdSetNumBufferedPics(&player->s, (u32)player->max_buffered_pics); + } + h264bsdSetSeiCallback(&player->s, h264_sei_callback, player); + + rsph264_begin_frame(); + while (1) { + int status = decode_next_slice(player, 0); + switch (status) { + case H264BSD_RDY: + continue; + case H264BSD_HDRS_RDY: + // First open: SPS activated for the first time + player->in_frame_decoding = true; + return; + case H264BSD_PIC_RDY: + // Rewind: SPS unchanged, first IDR decoded immediately + player->in_frame_decoding = true; + return; + default: + assertf(0, "h264 rewind error: %d", status); + return; + } + } +} + +static video_t* h264_open(const char *fn, const video_parms_t *parms) { + h264_t *player = malloc(sizeof(h264_t) + H264_BUF_SIZE); + assertf(player, "Out of memory"); + sys_hw_memset(player, 0, sizeof(h264_t) + H264_BUF_SIZE); + player->fd = -1; + if (parms && parms->buffered_pics) + player->max_buffered_pics = parms->buffered_pics; + + rsph264_init(); + bwprof_init(); + h264bsdInitStorage(&player->s); + h264bsdSetNumBufferedPics(&player->s, (u32)player->max_buffered_pics); + + player->fd = must_open(fn); + h264_rewind(&player->video); + + // Fetch video information + player->video.info.width = h264bsdPicWidth(&player->s) * 16; + player->video.info.height = h264bsdPicHeight(&player->s) * 16; + player->video.info.framerate = get_framerate(player); + player->video.info.aspect_ratio = get_aspect_ratio(player); + player->video.info.colorspace = get_colorspace(player); + + return &player->video; +} + + +static void h264_close(video_t *v) { + h264_t *player = (h264_t*)v; + if (player->fd >= 0) { + close(player->fd); + player->fd = -1; + } + h264bsdShutdown(&player->s); + free(player); +} + +static const char* h264_status_str(int status) { + switch (status) { + case H264BSD_RDY: return "H264BSD_RDY"; + case H264BSD_PIC_RDY: return "H264BSD_PIC_RDY"; + case H264BSD_HDRS_RDY: return "H264BSD_HDRS_RDY"; + case H264BSD_ERROR: return "H264BSD_ERROR"; + case H264BSD_PARAM_SET_ERROR: return "H264BSD_PARAM_SET_ERROR"; + case H264BSD_MEMALLOC_ERROR: return "H264BSD_MEMALLOC_ERROR"; + default: return "???"; + } +} + +typedef enum { + POLL_NOTHING = 0, + POLL_DECODING = 1, + POLL_READY = 2, + POLL_EOF = -1, +} poll_status_t; + +static poll_status_t poll(h264_t *player, u32 mb_budget) +{ + // If the output buffer is full, we can't decode more, as there + // wouldn't be space for further pictures. Just exit. + if (player->max_buffered_pics > 0 && + h264bsdDpbNumOutputPictures(player->s.dpb) >= (u32)player->max_buffered_pics) + return POLL_NOTHING; + + int status = decode_next_slice(player, mb_budget); + + switch (status) { + case H264BSD_EOF: + // Video is ended + return POLL_EOF; + case H264BSD_RDY: + player->in_frame_decoding = true; + return POLL_DECODING; + case H264BSD_PIC_RDY: + player->in_frame_decoding = false; + bwprof_frame_end(); + return POLL_READY; + default: + assertf(!"h264 status error", "status == %d (%s)\n", status, h264_status_str(status)); + return POLL_NOTHING; + } +} + +static bool poll_loop(h264_t *player) +{ + while (1) { + /* Unlimited MB budget: finish the frame as fast as possible. */ + poll_status_t status = poll(player, 0); + assertf(status != POLL_NOTHING, "Decoder stalled while decoding frame"); + if (status == POLL_EOF) + return false; + if (status == POLL_READY) + return true; + } +} + +static int h264_poll(video_t *v) +{ + h264_t *player = (h264_t*)v; + + /* If buffering is disabled, do not decode in background. Decoding will + still happen on-demand via h264_next_frame()/poll_loop(). */ + if (player->max_buffered_pics == 0) + return 0; + + /* How many macroblocks to decode per poll */ + const u32 budget = 32; + + switch (poll(player, budget)) + { + case POLL_DECODING: + case POLL_READY: + return 1; + case POLL_NOTHING: + case POLL_EOF: + default: + return 0; + } +} + +static bool h264_next_frame(video_t *v) +{ + h264_t *player = (h264_t*)v; + + /* Release previous frame, if any. This makes the DPB buffer slot available + again for background decoding. */ + release_current_picture(player); + + // debugf("buffered: %d\n", h264bsdDpbNumOutputPictures(player->s.dpb)); + + // Fetch one picture from the output buffer. It could be pending because + // it's the first frame, or because previous calls to decode_loop have + // decoded more than one picture + if (fetch_picture(player)) + return true; + + // If no picture was decoded yet, we need to keep decoding until + // one picture is ready. + if (!poll_loop(player)) + return false; + + bool fetched = fetch_picture(player); + assertf(fetched, "Failed to fetch picture after decoding"); + return true; +} + +static yuv_frame_t h264_get_frame(video_t *v) +{ + h264_t *player = (h264_t*)v; + + int w = player->video.info.width, h = player->video.info.height; + int w2 = w / 2, h2 = h / 2; + + return (yuv_frame_t){ + .y = surface_make(player->pic, FMT_I8, w, h, w), + .u = surface_make(player->pic + w * h, FMT_I8, w2, h2, w2), + .v = surface_make(player->pic + w * h + w2 * h2, FMT_I8, w2, h2, w2), + }; +} + +static void h264_seekfast(video_t *v, int frame_idx, uint32_t file_off) +{ + h264_t *player = (h264_t*)v; + + /* Discard the current frame (if any) */ + release_current_picture(player); + + // Finish decoding the current frame, if any. This is required if we're + // using poll() and we are in the middle of a single frame decoding + // (or if we've just initialized the player and we're in the middle + // of the first frame). + if (player->in_frame_decoding) { + poll_loop(player); + assert(!player->in_frame_decoding); + } + + // Flush all pending pictures + while (fetch_picture(player)) { + /* We're discarding these pictures, so release them immediately. */ + release_current_picture(player); + } + + // Seek the underlying file to the specified offset, so that we're + // ready for next frame. + lseek(player->fd, file_off, SEEK_SET); + player->idx = 0; + player->buf_len = 0; +} + +void __h264_profile_init(void) { + profile_register(PS_H264, "H264", 0); + profile_register(PS_H264_NAL, "NAL", 1); + profile_register(PS_H264_LAYER, "Layer", 1); + // Clear is also entered from within Predict, so its time partly overlaps it + profile_register(PS_H264_LAYER_CLEAR, "Clear", 2); + profile_register(PS_H264_LAYER_PRED, "Predict", 2); + profile_register(PS_H264_LAYER_RES, "Residual", 2); + profile_register(PS_H264_LAYER_RES_ENC, "Encode", 3); + profile_register(PS_H264_MACROB, "MacroB", 1); + profile_register(PS_H264_INTERPRED, "InterPred", 2); + profile_register(PS_H264_INTERPRED_LUMA, "InterPred Luma", 3); + profile_register(PS_H264_INTERPRED_CHROMA, "InterPred Chroma", 3); + profile_register(PS_H264_RESIDUAL_LUMA, "Residual Luma", 2); + profile_register(PS_H264_INTRAPRED_4X4, "IntraPred 4x4", 2); + profile_register(PS_H264_INTRAPRED_16X16, "IntraPred 16x16", 2); + profile_register(PS_H264_RESIDUAL_CHROMA, "Residual Chroma", 2); +} + +video_codec_t h264_codec = { + .extension = ".h264", + .open = h264_open, + .close = h264_close, + .poll = h264_poll, + .get_frame = h264_get_frame, + .next_frame = h264_next_frame, + .rewind = h264_rewind, + .seekfast = h264_seekfast, +}; diff --git a/src/video/h264_decoder.c b/src/video/h264_decoder.c new file mode 100644 index 000000000..f7214fd0e --- /dev/null +++ b/src/video/h264_decoder.c @@ -0,0 +1,88 @@ +/** + * @file h264_decoder.c + * @author Giovanni Bajo + */ + +#if 0 // Enable debug prints +//#define _DEBUG_PRINT +#define _ASSERT_USED +#include "debug.h" +#endif + +// Enable error prints +#define _ERROR_PRINT + +#include "h264_decoder.h" + +#include "rsph264.c" + +#include "h264_decoder/h264bsd_byte_stream.c" +#include "h264_decoder/h264bsd_cavlc.c" +#include "h264_decoder/h264bsd_deblocking.c" +#include "h264_decoder/h264bsd_decoder.c" +#include "h264_decoder/h264bsd_dpb.c" +#include "h264_decoder/h264bsd_image.c" +#include "h264_decoder/h264bsd_inter_prediction.c" +#include "h264_decoder/h264bsd_intra_prediction.c" +#include "h264_decoder/h264bsd_macroblock_layer.c" +#include "h264_decoder/h264bsd_nal_unit.c" +#include "h264_decoder/h264bsd_neighbour.c" +#include "h264_decoder/h264bsd_pic_order_cnt.c" +#include "h264_decoder/h264bsd_pic_param_set.c" +#include "h264_decoder/h264bsd_reconstruct.c" +#include "h264_decoder/h264bsd_seq_param_set.c" +#include "h264_decoder/h264bsd_slice_data.c" +#include "h264_decoder/h264bsd_slice_group_map.c" +#include "h264_decoder/h264bsd_slice_header.c" +#include "h264_decoder/h264bsd_storage.c" +#include "h264_decoder/h264bsd_stream.c" +#include "h264_decoder/h264bsd_transform.c" +#include "h264_decoder/h264bsd_util.c" +#include "h264_decoder/h264bsd_vlc.c" +#include "h264_decoder/h264bsd_vui.c" +#include "h264_decoder/h264bsd_conceal.c" + +#include "h264_decoder/H264SwDecApi.c" + +#include "h264_decoder/omxdl/armCOMM.c" +#include "h264_decoder/omxdl/armCOMM_Bitstream.c" +#include "h264_decoder/omxdl/armVCCOMM_Average.c" +#include "h264_decoder/omxdl/armVCCOMM_SAD.c" +#include "h264_decoder/omxdl/armVCM4P10_CAVLCTables.c" +#include "h264_decoder/omxdl/armVCM4P10_DeBlockPixel.c" +#include "h264_decoder/omxdl/armVCM4P10_DecodeCoeffsToPair.c" +#include "h264_decoder/omxdl/armVCM4P10_DequantTables.c" +#include "h264_decoder/omxdl/armVCM4P10_InterpolateHalfDiag_Luma.c" +#include "h264_decoder/omxdl/armVCM4P10_InterpolateHalfHor_Luma.c" +#include "h264_decoder/omxdl/armVCM4P10_InterpolateHalfVer_Luma.c" +#include "h264_decoder/omxdl/armVCM4P10_Interpolate_Chroma.c" +#include "h264_decoder/omxdl/armVCM4P10_Interpolate_Luma.c" +#include "h264_decoder/omxdl/armVCM4P10_PredictIntraDC4x4.c" +#include "h264_decoder/omxdl/armVCM4P10_QuantTables.c" +#include "h264_decoder/omxdl/armVCM4P10_TransformResidual4x4.c" +#include "h264_decoder/omxdl/armVCM4P10_UnpackBlock2x2.c" +#include "h264_decoder/omxdl/armVCM4P10_UnpackBlock4x4.c" +#include "h264_decoder/omxdl/omxVCM4P10_DeblockChroma_I.c" +#include "h264_decoder/omxdl/omxVCM4P10_DeblockLuma_I.c" +#include "h264_decoder/omxdl/omxVCM4P10_DecodeChromaDcCoeffsToPairCAVLC.c" +#include "h264_decoder/omxdl/omxVCM4P10_DecodeCoeffsToPairCAVLC.c" +#include "h264_decoder/omxdl/omxVCM4P10_DequantTransformResidualFromPairAndAdd.c" +#include "h264_decoder/omxdl/omxVCM4P10_FilterDeblockingChroma_HorEdge_I.c" +#include "h264_decoder/omxdl/omxVCM4P10_FilterDeblockingChroma_VerEdge_I.c" +#include "h264_decoder/omxdl/omxVCM4P10_FilterDeblockingLuma_HorEdge_I.c" +#include "h264_decoder/omxdl/omxVCM4P10_FilterDeblockingLuma_VerEdge_I.c" +#include "h264_decoder/omxdl/omxVCM4P10_GetVLCInfo.c" +#include "h264_decoder/omxdl/omxVCM4P10_InterpolateChroma.c" +#include "h264_decoder/omxdl/omxVCM4P10_InterpolateHalfHor_Luma.c" +#include "h264_decoder/omxdl/omxVCM4P10_InterpolateHalfVer_Luma.c" +#include "h264_decoder/omxdl/omxVCM4P10_InterpolateLuma.c" +#include "h264_decoder/omxdl/omxVCM4P10_InvTransformDequant_ChromaDC.c" +#include "h264_decoder/omxdl/omxVCM4P10_InvTransformDequant_LumaDC.c" +#include "h264_decoder/omxdl/omxVCM4P10_InvTransformResidualAndAdd.c" +#include "h264_decoder/omxdl/omxVCM4P10_MEGetBufSize.c" +#include "h264_decoder/omxdl/omxVCM4P10_MEInit.c" +#include "h264_decoder/omxdl/omxVCM4P10_PredictIntraChroma_8x8.c" +#include "h264_decoder/omxdl/omxVCM4P10_PredictIntra_16x16.c" +#include "h264_decoder/omxdl/omxVCM4P10_PredictIntra_4x4.c" +#include "h264_decoder/omxdl/omxVCM4P10_TransformDequantChromaDCFromPair.c" +#include "h264_decoder/omxdl/omxVCM4P10_TransformDequantLumaDCFromPair.c" diff --git a/src/video/h264_decoder.h b/src/video/h264_decoder.h new file mode 100644 index 000000000..a8d48caa1 --- /dev/null +++ b/src/video/h264_decoder.h @@ -0,0 +1,54 @@ +/** + * @file h264_decoder.h + * @author Giovanni Bajo + */ +#ifndef LIBDRAGON_VIDEO_H264_DECODER_H +#define LIBDRAGON_VIDEO_H264_DECODER_H + +// Activate N64 specific codepath +#define H264BSD_N64 1 +#define H264BSD_N64_INTRA 1 // Intraprediction on RSP + +// Disable all code related to concealment (recovering of corrupted data). +// This must be defined before including the h264bsd headers, as it changes +// the layout of mbStorage_t (drops the `decoded` field); every translation +// unit that pulls in the h264 decoder must agree on it, so it lives here. +#define OPTIMIZE_NO_DECODED_FLAG + +// Maximum number of macroblocks that the RSP will be able to lag behind the +// CPU, and process in background. This basically specifies how big is the +// mbLayers array above. +// There is currently no explicit sync for this, this number is +// experimental. If the number is too little, some corruption might appear on +// some frames, especially when the RSP is too slow. We could in theory use +// syncpoints for this. +// One macroblock takes about 1.5KB of memory, so 128 macroblocks is +// about 192KB of RAM. +#define NUM_PARALLEL_MACROBLOCKS 128 + +#include "profile.h" +#include "h264_decoder/h264bsd_decoder.h" +#include "h264_decoder/h264bsd_storage.h" + +// Profile slots, listed in call order: profile_dump() prints them by slot +// number, and indents each one by the nesting level declared in +// __h264_profile_init(). Keep the two in sync when adding a slot. +typedef enum { + PS_H264 = PROFILE_SLOT_H264, // h264bsdDecode + PS_H264_NAL, // h264bsdExtractNalUnit + h264bsdDecodeNalUnit + PS_H264_LAYER, // h264bsdDecodeMacroblockLayer (bitstream) + PS_H264_LAYER_CLEAR, + PS_H264_LAYER_PRED, // DecodeMbPred / DecodeSubMbPred + PS_H264_LAYER_RES, // DecodeResidual + PS_H264_LAYER_RES_ENC, // CAVLC coefficient decoding + PS_H264_MACROB, // h264bsdDecodeMacroblock (reconstruction) + PS_H264_INTERPRED, // h264bsdInterPrediction + PS_H264_INTERPRED_LUMA, // C fallback interpolation (unused on N64) + PS_H264_INTERPRED_CHROMA, + PS_H264_RESIDUAL_LUMA, // inter luma residual + PS_H264_INTRAPRED_4X4, // ProcessIntra4x4Residual + PS_H264_INTRAPRED_16X16, // ProcessIntra16x16Residual + PS_H264_RESIDUAL_CHROMA, // ProcessChromaResidual +} H264ProfileSlot; + +#endif diff --git a/src/video/h264_decoder/Decoder.cpp b/src/video/h264_decoder/Decoder.cpp new file mode 100644 index 000000000..1234bb290 --- /dev/null +++ b/src/video/h264_decoder/Decoder.cpp @@ -0,0 +1,164 @@ +#include "Decoder.h" + +Stream::Stream() { + H264SwDecRet ret; +#ifdef DISABLE_OUTPUT_REORDERING + u32 disableOutputReordering = 1; +#else + u32 disableOutputReordering = 0; +#endif + + // Initialize decoder instance. + ret = H264SwDecInit(&decInst, disableOutputReordering); + if (ret != H264SWDEC_OK) { + // TODO: stop this Stream object from trying to decode any video! + // Perhaps use a flag to indicate that the initialization was successful? + DEBUG(("DECODER INITIALIZATION FAILED\n")); + return; + } + + picDecodeNumber = 1; +} + +Stream::~Stream() { + if (streamBuffer) { + free(streamBuffer); + } +} + +void Stream::SetStream(u8* strmBuffer, u32 strmLength) { + u8* new_buffer = new u8[strmLength]; + if (new_buffer == NULL) { + DEBUG(("UNABLE TO ALLOCATE MEMORY\n")); + exit(1); + } + + memcpy(new_buffer, strmBuffer, strmLength); + streamBuffer = new_buffer; + decInput.pStream = new_buffer; + decInput.dataLen = strmLength; + currPackagePos = 0; + bufferSize = strmLength; +} + + +void Stream::UpdateStream(u8* strmBuffer, u32 strmLength) { + u32 new_buffer_size = bufferSize - currPackagePos + strmLength; + u8* new_buffer = new u8[new_buffer_size]; + if (new_buffer == NULL) { + DEBUG(("UNABLE TO ALLOCATE MEMORY\n")); + exit(1); + } + + u32 last_package_size = bufferSize-currPackagePos; + // keep the current package that is being processed + memcpy(new_buffer, streamBuffer+currPackagePos, last_package_size); + // and add the new one + memcpy(new_buffer+last_package_size, strmBuffer, strmLength); + free(streamBuffer); + streamBuffer = new_buffer; + + bufferSize = new_buffer_size; + currPackagePos = 0; + + // Update the decoder input buffer + decInput.pStream = streamBuffer; + decInput.dataLen = new_buffer_size; +} + + +u8* Stream::GetFrame(u32* outImageWidth, u32* outImageHeight) { + *outImageWidth = decInfo.picWidth; + *outImageHeight = decInfo.picHeight; + + return (u8*)decPicture.pOutputPicture; +} + + +StreamStatus Stream::BroadwayDecode() { + decInput.picId = picDecodeNumber; + + H264SwDecRet ret = H264SwDecDecode(decInst, &decInput, &decOutput); + StreamStatus status; + + switch (ret) { + case H264SWDEC_HDRS_RDY_BUFF_NOT_EMPTY: + // Stream headers were successfully decoded, thus stream information is available for query now. + ret = H264SwDecGetInfo(decInst, &decInfo); + if (ret != H264SWDEC_OK) { + return STREAM_ERROR; + } + + picSize = decInfo.picWidth * decInfo.picHeight; + picSize = (3 * picSize) / 2; + + decInput.dataLen -= decOutput.pStrmCurrPos - decInput.pStream; + decInput.pStream = decOutput.pStrmCurrPos; + + // Try to decode again to get a picture. The user is not interested on the Headers info... + return BroadwayDecode(); + + break; + + case H264SWDEC_PIC_RDY_BUFF_NOT_EMPTY: + // Picture is ready and more data remains in the input buffer, + // update input structure. + decInput.dataLen -= decOutput.pStrmCurrPos - decInput.pStream; + decInput.pStream = decOutput.pStrmCurrPos; + + currPackagePos = bufferSize - decInput.dataLen; + picDecodeNumber++; + + while (H264SwDecNextPicture(decInst, &decPicture, 1) == H264SWDEC_PIC_RDY) { } + + break; + + case H264SWDEC_PIC_RDY: + currPackagePos = bufferSize; + picDecodeNumber++; + + decInput.dataLen -= decOutput.pStrmCurrPos - decInput.pStream; + currPackagePos = bufferSize - decInput.dataLen; + + while (H264SwDecNextPicture(decInst, &decPicture, 1) == H264SWDEC_PIC_RDY) { } + + break; + + case H264SWDEC_STRM_PROCESSED: + case H264SWDEC_STRM_ERR: + // Input stream was decoded but no picture is ready, thus get more data. + // decInput.dataLen = 0; + break; + + default: + break; + } + + switch (ret) { + case H264SWDEC_PIC_RDY_BUFF_NOT_EMPTY: + case H264SWDEC_PIC_RDY: + status = PIC_READY; + break; + + case H264SWDEC_STRM_PROCESSED: + status = STREAM_ENDED; + break; + + case H264SWDEC_STRM_ERR: + default: + status = STREAM_ERROR; + } + + + return status; +} + + +u32 broadwayGetMajorVersion() { + return H264SwDecGetAPIVersion().major; +} + +u32 broadwayGetMinorVersion() { + return H264SwDecGetAPIVersion().minor; +} + diff --git a/src/video/h264_decoder/Decoder.h b/src/video/h264_decoder/Decoder.h new file mode 100644 index 000000000..537ee5d14 --- /dev/null +++ b/src/video/h264_decoder/Decoder.h @@ -0,0 +1,52 @@ +extern "C" { +#include "H264SwDecApi.h" + +#include +#include +#include + +/* Debug prints */ +#define DEBUG(argv) printf argv + +#include "opttarget.h" + +#include "extraFlags.h" + +} + +// This is just H264SwDecRet with clearer names +enum StreamStatus { + PIC_READY = 0, // Picture is ready and no more data is on buffer + STREAM_ENDED = 1, // Input stream was decoded but no picture is ready, thus get more data + STREAM_ERROR = 2 // Internal stream error +}; + +class Stream { +public: + Stream(); + ~Stream(); + + u8* GetFrame(u32* outImageWidth, u32* outImageHeight); + void SetStream(u8* strmBuffer, u32 strmLength); + void UpdateStream(u8* strmBuffer, u32 strmLength); + StreamStatus BroadwayDecode(); + +private: + u8 *streamBuffer = NULL; + + H264SwDecInst decInst; + H264SwDecInput decInput; + H264SwDecOutput decOutput; + H264SwDecPicture decPicture; + H264SwDecInfo decInfo; + + u32 picDecodeNumber; + u32 picSize; + u32 currPackagePos; + u32 bufferSize; +}; + + +u32 broadwayGetMajorVersion(); + +u32 broadwayGetMinorVersion(); \ No newline at end of file diff --git a/src/video/h264_decoder/H264SwDecApi.c b/src/video/h264_decoder/H264SwDecApi.c new file mode 100644 index 000000000..01b7c1ec4 --- /dev/null +++ b/src/video/h264_decoder/H264SwDecApi.c @@ -0,0 +1,574 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + H264SwDecInit + H264SwDecGetInfo + H264SwDecRelease + H264SwDecDecode + H264SwDecGetAPIVersion + H264SwDecNextPicture + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ +#include +#include +#include "basetype.h" +#include "h264bsd_container.h" +#include "H264SwDecApi.h" +#include "h264bsd_decoder.h" +#include "h264bsd_util.h" + +#define UNUSED(x) (void)(x) + +/*------------------------------------------------------------------------------ + Version Information +------------------------------------------------------------------------------*/ + +#define H264SWDEC_MAJOR_VERSION 2 +#define H264SWDEC_MINOR_VERSION 3 + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +H264DEC_TRACE Trace H264 Decoder API function calls. +H264DEC_EVALUATION Compile evaluation version, restricts number of frames + that can be decoded + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +#ifdef H264DEC_TRACE +#include +#define DEC_API_TRC(str) H264SwDecTrace(str) +#else +#define DEC_API_TRC(str) +#endif + +#ifdef H264DEC_EVALUATION +#define H264DEC_EVALUATION_LIMIT 500 +#endif + +void H264SwDecTrace(char *string) { + UNUSED(string); +} + +void* H264SwDecMalloc(u32 size) { + return malloc(size); +} + +void H264SwDecFree(void *ptr) { + free(ptr); +} + +#ifndef H264BSD_N64 + +void H264SwDecMemcpy(void *dest, void *src, u32 count) { + memcpy(dest, src, count); +} + +void H264SwDecMemset(void *ptr, i32 value, u32 count) { + memset(ptr, value, count); +} + +#endif + +/*------------------------------------------------------------------------------ + + Function: H264SwDecInit() + + Functional description: + Initialize decoder software. Function reserves memory for the + decoder instance and calls h264bsdInit to initialize the + instance data. + + Inputs: + noOutputReordering flag to indicate decoder that it doesn't have + to try to provide output pictures in display + order, saves memory + + Outputs: + decInst pointer to initialized instance is stored here + + Returns: + H264SWDEC_OK successfully initialized the instance + H264SWDEC_INITFAIL initialization failed + H264SWDEC_PARAM_ERR invalid parameters + H264SWDEC_MEM_FAIL memory allocation failed + +------------------------------------------------------------------------------*/ + +H264SwDecRet H264SwDecInit(H264SwDecInst *decInst, u32 noOutputReordering) +{ + u32 rv = 0; + + decContainer_t *pDecCont; + + DEC_API_TRC("H264SwDecInit#"); + + /* check that right shift on negative numbers is performed signed */ + /*lint -save -e* following check causes multiple lint messages */ + if ( ((-1)>>1) != (-1) ) + { + DEC_API_TRC("H264SwDecInit# ERROR: Right shift is not signed"); + return(H264SWDEC_INITFAIL); + } + /*lint -restore */ + + if (decInst == NULL) + { + DEC_API_TRC("H264SwDecInit# ERROR: decInst == NULL"); + return(H264SWDEC_PARAM_ERR); + } + + pDecCont = (decContainer_t *)H264SwDecMalloc(sizeof(decContainer_t)); + + if (pDecCont == NULL) + { + DEC_API_TRC("H264SwDecInit# ERROR: Memory allocation failed"); + return(H264SWDEC_MEMFAIL); + } + +#ifdef H264DEC_TRACE + sprintf(pDecCont->str, "H264SwDecInit# decInst %p noOutputReordering %d", + (void*)decInst, noOutputReordering); + DEC_API_TRC(pDecCont->str); +#endif + + rv = h264bsdInit(&pDecCont->storage, noOutputReordering); + if (rv != HANTRO_OK) + { + H264SwDecRelease(pDecCont); + return(H264SWDEC_MEMFAIL); + } + + pDecCont->decStat = INITIALIZED; + pDecCont->picNumber = 0; + +#ifdef H264DEC_TRACE + sprintf(pDecCont->str, "H264SwDecInit# OK: return %p", (void*)pDecCont); + DEC_API_TRC(pDecCont->str); +#endif + + *decInst = (decContainer_t *)pDecCont; + + return(H264SWDEC_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: H264SwDecGetInfo() + + Functional description: + This function provides read access to decoder information. This + function should not be called before H264SwDecDecode function has + indicated that headers are ready. + + Inputs: + decInst decoder instance + + Outputs: + pDecInfo pointer to info struct where data is written + + Returns: + H264SWDEC_OK success + H264SWDEC_PARAM_ERR invalid parameters + H264SWDEC_HDRS_NOT_RDY information not available yet + +------------------------------------------------------------------------------*/ + +H264SwDecRet H264SwDecGetInfo(H264SwDecInst decInst, H264SwDecInfo *pDecInfo) +{ + + storage_t *pStorage; + + DEC_API_TRC("H264SwDecGetInfo#"); + + if (decInst == NULL || pDecInfo == NULL) + { + DEC_API_TRC("H264SwDecGetInfo# ERROR: decInst or pDecInfo is NULL"); + return(H264SWDEC_PARAM_ERR); + } + + pStorage = &(((decContainer_t *)decInst)->storage); + + if (pStorage->activeSps == NULL || pStorage->activePps == NULL) + { + DEC_API_TRC("H264SwDecGetInfo# ERROR: Headers not decoded yet"); + return(H264SWDEC_HDRS_NOT_RDY); + } + +#ifdef H264DEC_TRACE + sprintf(((decContainer_t*)decInst)->str, + "H264SwDecGetInfo# decInst %p pDecInfo %p", decInst, (void*)pDecInfo); + DEC_API_TRC(((decContainer_t*)decInst)->str); +#endif + + /* h264bsdPicWidth and -Height return dimensions in macroblock units, + * picWidth and -Height in pixels */ + pDecInfo->picWidth = h264bsdPicWidth(pStorage) << 4; + pDecInfo->picHeight = h264bsdPicHeight(pStorage) << 4; + pDecInfo->videoRange = h264bsdVideoRange(pStorage); + pDecInfo->matrixCoefficients = h264bsdMatrixCoefficients(pStorage); + + h264bsdCroppingParams(pStorage, + &pDecInfo->croppingFlag, + &pDecInfo->cropParams.cropLeftOffset, + &pDecInfo->cropParams.cropOutWidth, + &pDecInfo->cropParams.cropTopOffset, + &pDecInfo->cropParams.cropOutHeight); + + /* sample aspect ratio */ + h264bsdSampleAspectRatio(pStorage, + &pDecInfo->parWidth, + &pDecInfo->parHeight); + + /* profile */ + pDecInfo->profile = h264bsdProfile(pStorage); + + DEC_API_TRC("H264SwDecGetInfo# OK"); + + return(H264SWDEC_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: H264SwDecRelease() + + Functional description: + Release the decoder instance. Function calls h264bsdShutDown to + release instance data and frees the memory allocated for the + instance. + + Inputs: + decInst Decoder instance + + Outputs: + none + + Returns: + none + +------------------------------------------------------------------------------*/ + +void H264SwDecRelease(H264SwDecInst decInst) +{ + + decContainer_t *pDecCont; + + DEC_API_TRC("H264SwDecRelease#"); + + if (decInst == NULL) + { + DEC_API_TRC("H264SwDecRelease# ERROR: decInst == NULL"); + return; + } + + pDecCont = (decContainer_t*)decInst; + +#ifdef H264DEC_TRACE + sprintf(pDecCont->str, "H264SwDecRelease# decInst %p",decInst); + DEC_API_TRC(pDecCont->str); +#endif + + h264bsdShutdown(&pDecCont->storage); + + H264SwDecFree(pDecCont); + +} + +/*------------------------------------------------------------------------------ + + Function: H264SwDecDecode + + Functional description: + Decode stream data. Calls h264bsdDecode to do the actual decoding. + + Input: + decInst decoder instance + pInput pointer to input struct + + Outputs: + pOutput pointer to output struct + + Returns: + H264SWDEC_NOT_INITIALIZED decoder instance not initialized yet + H264SWDEC_PARAM_ERR invalid parameters + + H264SWDEC_STRM_PROCESSED stream buffer decoded + H264SWDEC_HDRS_RDY_BUFF_NOT_EMPTY headers decoded, + stream buffer not finished + H264SWDEC_PIC_RDY decoding of a picture finished + H264SWDEC_PIC_RDY_BUFF_NOT_EMPTY decoding of a picture finished, + stream buffer not finished + H264SWDEC_STRM_ERR serious error in decoding, no + valid parameter sets available + to decode picture data + H264SWDEC_EVALUATION_LIMIT_EXCEEDED this can only occur when + evaluation version is used, + max number of frames reached + +------------------------------------------------------------------------------*/ + +H264SwDecRet H264SwDecDecode(H264SwDecInst decInst, H264SwDecInput *pInput, + H264SwDecOutput *pOutput) +{ + + decContainer_t *pDecCont; + u32 strmLen; + u32 numReadBytes; + u8 *tmpStream; + u32 decResult = 0; + H264SwDecRet returnValue = H264SWDEC_STRM_PROCESSED; + + DEC_API_TRC("H264SwDecDecode#"); + + /* Check that function input parameters are valid */ + if (pInput == NULL || pOutput == NULL) + { + DEC_API_TRC("H264SwDecDecode# ERROR: pInput or pOutput is NULL"); + return(H264SWDEC_PARAM_ERR); + } + + if ((pInput->pStream == NULL) || (pInput->dataLen == 0)) + { + DEC_API_TRC("H264SwDecDecode# ERROR: Invalid input parameters"); + return(H264SWDEC_PARAM_ERR); + } + + pDecCont = (decContainer_t *)decInst; + + /* Check if decoder is in an incorrect mode */ + if (decInst == NULL || pDecCont->decStat == UNINITIALIZED) + { + DEC_API_TRC("H264SwDecDecode# ERROR: Decoder not initialized"); + return(H264SWDEC_NOT_INITIALIZED); + } + +#ifdef H264DEC_EVALUATION + if (pDecCont->picNumber >= H264DEC_EVALUATION_LIMIT) + return(H264SWDEC_EVALUATION_LIMIT_EXCEEDED); +#endif + +#ifdef H264DEC_TRACE + sprintf(pDecCont->str, "H264SwDecDecode# decInst %p pInput %p pOutput %p", + decInst, (void*)pInput, (void*)pOutput); + DEC_API_TRC(pDecCont->str); +#endif + + pOutput->pStrmCurrPos = NULL; + + numReadBytes = 0; + strmLen = pInput->dataLen; + tmpStream = pInput->pStream; + pDecCont->storage.intraConcealmentFlag = pInput->intraConcealmentMethod; + + do + { + /* Return HDRS_RDY after DPB flush caused by new SPS */ + if (pDecCont->decStat == NEW_HEADERS) + { + decResult = H264BSD_HDRS_RDY; + pDecCont->decStat = INITIALIZED; + } + else /* Continue decoding normally */ + { + decResult = h264bsdDecode(&pDecCont->storage, tmpStream, strmLen, + pInput->picId, &numReadBytes); + } + tmpStream += numReadBytes; + /* check if too many bytes are read from stream */ + if ( (i32)(strmLen - numReadBytes) >= 0 ) + strmLen -= numReadBytes; + else + strmLen = 0; + + pOutput->pStrmCurrPos = tmpStream; + + switch (decResult) + { + case H264BSD_HDRS_RDY: + + if(pDecCont->storage.dpb->flushed && + pDecCont->storage.dpb->numOut != + pDecCont->storage.dpb->outIndex) + { + /* output first all DPB stored pictures + * DPB flush caused by new SPS */ + pDecCont->storage.dpb->flushed = 0; + pDecCont->decStat = NEW_HEADERS; + returnValue = H264SWDEC_PIC_RDY_BUFF_NOT_EMPTY; + strmLen = 0; + } + else + { + returnValue = H264SWDEC_HDRS_RDY_BUFF_NOT_EMPTY; + strmLen = 0; + } + break; + + case H264BSD_PIC_RDY: + pDecCont->picNumber++; + + if (strmLen == 0) + returnValue = H264SWDEC_PIC_RDY; + else + returnValue = H264SWDEC_PIC_RDY_BUFF_NOT_EMPTY; + + strmLen = 0; + break; + + case H264BSD_PARAM_SET_ERROR: + if ( !h264bsdCheckValidParamSets(&pDecCont->storage) && + strmLen == 0 ) + { + returnValue = H264SWDEC_STRM_ERR; + } + break; + case H264BSD_MEMALLOC_ERROR: + { + returnValue = H264SWDEC_MEMFAIL; + strmLen = 0; + } + break; + default: + break; + } + + } while (strmLen); + +#ifdef H264DEC_TRACE + sprintf(pDecCont->str, "H264SwDecDecode# OK: DecResult %d", + returnValue); + DEC_API_TRC(pDecCont->str); +#endif + + return(returnValue); + +} + +/*------------------------------------------------------------------------------ + + Function: H264SwDecGetAPIVersion + + Functional description: + Return version information of the API + + Inputs: + none + + Outputs: + none + + Returns: + API version + +------------------------------------------------------------------------------*/ + +H264SwDecApiVersion H264SwDecGetAPIVersion() +{ + H264SwDecApiVersion ver; + + ver.major = H264SWDEC_MAJOR_VERSION; + ver.minor = H264SWDEC_MINOR_VERSION; + + return(ver); +} + +/*------------------------------------------------------------------------------ + + Function: H264SwDecNextPicture + + Functional description: + Get next picture in display order if any available. + + Input: + decInst decoder instance. + flushBuffer force output of all buffered pictures + + Output: + pOutput pointer to output structure + + Returns: + H264SWDEC_OK no pictures available for display + H264SWDEC_PIC_RDY picture available for display + H264SWDEC_PARAM_ERR invalid parameters + +------------------------------------------------------------------------------*/ + +H264SwDecRet H264SwDecNextPicture(H264SwDecInst decInst, + H264SwDecPicture *pOutput, u32 flushBuffer) +{ + + decContainer_t *pDecCont; + u32 numErrMbs, isIdrPic, picId; + u32 *pOutPic; + + DEC_API_TRC("H264SwDecNextPicture#"); + + if (decInst == NULL || pOutput == NULL) + { + DEC_API_TRC("H264SwDecNextPicture# ERROR: decInst or pOutput is NULL"); + return(H264SWDEC_PARAM_ERR); + } + + pDecCont = (decContainer_t*)decInst; + +#ifdef H264DEC_TRACE + sprintf(pDecCont->str, "H264SwDecNextPicture# decInst %p pOutput %p %s %d", + decInst, (void*)pOutput, "flushBuffer", flushBuffer); + DEC_API_TRC(pDecCont->str); +#endif + + if (flushBuffer) + h264bsdFlushBuffer(&pDecCont->storage); + + pOutPic = (u32*)h264bsdNextOutputPicture(&pDecCont->storage, &picId, + &isIdrPic, &numErrMbs); + + if (pOutPic == NULL) + { + DEC_API_TRC("H264SwDecNextPicture# OK: return H264SWDEC_OK"); + return(H264SWDEC_OK); + } + else + { + pOutput->pOutputPicture = pOutPic; + pOutput->picId = picId; + pOutput->isIdrPicture = isIdrPic; + pOutput->nbrOfErrMBs = numErrMbs; + DEC_API_TRC("H264SwDecNextPicture# OK: return H264SWDEC_PIC_RDY"); + return(H264SWDEC_PIC_RDY); + } + +} + + diff --git a/src/video/h264_decoder/H264SwDecApi.h b/src/video/h264_decoder/H264SwDecApi.h new file mode 100644 index 000000000..167f89fbc --- /dev/null +++ b/src/video/h264_decoder/H264SwDecApi.h @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include Headers + + 2. Enumerations used as a return value or a parameter. + 2.1. API's return value enumerations. + + 3. User Structures + 3.1. Structures for H264SwDecDecode() parameters. + 3.2. Structures for information interchange with + DEC API and user application. + + 4. Prototypes of Decoder API functions + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDECAPI_H +#define H264SWDECAPI_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +/*------------------------------------------------------------------------------ + 1. Include Headers +------------------------------------------------------------------------------*/ + + #include "basetype.h" + +/*------------------------------------------------------------------------------ + 2.1. API's return value enumerations. +------------------------------------------------------------------------------*/ + + typedef enum + { + H264SWDEC_OK = 0, + H264SWDEC_STRM_PROCESSED = 1, + H264SWDEC_PIC_RDY, + H264SWDEC_PIC_RDY_BUFF_NOT_EMPTY, + H264SWDEC_HDRS_RDY_BUFF_NOT_EMPTY, + H264SWDEC_PARAM_ERR = -1, + H264SWDEC_STRM_ERR = -2, + H264SWDEC_NOT_INITIALIZED = -3, + H264SWDEC_MEMFAIL = -4, + H264SWDEC_INITFAIL = -5, + H264SWDEC_HDRS_NOT_RDY = -6, + H264SWDEC_EVALUATION_LIMIT_EXCEEDED = -7 + } H264SwDecRet; + +/*------------------------------------------------------------------------------ + 3.1. Structures for H264SwDecDecode() parameters. +------------------------------------------------------------------------------*/ + + /* typedef of the Decoder instance */ + typedef void *H264SwDecInst; + + /* Input structure */ + typedef struct + { + u8 *pStream; /* Pointer to stream to be decoded */ + u32 dataLen; /* Number of bytes to be decoded */ + u32 picId; /* Identifier for the picture to be decoded */ + u32 intraConcealmentMethod; /* 0 = Gray concealment for intra + 1 = Reference concealment for intra */ + + } H264SwDecInput; + + + /* Output structure */ + typedef struct + { + u8 *pStrmCurrPos; /* Pointer to stream position where decoder + ended up */ + } H264SwDecOutput; + + /* Output structure for H264SwDecNextPicture */ + typedef struct + { + u32 *pOutputPicture; /* Pointer to the picture, YUV format */ + u32 picId; /* Identifier of the picture to be displayed*/ + u32 isIdrPicture; /* Flag to indicate if the picture is an + IDR picture */ + u32 nbrOfErrMBs; /* Number of concealed MB's in the picture */ + } H264SwDecPicture; + +/*------------------------------------------------------------------------------ + 3.2. Structures for information interchange with DEC API + and user application. +------------------------------------------------------------------------------*/ + + typedef struct + { + u32 cropLeftOffset; + u32 cropOutWidth; + u32 cropTopOffset; + u32 cropOutHeight; + } CropParams; + + typedef struct + { + u32 profile; + u32 picWidth; + u32 picHeight; + u32 videoRange; + u32 matrixCoefficients; + u32 parWidth; + u32 parHeight; + u32 croppingFlag; + CropParams cropParams; + } H264SwDecInfo; + + /* Version information */ + typedef struct + { + u32 major; /* Decoder API major version */ + u32 minor; /* Dncoder API minor version */ + } H264SwDecApiVersion; + +/*------------------------------------------------------------------------------ + 4. Prototypes of Decoder API functions +------------------------------------------------------------------------------*/ + + H264SwDecRet H264SwDecDecode(H264SwDecInst decInst, + H264SwDecInput *pInput, + H264SwDecOutput *pOutput); + + H264SwDecRet H264SwDecInit(H264SwDecInst *decInst, + u32 noOutputReordering); + + H264SwDecRet H264SwDecNextPicture(H264SwDecInst decInst, + H264SwDecPicture *pOutput, + u32 endOfStream); + + H264SwDecRet H264SwDecGetInfo(H264SwDecInst decInst, + H264SwDecInfo *pDecInfo); + + void H264SwDecRelease(H264SwDecInst decInst); + + H264SwDecApiVersion H264SwDecGetAPIVersion(void); + + /* function prototype for API trace */ + void H264SwDecTrace(char *); + + /* function prototype for memory allocation */ + void* H264SwDecMalloc(u32 size); + + /* function prototype for memory free */ + void H264SwDecFree(void *ptr); + + #ifdef H264BSD_N64 + #include + #include "n64sys.h" + + inline void H264SwDecMemcpy(void *dest, void *src, u32 count) { + memcpy(dest, src, count); + } + inline void H264SwDecMemset(void *ptr, i32 value, u32 count) { + //memset(ptr, value, count); + sys_hw_memset(ptr, value, count); + } + + #else + /* function prototype for memory copy */ + void H264SwDecMemcpy(void *dest, void *src, u32 count); + + /* function prototype for memset */ + void H264SwDecMemset(void *ptr, i32 value, u32 count); + #endif + + +#ifdef __cplusplus +} +#endif + +#endif /* H264SWDECAPI_H */ + + + + + + + + + + + + diff --git a/src/video/h264_decoder/basetype.h b/src/video/h264_decoder/basetype.h new file mode 100644 index 000000000..5686acdad --- /dev/null +++ b/src/video/h264_decoder/basetype.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#ifndef BASETYPE_H_INCLUDED +#define BASETYPE_H_INCLUDED + + +#ifdef __arm +#define VOLATILE volatile +#else +#define VOLATILE +#endif + +typedef unsigned char u8; +typedef signed char i8; +typedef unsigned short u16; +typedef signed short i16; +typedef unsigned int u32; +typedef signed int i32; +typedef unsigned long long u64; +typedef signed long long i64; + +#if defined(VC1SWDEC_16BIT) || defined(MP4ENC_ARM11) +typedef unsigned short u16x; +typedef signed short i16x; +#else +typedef unsigned int u16x; +typedef signed int i16x; +#endif + + +#ifndef NULL +#ifdef __cplusplus +#define NULL 0 +#else +#define NULL ((void *)0) +#endif +#endif + +#endif /* BASETYPE_H_INCLUDED */ diff --git a/src/video/h264_decoder/h264bsd_byte_stream.c b/src/video/h264_decoder/h264bsd_byte_stream.c new file mode 100644 index 000000000..9626eb998 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_byte_stream.c @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + ExtractNalUnit + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_byte_stream.h" +#include "h264bsd_util.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +#define BYTE_STREAM_ERROR 0xFFFFFFFF + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + + Function name: ExtractNalUnit + + Functional description: + Extracts one NAL unit from the byte stream buffer. Removes + emulation prevention bytes if present. The original stream buffer + is used directly and is therefore modified if emulation prevention + bytes are present in the stream. + + Stream buffer is assumed to contain either exactly one NAL unit + and nothing else, or one or more NAL units embedded in byte + stream format described in the Annex B of the standard. Function + detects which one is used based on the first bytes in the buffer. + + Inputs: + pByteStream pointer to byte stream buffer + len length of the stream buffer (in bytes) + + Outputs: + pStrmData stream information is stored here + readBytes number of bytes "consumed" from the stream buffer + + Returns: + HANTRO_OK success + HANTRO_NOK error in byte stream + +------------------------------------------------------------------------------*/ + +u32 h264bsdExtractNalUnit(u8 *pByteStream, u32 len, strmData_t *pStrmData, + u32 *readBytes) +{ + +/* Variables */ + + u32 i, tmp; + u32 byteCount,initByteCount; + u32 zeroCount; + u8 byte; + u32 hasEmulation = HANTRO_FALSE; + u32 invalidStream = HANTRO_FALSE; + u32 strmBuffSize; + u8 *readPtr, *writePtr; + +/* Code */ + + ASSERT(pByteStream); + ASSERT(len); + ASSERT(len < BYTE_STREAM_ERROR); + ASSERT(pStrmData); + + /* byte stream format if starts with 0x000001 or 0x000000 */ + if (len > 3 && pByteStream[0] == 0x00 && pByteStream[1] == 0x00 && + (pByteStream[2]&0xFE) == 0x00) + { + /* search for NAL unit start point, i.e. point after first start code + * prefix in the stream */ + zeroCount = byteCount = 2; + readPtr = pByteStream + 2; + /*lint -e(716) while(1) used consciously */ + while (1) + { + byte = *readPtr++; + byteCount++; + + if (byteCount == len) + { + /* no start code prefix found -> error */ + *readBytes = len; + return(HANTRO_NOK); + } + + if (!byte) + zeroCount++; + else if ((byte == 0x01) && (zeroCount >= 2)) + break; + else + zeroCount = 0; + } + + initByteCount = byteCount; + + /* determine size of the NAL unit. Search for next start code prefix + * or end of stream and ignore possible trailing zero bytes */ + zeroCount = 0; + /*lint -e(716) while(1) used consciously */ + while (1) + { + byte = *readPtr++; + byteCount++; + if (!byte) + zeroCount++; + + if ( (byte == 0x03) && (zeroCount == 2) ) + { + hasEmulation = HANTRO_TRUE; + } + + if ( (byte == 0x01) && (zeroCount >= 2 ) ) + { + strmBuffSize = + byteCount - initByteCount - zeroCount - 1; + zeroCount -= MIN(zeroCount, 3); + break; + } + else if (byte) + { + if (zeroCount >= 3) + invalidStream = HANTRO_TRUE; + zeroCount = 0; + } + + if (byteCount == len) + { + strmBuffSize = byteCount - initByteCount - zeroCount; + break; + } + + } + } + /* separate NAL units as input -> just set stream params */ + else + { + initByteCount = 0; + zeroCount = 0; + strmBuffSize = len; + hasEmulation = HANTRO_TRUE; + } + + #ifdef H264BSD_N64 + pStrmData->pStart = pByteStream + initByteCount; + pStrmData->pCurr = (u64)(u32)pStrmData->pStart << 3; + pStrmData->pEnd = pStrmData->pCurr + strmBuffSize*8; + #else + pStrmData->pStrmBuffStart = pByteStream + initByteCount; + pStrmData->pStrmCurrPos = pStrmData->pStrmBuffStart; + pStrmData->bitPosInWord = 0; + pStrmData->strmBuffReadBits = 0; + pStrmData->strmBuffSize = strmBuffSize; + #endif + + /* return number of bytes "consumed" */ + *readBytes = strmBuffSize + initByteCount + zeroCount; + + if (invalidStream) + { + return(HANTRO_NOK); + } + + /* remove emulation prevention bytes before rbsp processing */ + if (hasEmulation) + { + tmp = strmBuffSize; + #ifdef H264BSD_N64 + readPtr = writePtr = pStrmData->pStart; + #else + readPtr = writePtr = pStrmData->pStrmBuffStart; + #endif + zeroCount = 0; + for (i = tmp; i--;) + { + if ((zeroCount == 2) && (*readPtr == 0x03)) + { + /* emulation prevention byte shall be followed by one of the + * following bytes: 0x00, 0x01, 0x02, 0x03. This implies that + * emulation prevention 0x03 byte shall not be the last byte + * of the stream. */ + if ( (i == 0) || (*(readPtr+1) > 0x03) ) + return(HANTRO_NOK); + + /* do not write emulation prevention byte */ + readPtr++; + zeroCount = 0; + } + else + { + /* NAL unit shall not contain byte sequences 0x000000, + * 0x000001 or 0x000002 */ + if ( (zeroCount == 2) && (*readPtr <= 0x02) ) + return(HANTRO_NOK); + + if (*readPtr == 0) + zeroCount++; + else + zeroCount = 0; + + *writePtr++ = *readPtr++; + } + } + + /* (readPtr - writePtr) indicates number of "removed" emulation + * prevention bytes -> subtract from stream buffer size */ + #ifdef H264BSD_N64 + pStrmData->pEnd -= (u32)(readPtr - writePtr)*8; + #else + pStrmData->strmBuffSize -= (u32)(readPtr - writePtr); + #endif + } + + return(HANTRO_OK); + +} + diff --git a/src/video/h264_decoder/h264bsd_byte_stream.h b/src/video/h264_decoder/h264bsd_byte_stream.h new file mode 100644 index 000000000..36aec76e9 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_byte_stream.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_BYTE_STREAM_H +#define H264SWDEC_BYTE_STREAM_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_stream.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +u32 h264bsdExtractNalUnit(u8 *pByteStream, u32 len, strmData_t *pStrmData, + u32 *readBytes); + +#endif /* #ifdef H264SWDEC_BYTE_STREAM_H */ + diff --git a/src/video/h264_decoder/h264bsd_cavlc.c b/src/video/h264_decoder/h264bsd_cavlc.c new file mode 100644 index 000000000..70ef6ea16 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_cavlc.c @@ -0,0 +1,995 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + DecodeCoeffToken + DecodeLevelPrefix + DecodeTotalZeros + DecodeRunBefore + DecodeResidualBlockCavlc + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_cavlc.h" +#include "h264bsd_util.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/* Following descriptions use term "information field" to represent combination + * of certain decoded symbol value and the length of the corresponding variable + * length code word. For example, total_zeros information field consists of + * 4 bits symbol value (bits [4,7]) along with four bits to represent length + * of the VLC code word (bits [0,3]) */ + +/* macro to obtain length of the coeff token information field, bits [0,4] */ +#define LENGTH_TC(vlc) ((vlc) & 0x1F) +/* macro to obtain length of the other information fields, bits [0,3] */ +#define LENGTH(vlc) ((vlc) & 0xF) +/* macro to obtain code word from the information fields, bits [4,7] */ +#define INFO(vlc) (((vlc) >> 4) & 0xF) /* 4 MSB bits contain information */ +/* macro to obtain trailing ones from the coeff token information word, + * bits [5,10] */ +#define TRAILING_ONES(coeffToken) ((coeffToken>>5) & 0x3F) +/* macro to obtain total coeff from the coeff token information word, + * bits [11,15] */ +#define TOTAL_COEFF(coeffToken) (((coeffToken) >> 11) & 0x1F) + +#define VLC_NOT_FOUND 0xFFFFFFFEU + +/* VLC tables for coeff_token. Because of long codes (max. 16 bits) some of the + * tables have been splitted into multiple separate tables. Each array/table + * element has the following structure: + * [5 bits for tot.coeff.] [6 bits for tr.ones] [5 bits for VLC length] + * If there is a 0x0000 value, it means that there is not corresponding VLC + * codeword for that index. */ + +/* VLC lengths up to 6 bits, 0 <= nC < 2 */ +static const u16 coeffToken0_0[32] = { + 0x0000,0x0000,0x0000,0x2066,0x1026,0x0806,0x1865,0x1865, + 0x1043,0x1043,0x1043,0x1043,0x1043,0x1043,0x1043,0x1043, + 0x0822,0x0822,0x0822,0x0822,0x0822,0x0822,0x0822,0x0822, + 0x0822,0x0822,0x0822,0x0822,0x0822,0x0822,0x0822,0x0822}; + +/* VLC lengths up to 10 bits, 0 <= nC < 2 */ +static const u16 coeffToken0_1[48] = { + 0x0000,0x0000,0x0000,0x0000,0x406a,0x304a,0x282a,0x200a, + 0x3869,0x3869,0x2849,0x2849,0x2029,0x2029,0x1809,0x1809, + 0x3068,0x3068,0x3068,0x3068,0x2048,0x2048,0x2048,0x2048, + 0x1828,0x1828,0x1828,0x1828,0x1008,0x1008,0x1008,0x1008, + 0x2867,0x2867,0x2867,0x2867,0x2867,0x2867,0x2867,0x2867, + 0x1847,0x1847,0x1847,0x1847,0x1847,0x1847,0x1847,0x1847}; + +/* VLC lengths up to 14 bits, 0 <= nC < 2 */ +static const u16 coeffToken0_2[56] = { + 0x606e,0x584e,0x502e,0x500e,0x586e,0x504e,0x482e,0x480e, + 0x400d,0x400d,0x484d,0x484d,0x402d,0x402d,0x380d,0x380d, + 0x506d,0x506d,0x404d,0x404d,0x382d,0x382d,0x300d,0x300d, + 0x486b,0x486b,0x486b,0x486b,0x486b,0x486b,0x486b,0x486b, + 0x384b,0x384b,0x384b,0x384b,0x384b,0x384b,0x384b,0x384b, + 0x302b,0x302b,0x302b,0x302b,0x302b,0x302b,0x302b,0x302b, + 0x280b,0x280b,0x280b,0x280b,0x280b,0x280b,0x280b,0x280b}; + +/* VLC lengths up to 16 bits, 0 <= nC < 2 */ +static const u16 coeffToken0_3[32] = { + 0x0000,0x0000,0x682f,0x682f,0x8010,0x8050,0x8030,0x7810, + 0x8070,0x7850,0x7830,0x7010,0x7870,0x7050,0x7030,0x6810, + 0x706f,0x706f,0x684f,0x684f,0x602f,0x602f,0x600f,0x600f, + 0x686f,0x686f,0x604f,0x604f,0x582f,0x582f,0x580f,0x580f}; + +/* VLC lengths up to 6 bits, 2 <= nC < 4 */ +static const u16 coeffToken2_0[32] = { + 0x0000,0x0000,0x0000,0x0000,0x3866,0x2046,0x2026,0x1006, + 0x3066,0x1846,0x1826,0x0806,0x2865,0x2865,0x1025,0x1025, + 0x2064,0x2064,0x2064,0x2064,0x1864,0x1864,0x1864,0x1864, + 0x1043,0x1043,0x1043,0x1043,0x1043,0x1043,0x1043,0x1043}; + +/* VLC lengths up to 9 bits, 2 <= nC < 4 */ +static const u16 coeffToken2_1[32] = { + 0x0000,0x0000,0x0000,0x0000,0x4869,0x3849,0x3829,0x3009, + 0x2808,0x2808,0x3048,0x3048,0x3028,0x3028,0x2008,0x2008, + 0x4067,0x4067,0x4067,0x4067,0x2847,0x2847,0x2847,0x2847, + 0x2827,0x2827,0x2827,0x2827,0x1807,0x1807,0x1807,0x1807}; + +/* VLC lengths up to 14 bits, 2 <= nC < 4 */ +static const u16 coeffToken2_2[128] = { + 0x0000,0x0000,0x786d,0x786d,0x806e,0x804e,0x802e,0x800e, + 0x782e,0x780e,0x784e,0x702e,0x704d,0x704d,0x700d,0x700d, + 0x706d,0x706d,0x684d,0x684d,0x682d,0x682d,0x680d,0x680d, + 0x686d,0x686d,0x604d,0x604d,0x602d,0x602d,0x600d,0x600d, + 0x580c,0x580c,0x580c,0x580c,0x584c,0x584c,0x584c,0x584c, + 0x582c,0x582c,0x582c,0x582c,0x500c,0x500c,0x500c,0x500c, + 0x606c,0x606c,0x606c,0x606c,0x504c,0x504c,0x504c,0x504c, + 0x502c,0x502c,0x502c,0x502c,0x480c,0x480c,0x480c,0x480c, + 0x586b,0x586b,0x586b,0x586b,0x586b,0x586b,0x586b,0x586b, + 0x484b,0x484b,0x484b,0x484b,0x484b,0x484b,0x484b,0x484b, + 0x482b,0x482b,0x482b,0x482b,0x482b,0x482b,0x482b,0x482b, + 0x400b,0x400b,0x400b,0x400b,0x400b,0x400b,0x400b,0x400b, + 0x506b,0x506b,0x506b,0x506b,0x506b,0x506b,0x506b,0x506b, + 0x404b,0x404b,0x404b,0x404b,0x404b,0x404b,0x404b,0x404b, + 0x402b,0x402b,0x402b,0x402b,0x402b,0x402b,0x402b,0x402b, + 0x380b,0x380b,0x380b,0x380b,0x380b,0x380b,0x380b,0x380b}; + +/* VLC lengths up to 6 bits, 4 <= nC < 8 */ +static const u16 coeffToken4_0[64] = { + 0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000, + 0x1806,0x3846,0x3826,0x1006,0x4866,0x3046,0x3026,0x0806, + 0x2825,0x2825,0x2845,0x2845,0x2025,0x2025,0x2045,0x2045, + 0x1825,0x1825,0x4065,0x4065,0x1845,0x1845,0x1025,0x1025, + 0x3864,0x3864,0x3864,0x3864,0x3064,0x3064,0x3064,0x3064, + 0x2864,0x2864,0x2864,0x2864,0x2064,0x2064,0x2064,0x2064, + 0x1864,0x1864,0x1864,0x1864,0x1044,0x1044,0x1044,0x1044, + 0x0824,0x0824,0x0824,0x0824,0x0004,0x0004,0x0004,0x0004}; + +/* VLC lengths up to 10 bits, 4 <= nC < 8 */ +static const u16 coeffToken4_1[128] = { + 0x0000,0x800a,0x806a,0x804a,0x802a,0x780a,0x786a,0x784a, + 0x782a,0x700a,0x706a,0x704a,0x702a,0x680a,0x6829,0x6829, + 0x6009,0x6009,0x6849,0x6849,0x6029,0x6029,0x5809,0x5809, + 0x6869,0x6869,0x6049,0x6049,0x5829,0x5829,0x5009,0x5009, + 0x6068,0x6068,0x6068,0x6068,0x5848,0x5848,0x5848,0x5848, + 0x5028,0x5028,0x5028,0x5028,0x4808,0x4808,0x4808,0x4808, + 0x5868,0x5868,0x5868,0x5868,0x5048,0x5048,0x5048,0x5048, + 0x4828,0x4828,0x4828,0x4828,0x4008,0x4008,0x4008,0x4008, + 0x3807,0x3807,0x3807,0x3807,0x3807,0x3807,0x3807,0x3807, + 0x3007,0x3007,0x3007,0x3007,0x3007,0x3007,0x3007,0x3007, + 0x4847,0x4847,0x4847,0x4847,0x4847,0x4847,0x4847,0x4847, + 0x2807,0x2807,0x2807,0x2807,0x2807,0x2807,0x2807,0x2807, + 0x5067,0x5067,0x5067,0x5067,0x5067,0x5067,0x5067,0x5067, + 0x4047,0x4047,0x4047,0x4047,0x4047,0x4047,0x4047,0x4047, + 0x4027,0x4027,0x4027,0x4027,0x4027,0x4027,0x4027,0x4027, + 0x2007,0x2007,0x2007,0x2007,0x2007,0x2007,0x2007,0x2007}; + +/* fixed 6 bit length VLC, nC <= 8 */ +static const u16 coeffToken8[64] = { + 0x0806,0x0826,0x0000,0x0006,0x1006,0x1026,0x1046,0x0000, + 0x1806,0x1826,0x1846,0x1866,0x2006,0x2026,0x2046,0x2066, + 0x2806,0x2826,0x2846,0x2866,0x3006,0x3026,0x3046,0x3066, + 0x3806,0x3826,0x3846,0x3866,0x4006,0x4026,0x4046,0x4066, + 0x4806,0x4826,0x4846,0x4866,0x5006,0x5026,0x5046,0x5066, + 0x5806,0x5826,0x5846,0x5866,0x6006,0x6026,0x6046,0x6066, + 0x6806,0x6826,0x6846,0x6866,0x7006,0x7026,0x7046,0x7066, + 0x7806,0x7826,0x7846,0x7866,0x8006,0x8026,0x8046,0x8066}; + +/* VLC lengths up to 3 bits, nC == -1 */ +static const u16 coeffTokenMinus1_0[8] = { + 0x0000,0x1043,0x0002,0x0002,0x0821,0x0821,0x0821,0x0821}; + +/* VLC lengths up to 8 bits, nC == -1 */ +static const u16 coeffTokenMinus1_1[32] = { + 0x2067,0x2067,0x2048,0x2028,0x1847,0x1847,0x1827,0x1827, + 0x2006,0x2006,0x2006,0x2006,0x1806,0x1806,0x1806,0x1806, + 0x1006,0x1006,0x1006,0x1006,0x1866,0x1866,0x1866,0x1866, + 0x1026,0x1026,0x1026,0x1026,0x0806,0x0806,0x0806,0x0806}; + +/* VLC tables for total_zeros. One table containing longer code, totalZeros_1, + * has been broken into two separate tables. Table elements have the + * following structure: + * [4 bits for info] [4 bits for VLC length] */ + +/* VLC lengths up to 5 bits */ +static const u8 totalZeros_1_0[32] = { + 0x00,0x00,0x65,0x55,0x44,0x44,0x34,0x34, + 0x23,0x23,0x23,0x23,0x13,0x13,0x13,0x13, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01}; + +/* VLC lengths up to 9 bits */ +static const u8 totalZeros_1_1[32] = { + 0x00,0xf9,0xe9,0xd9,0xc8,0xc8,0xb8,0xb8, + 0xa7,0xa7,0xa7,0xa7,0x97,0x97,0x97,0x97, + 0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86, + 0x76,0x76,0x76,0x76,0x76,0x76,0x76,0x76}; + +static const u8 totalZeros_2[64] = { + 0xe6,0xd6,0xc6,0xb6,0xa5,0xa5,0x95,0x95, + 0x84,0x84,0x84,0x84,0x74,0x74,0x74,0x74, + 0x64,0x64,0x64,0x64,0x54,0x54,0x54,0x54, + 0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43, + 0x33,0x33,0x33,0x33,0x33,0x33,0x33,0x33, + 0x23,0x23,0x23,0x23,0x23,0x23,0x23,0x23, + 0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13, + 0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03}; + +static const u8 totalZeros_3[64] = { + 0xd6,0xb6,0xc5,0xc5,0xa5,0xa5,0x95,0x95, + 0x84,0x84,0x84,0x84,0x54,0x54,0x54,0x54, + 0x44,0x44,0x44,0x44,0x04,0x04,0x04,0x04, + 0x73,0x73,0x73,0x73,0x73,0x73,0x73,0x73, + 0x63,0x63,0x63,0x63,0x63,0x63,0x63,0x63, + 0x33,0x33,0x33,0x33,0x33,0x33,0x33,0x33, + 0x23,0x23,0x23,0x23,0x23,0x23,0x23,0x23, + 0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13}; + +static const u8 totalZeros_4[32] = { + 0xc5,0xb5,0xa5,0x05,0x94,0x94,0x74,0x74, + 0x34,0x34,0x24,0x24,0x83,0x83,0x83,0x83, + 0x63,0x63,0x63,0x63,0x53,0x53,0x53,0x53, + 0x43,0x43,0x43,0x43,0x13,0x13,0x13,0x13}; + +static const u8 totalZeros_5[32] = { + 0xb5,0x95,0xa4,0xa4,0x84,0x84,0x24,0x24, + 0x14,0x14,0x04,0x04,0x73,0x73,0x73,0x73, + 0x63,0x63,0x63,0x63,0x53,0x53,0x53,0x53, + 0x43,0x43,0x43,0x43,0x33,0x33,0x33,0x33}; + +static const u8 totalZeros_6[64] = { + 0xa6,0x06,0x15,0x15,0x84,0x84,0x84,0x84, + 0x93,0x93,0x93,0x93,0x93,0x93,0x93,0x93, + 0x73,0x73,0x73,0x73,0x73,0x73,0x73,0x73, + 0x63,0x63,0x63,0x63,0x63,0x63,0x63,0x63, + 0x53,0x53,0x53,0x53,0x53,0x53,0x53,0x53, + 0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43, + 0x33,0x33,0x33,0x33,0x33,0x33,0x33,0x33, + 0x23,0x23,0x23,0x23,0x23,0x23,0x23,0x23}; + +static const u8 totalZeros_7[64] = { + 0x96,0x06,0x15,0x15,0x74,0x74,0x74,0x74, + 0x83,0x83,0x83,0x83,0x83,0x83,0x83,0x83, + 0x63,0x63,0x63,0x63,0x63,0x63,0x63,0x63, + 0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43, + 0x33,0x33,0x33,0x33,0x33,0x33,0x33,0x33, + 0x23,0x23,0x23,0x23,0x23,0x23,0x23,0x23, + 0x52,0x52,0x52,0x52,0x52,0x52,0x52,0x52, + 0x52,0x52,0x52,0x52,0x52,0x52,0x52,0x52}; + +static const u8 totalZeros_8[64] = { + 0x86,0x06,0x25,0x25,0x14,0x14,0x14,0x14, + 0x73,0x73,0x73,0x73,0x73,0x73,0x73,0x73, + 0x63,0x63,0x63,0x63,0x63,0x63,0x63,0x63, + 0x33,0x33,0x33,0x33,0x33,0x33,0x33,0x33, + 0x52,0x52,0x52,0x52,0x52,0x52,0x52,0x52, + 0x52,0x52,0x52,0x52,0x52,0x52,0x52,0x52, + 0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42, + 0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42}; + +static const u8 totalZeros_9[64] = { + 0x16,0x06,0x75,0x75,0x24,0x24,0x24,0x24, + 0x53,0x53,0x53,0x53,0x53,0x53,0x53,0x53, + 0x62,0x62,0x62,0x62,0x62,0x62,0x62,0x62, + 0x62,0x62,0x62,0x62,0x62,0x62,0x62,0x62, + 0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42, + 0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42, + 0x32,0x32,0x32,0x32,0x32,0x32,0x32,0x32, + 0x32,0x32,0x32,0x32,0x32,0x32,0x32,0x32}; + +static const u8 totalZeros_10[32] = { + 0x15,0x05,0x64,0x64,0x23,0x23,0x23,0x23, + 0x52,0x52,0x52,0x52,0x52,0x52,0x52,0x52, + 0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42, + 0x32,0x32,0x32,0x32,0x32,0x32,0x32,0x32}; + +static const u8 totalZeros_11[16] = { + 0x04,0x14,0x23,0x23,0x33,0x33,0x53,0x53, + 0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41}; + +static const u8 totalZeros_12[16] = { + 0x04,0x14,0x43,0x43,0x22,0x22,0x22,0x22, + 0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31}; + +static const u8 totalZeros_13[8] = {0x03,0x13,0x32,0x32,0x21,0x21,0x21,0x21}; + +static const u8 totalZeros_14[4] = {0x02,0x12,0x21,0x21}; + +/* VLC tables for run_before. Table elements have the following structure: + * [4 bits for info] [4bits for VLC length] + */ + +static const u8 runBefore_6[8] = {0x13,0x23,0x43,0x33,0x63,0x53,0x02,0x02}; + +static const u8 runBefore_5[8] = {0x53,0x43,0x33,0x23,0x12,0x12,0x02,0x02}; + +static const u8 runBefore_4[8] = {0x43,0x33,0x22,0x22,0x12,0x12,0x02,0x02}; + +static const u8 runBefore_3[4] = {0x32,0x22,0x12,0x02}; + +static const u8 runBefore_2[4] = {0x22,0x12,0x01,0x01}; + +static const u8 runBefore_1[2] = {0x11,0x01}; + +/* following four macros are used to handle stream buffer "cache" in the CAVLC + * decoding function */ + +#ifndef H264BSD_N64 + +/* macro to initialize stream buffer cache, fills the buffer (32 bits) */ +#define BUFFER_INIT(value, bits) \ + u32 bits = 32; \ + u32 value = h264bsdShowBits32(pStrmData); + +/* macro to read numBits bits from the buffer, bits will be written to + * outVal. Refills the buffer if not enough bits left */ +#define BUFFER_SHOW(value, bits, outVal, numBits) \ +{ \ + if (bits < (numBits)) \ + { \ + if(h264bsdFlushBits(pStrmData,32-bits) == END_OF_STREAM) \ + return(HANTRO_NOK); \ + value = h264bsdShowBits32(pStrmData); \ + bits = 32; \ + } \ + (outVal) = value >> (32 - (numBits)); \ +} + +/* macro to flush numBits bits from the buffer */ +#define BUFFER_FLUSH(value, bits, numBits) \ +{ \ + value <<= (numBits); \ + bits -= (numBits); \ +} + +/* macro to read and flush numBits bits from the buffer, bits will be written + * to outVal. Refills the buffer if not enough bits left */ +#define BUFFER_GET(value, bits, outVal, numBits) \ +{ \ + if (bits < (numBits)) \ + { \ + if(h264bsdFlushBits(pStrmData,32-bits) == END_OF_STREAM) \ + return(HANTRO_NOK); \ + value = h264bsdShowBits32(pStrmData); \ + bits = 32; \ + } \ + (outVal) = value >> (32 - (numBits)); \ + value <<= (numBits); \ + bits -= (numBits); \ +} + +#define BUFFER_CLOSE(bits) \ +{ \ + if (h264bsdFlushBits(pStrmData, 32-bufferBits) != HANTRO_OK) \ + return(HANTRO_NOK); \ +} + +#else + +// N64 has a 64-bit processor so we can locally cache a 64-bit value +// from which bits are extracted. + +/* macro to initialize stream buffer cache, fills the buffer (32 bits) */ +#define BUFFER_INIT(value, bits) \ + u32 bits; \ + u64 value = h264bsdShowBits64(pStrmData, &bits); + +/* macro to read numBits bits from the buffer, bits will be written to + * outVal. Refills the buffer if not enough bits left */ +#define BUFFER_SHOW(value, bits, outVal, numBits) \ +{ \ + if (bits < (numBits)) \ + { \ + if(h264bsdFlushBits(pStrmData,64-bits) == END_OF_STREAM) \ + return(HANTRO_NOK); \ + value = h264bsdShowBits64(pStrmData, &bits); \ + } \ + (outVal) = value >> (64 - (numBits)); \ +} + +/* macro to flush numBits bits from the buffer */ +#define BUFFER_FLUSH(value, bits, numBits) \ +{ \ + value <<= (numBits); \ + bits -= (numBits); \ +} + +/* macro to read and flush numBits bits from the buffer, bits will be written + * to outVal. Refills the buffer if not enough bits left */ +#define BUFFER_GET(value, bits, outVal, numBits) \ +{ \ + BUFFER_SHOW(value, bits, outVal, numBits); \ + BUFFER_FLUSH(value, bits, numBits); \ +} + +#define BUFFER_CLOSE(bits) \ +{ \ + if (h264bsdFlushBits(pStrmData, 64-bufferBits) != HANTRO_OK) \ + return(HANTRO_NOK); \ +} + +#endif + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +static u32 DecodeCoeffToken(u32 bits, u32 nc); + +static u32 DecodeLevelPrefix(u32 bits); + +static u32 DecodeTotalZeros(u32 bits, u32 totalCoeff, u32 isChromaDC); + +static u32 DecodeRunBefore(u32 bits,u32 zerosLeft); + +/*------------------------------------------------------------------------------ + + Function: DecodeCoeffToken + + Functional description: + Function to decode coeff_token information field from the stream. + + Inputs: + u32 bits next 16 stream bits + u32 nc nC, see standard for details + + Outputs: + u32 information field (11 bits for value, 5 bits for length) + +------------------------------------------------------------------------------*/ + +u32 DecodeCoeffToken(u32 bits, u32 nc) +{ + +/* Variables */ + + u32 value; + +/* Code */ + + /* standard defines that nc for decoding of chroma dc coefficients is -1, + * represented by u32 here -> -1 maps to 2^32 - 1 */ + ASSERT(nc <= 16 || nc == (u32)(-1)); + + if (nc < 2) + { + if (bits >= 0x8000) + { + value = 0x0001; + } + else if (bits >= 0x0C00) + value = coeffToken0_0[bits >> 10]; + else if (bits >= 0x0100) + value = coeffToken0_1[bits >> 6]; + else if (bits >= 0x0020) + value = coeffToken0_2[(bits>>2)-8]; + else + value = coeffToken0_3[bits]; + } + else if (nc < 4) + { + if (bits >= 0x8000) + { + value = bits & 0x4000 ? 0x0002 : 0x0822; + } + else if (bits >= 0x1000) + value = coeffToken2_0[bits >> 10]; + else if (bits >= 0x0200) + value = coeffToken2_1[bits >> 7]; + else + value = coeffToken2_2[bits>>2]; + } + else if (nc < 8) + { + value = coeffToken4_0[bits >> 10]; + if (!value) + value = coeffToken4_1[bits>>6]; + } + else if (nc <= 16) + { + value = coeffToken8[bits>>10]; + } + else + { + value = coeffTokenMinus1_0[bits >> 13]; + if (!value) + value = coeffTokenMinus1_1[bits>>8]; + } + + return(value); + +} + +/*------------------------------------------------------------------------------ + + Function: DecodeLevelPrefix + + Functional description: + Function to decode level_prefix information field from the stream + + Inputs: + u32 bits next 16 stream bits + + Outputs: + u32 level_prefix information field or VLC_NOT_FOUND + +------------------------------------------------------------------------------*/ + +u32 DecodeLevelPrefix(u32 bits) +{ +/* Variables */ + + u32 numZeros; + +/* Code */ + + if (bits >= 0x8000) + numZeros = 0; + else if (bits >= 0x4000) + numZeros = 1; + else if (bits >= 0x2000) + numZeros = 2; + else if (bits >= 0x1000) + numZeros = 3; + else if (bits >= 0x0800) + numZeros = 4; + else if (bits >= 0x0400) + numZeros = 5; + else if (bits >= 0x0200) + numZeros = 6; + else if (bits >= 0x0100) + numZeros = 7; + else if (bits >= 0x0080) + numZeros = 8; + else if (bits >= 0x0040) + numZeros = 9; + else if (bits >= 0x0020) + numZeros = 10; + else if (bits >= 0x0010) + numZeros = 11; + else if (bits >= 0x0008) + numZeros = 12; + else if (bits >= 0x0004) + numZeros = 13; + else if (bits >= 0x0002) + numZeros = 14; + else if (bits >= 0x0001) + numZeros = 15; + else /* more than 15 zeros encountered which is an error */ + return(VLC_NOT_FOUND); + + return(numZeros); +} + +/*------------------------------------------------------------------------------ + + Function: DecodeTotalZeros + + Functional description: + Function to decode total_zeros information field from the stream + + Inputs: + u32 bits next 9 stream bits + u32 totalCoeff total number of coefficients for the block + being decoded + u32 isChromaDC flag to indicate chroma DC block + + Outputs: + u32 information field (4 bits value, 4 bits length) + +------------------------------------------------------------------------------*/ + +u32 DecodeTotalZeros(u32 bits, u32 totalCoeff, u32 isChromaDC) +{ + +/* Variables */ + + u32 value = 0x0; + +/* Code */ + + ASSERT(totalCoeff); + + if (!isChromaDC) + { + ASSERT(totalCoeff < 16); + switch (totalCoeff) + { + case 1: + value = totalZeros_1_0[bits >> 4]; + if (!value) + value = totalZeros_1_1[bits]; + break; + + case 2: + value = totalZeros_2[bits >> 3]; + break; + + case 3: + value = totalZeros_3[bits >> 3]; + break; + + case 4: + value = totalZeros_4[bits >> 4]; + break; + + case 5: + value = totalZeros_5[bits >> 4]; + break; + + case 6: + value = totalZeros_6[bits >> 3]; + break; + + case 7: + value = totalZeros_7[bits >> 3]; + break; + + case 8: + value = totalZeros_8[bits >> 3]; + break; + + case 9: + value = totalZeros_9[bits >> 3]; + break; + + case 10: + value = totalZeros_10[bits >> 4]; + break; + + case 11: + value = totalZeros_11[bits >> 5]; + break; + + case 12: + value = totalZeros_12[bits >> 5]; + break; + + case 13: + value = totalZeros_13[bits >> 6]; + break; + + case 14: + value = totalZeros_14[bits >> 7]; + break; + + default: /* case 15 */ + value = (bits >> 8) ? 0x11 : 0x01; + break; + } + } + else + { + ASSERT(totalCoeff < 4); + bits >>= 6; + if (bits > 3) + value = 0x01; + else + { + if (totalCoeff == 3) + value = 0x11; + else if (bits > 1) + { + value = 0x12; + } + else if (totalCoeff == 2) + value = 0x22; + else if (bits) + value = 0x23; + else + value = 0x33; + } + } + + return(value); + +} + +/*------------------------------------------------------------------------------ + + Function: DecodeRunBefore + + Functional description: + Function to decode run_before information field from the stream + + Inputs: + u32 bits next 11 stream bits + u32 zerosLeft number of zeros left for the current block + + Outputs: + u32 information field (4 bits value, 4 bits length) + +------------------------------------------------------------------------------*/ + +u32 DecodeRunBefore(u32 bits, u32 zerosLeft) +{ + +/* Variables */ + + u32 value = 0x0; + +/* Code */ + + switch (zerosLeft) + { + case 1: + value = runBefore_1[bits>>10]; + break; + + case 2: + value = runBefore_2[bits>>9]; + break; + + case 3: + value = runBefore_3[bits>>9]; + break; + + case 4: + value = runBefore_4[bits>>8]; + break; + + case 5: + value = runBefore_5[bits>>8]; + break; + + case 6: + value = runBefore_6[bits>>8]; + break; + + default: + if (bits >= 0x100) + value = ((7-(bits>>8))<<4)+0x3; + else if (bits >= 0x80) + value = 0x74; + else if (bits >= 0x40) + value = 0x85; + else if (bits >= 0x20) + value = 0x96; + else if (bits >= 0x10) + value = 0xa7; + else if (bits >= 0x8) + value = 0xb8; + else if (bits >= 0x4) + value = 0xc9; + else if (bits >= 0x2) + value = 0xdA; + else if (bits) + value = 0xeB; + if (INFO(value) > zerosLeft) + value = 0; + break; + } + + return(value); + +} + +/*------------------------------------------------------------------------------ + + Function: DecodeResidualBlockCavlc + + Functional description: + Function to decode one CAVLC coded block. This corresponds to + syntax elements residual_block_cavlc() in the standard. + + Inputs: + pStrmData pointer to stream data structure + nc nC value + maxNumCoeff maximum number of residual coefficients + + Outputs: + coeffLevel stores decoded coefficient levels + + Returns: + numCoeffs on bits [4,11] if successful + coeffMap on bits [16,31] if successful, this is bit map + where each bit indicates if the corresponding + coefficient was zero (0) or non-zero (1) + HANTRO_NOK end of stream or error in stream + +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeResidualBlockCavlc( + strmData_t *pStrmData, + u8 **posCoefBuf, + i32 nc, + u32 maxNumCoeff) +{ + +/* Variables */ + + u32 i, tmp, totalCoeff, trailingOnes, suffixLength, levelPrefix; + u32 levelSuffix, zerosLeft, bit; + i16 level[16]; + u32 run[16]; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(posCoefBuf); + ASSERT(nc > -2); + ASSERT(maxNumCoeff == 4 || maxNumCoeff == 15 || maxNumCoeff == 16); + ASSERT(VLC_NOT_FOUND != END_OF_STREAM); + + /* assume that coeffLevel array has been "cleaned" by caller */ + + BUFFER_INIT(bufferValue, bufferBits); + + /*lint -e774 disable lint warning on always false comparison */ + BUFFER_SHOW(bufferValue, bufferBits, bit, 16); + /*lint +e774 */ + tmp = DecodeCoeffToken(bit, (u32)nc); + if (!tmp) + return(HANTRO_NOK); + BUFFER_FLUSH(bufferValue, bufferBits, LENGTH_TC(tmp)); + + totalCoeff = TOTAL_COEFF(tmp); + if (totalCoeff > maxNumCoeff) + return(HANTRO_NOK); + trailingOnes = TRAILING_ONES(tmp); + + if (totalCoeff != 0) + { + i = 0; + /* nonzero coefficients: +/- 1 */ + if (trailingOnes) + { + BUFFER_GET(bufferValue, bufferBits, bit, trailingOnes); + tmp = 1 << (trailingOnes - 1); + for (; tmp; i++) + { + level[i] = bit & tmp ? -1 : 1; + tmp >>= 1; + } + } + + /* other levels */ + u32 adder = 0; + suffixLength = 0; + if (trailingOnes < 3) { + if (totalCoeff > 10) + suffixLength = 1; + adder = 2; + } + + for (; i < totalCoeff; i++) + { + BUFFER_SHOW(bufferValue, bufferBits, bit, 16); + levelPrefix = DecodeLevelPrefix(bit); + if (levelPrefix == VLC_NOT_FOUND) + return(HANTRO_NOK); + BUFFER_FLUSH(bufferValue, bufferBits, levelPrefix+1); + + if (levelPrefix < 14) + tmp = suffixLength; + else if (levelPrefix == 14) + { + tmp = suffixLength ? suffixLength : 4; + } + else + { + /* setting suffixLength to 1 here corresponds to adding 15 + * to levelCode value if levelPrefix == 15 and + * suffixLength == 0 */ + if (!suffixLength) + suffixLength = 1; + tmp = 12; + } + + if (suffixLength) + levelPrefix <<= suffixLength; + + if (tmp) + { + BUFFER_GET(bufferValue, bufferBits, levelSuffix, tmp); + levelPrefix += levelSuffix; + } + + tmp = levelPrefix; + tmp += adder; + adder = 0; + + level[i] = (tmp+2)>>1; + + if (suffixLength == 0) + suffixLength = 1; + + if ((level[i] > (3 << (suffixLength - 1))) && suffixLength < 6) + suffixLength++; + + if (tmp & 0x1) + level[i] = -level[i]; + } + + /* zero runs */ + if (totalCoeff < maxNumCoeff) + { + BUFFER_SHOW(bufferValue, bufferBits, bit,9); + zerosLeft = DecodeTotalZeros(bit, totalCoeff, + (u32)(maxNumCoeff == 4)); + if (!zerosLeft) + return(HANTRO_NOK); + BUFFER_FLUSH(bufferValue, bufferBits, LENGTH(zerosLeft)); + zerosLeft = INFO(zerosLeft); + } + else + zerosLeft = 0; + + for (i = 0; i < totalCoeff - 1 && zerosLeft > 0; i++) + { + BUFFER_SHOW(bufferValue, bufferBits, bit,11); + tmp = DecodeRunBefore(bit, zerosLeft); + if (!tmp) + return(HANTRO_NOK); + BUFFER_FLUSH(bufferValue, bufferBits, LENGTH(tmp)); + run[i] = INFO(tmp); + zerosLeft -= run[i]++; + } + for (; i < totalCoeff-1; i++) { + run[i] = 1; + } + + /* combining level and run, levelSuffix variable used to hold coeffMap, + * i.e. bit map indicating which coefficients had non-zero value. */ + + /*lint -esym(771,level,run) level and run are always initialized */ + + // We want to emit packed delta buffers in the same format expected + // by OpenMAX functions later. + PROFILE_START(PS_H264_LAYER_RES_ENC); + + u8 *buf = *posCoefBuf; + u32 *crun = &run[totalCoeff-2]; + i16 *clevel = &level[totalCoeff-1]; + u8 idx = zerosLeft; + if (maxNumCoeff == 15) + idx++; + + int dozigzag = (maxNumCoeff > 4); + static const u8 zigzag[16] = { + 0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15 + }; + + levelSuffix = 0; + + // Iterate through all coefficients. Arrange the loop so that + // we iterate the last time with i==-1 + int i = totalCoeff-2; + do { + i32 lvl = *clevel--; + u8 idx2 = idx; + if (dozigzag) + idx2 = zigzag[idx]; + levelSuffix |= 1<>8; + buf[-3] |= 0x10; // 16-bit flag 0x10 + } + if (i >= 0) + idx += *crun--; + } while (i-- >= 0); + *posCoefBuf = buf; + + PROFILE_STOP(PS_H264_LAYER_RES_ENC); + + } + else + levelSuffix = 0; + + BUFFER_CLOSE(bufferBits); + + return((totalCoeff << 4) | (levelSuffix << 16)); +} + diff --git a/src/video/h264_decoder/h264bsd_cavlc.h b/src/video/h264_decoder/h264bsd_cavlc.h new file mode 100644 index 000000000..7fdeb47d2 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_cavlc.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_CAVLC_H +#define H264SWDEC_CAVLC_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_stream.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeResidualBlockCavlc( + strmData_t *pStrmData, + u8 **posCoefBuf, + i32 nc, + u32 maxNumCoeff); + +#endif /* #ifdef H264SWDEC_CAVLC_H */ + diff --git a/src/video/h264_decoder/h264bsd_cfg.h b/src/video/h264_decoder/h264bsd_cfg.h new file mode 100644 index 000000000..2baba5add --- /dev/null +++ b/src/video/h264_decoder/h264bsd_cfg.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_CFG_H +#define H264SWDEC_CFG_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +#define MAX_NUM_REF_PICS 16 +#define MAX_NUM_SLICE_GROUPS 8 +#define MAX_NUM_SEQ_PARAM_SETS 32 +#define MAX_NUM_PIC_PARAM_SETS 256 + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +#endif /* #ifdef H264SWDEC_CFG_H */ + diff --git a/src/video/h264_decoder/h264bsd_conceal.c b/src/video/h264_decoder/h264bsd_conceal.c new file mode 100644 index 000000000..90971a2be --- /dev/null +++ b/src/video/h264_decoder/h264bsd_conceal.c @@ -0,0 +1,634 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdConceal + ConcealMb + Transform + +------------------------------------------------------------------------------*/ + +#include "opttarget.h" + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_conceal.h" +#include "h264bsd_util.h" +#include "h264bsd_reconstruct.h" +#include "h264bsd_dpb.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/*lint -e702 disable lint warning on right shift of signed quantity */ + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +__attribute__((used)) +static u32 ConcealMb(mbStorage_t *pMb, image_t *currImage, u32 row, u32 col, + u32 sliceType, u8 *data); + +__attribute__((used)) +static void Transform(i32 *data); + +/*------------------------------------------------------------------------------ + + Function name: h264bsdConceal + + Functional description: + Perform error concealment for a picture. Two types of concealment + is performed based on sliceType: + 1) copy from previous picture for P-slices. + 2) concealment from neighbour pixels for I-slices + + I-type concealment is based on ideas presented by Jarno Tulkki. + The concealment algorithm determines frequency domain coefficients + from the neighbour pixels, applies integer transform (the same + transform used in the residual processing) and uses the results as + pixel values for concealed macroblocks. Transform produces 4x4 + array and one pixel value has to be used for 4x4 luma blocks and + 2x2 chroma blocks. + + Similar concealment is performed for whole picture (the choise + of the type is based on last successfully decoded slice header of + the picture but it is handled by the calling function). It is + acknowledged that this may result in wrong type of concealment + when a picture contains both types of slices. However, + determination of slice type macroblock-by-macroblock cannot + be done due to the fact that it is impossible to know to which + slice each corrupted (not successfully decoded) macroblock + belongs. + + The error concealment is started by searching the first propoerly + decoded macroblock and concealing the row containing the macroblock + in question. After that all macroblocks above the row in question + are concealed. Finally concealment of rows below is performed. + The order of concealment for 4x4 picture where macroblock 9 is the + first properly decoded one is as follows (properly decoded + macroblocks marked with 'x', numbers indicating the order of + concealment): + + 4 6 8 10 + 3 5 7 9 + 1 x x 2 + 11 12 13 14 + + If all macroblocks of the picture are lost, the concealment is + copy of previous picture for P-type and setting the image to + constant gray (pixel value 128) for I-type. + + Concealment sets quantization parameter of the concealed + macroblocks to value 40 and macroblock type to intra to enable + deblocking filter to smooth the edges of the concealed areas. + + Inputs: + pStorage pointer to storage structure + currImage pointer to current image structure + sliceType type of the slice + + Outputs: + currImage concealed macroblocks will be written here + + Returns: + HANTRO_OK + +------------------------------------------------------------------------------*/ + +u32 h264bsdConceal(storage_t *pStorage, image_t *currImage, u32 sliceType) +{ +#ifndef OPTIMIZE_NO_DECODED_FLAG + +/* Variables */ + + u32 i, j; + u32 row, col; + u32 width, height; + u8 *refData; + mbStorage_t *mb; + +/* Code */ + + ASSERT(pStorage); + ASSERT(currImage); + + DEBUG(("Concealing %s slice\n", IS_I_SLICE(sliceType) ? + "intra" : "inter")); + + width = currImage->width; + height = currImage->height; + refData = NULL; + /* use reference picture with smallest available index */ + if (IS_P_SLICE(sliceType) || (pStorage->intraConcealmentFlag != 0)) + { + i = 0; + do + { + refData = h264bsdGetRefPicData(pStorage->dpb, i); + i++; + if (i >= 16) + break; + } while (refData == NULL); + } + + i = row = col = 0; + /* find first properly decoded macroblock -> start point for concealment */ + while (i < pStorage->picSizeInMbs && !pStorage->mb[i].decoded) + { + i++; + col++; + if (col == width) + { + row++; + col = 0; + } + } + + /* whole picture lost -> copy previous or set grey */ + if (i == pStorage->picSizeInMbs) + { + if ( (IS_I_SLICE(sliceType) && (pStorage->intraConcealmentFlag == 0)) || + refData == NULL) + H264SwDecMemset(currImage->data, 128, width*height*384); + else + H264SwDecMemcpy(currImage->data, refData, width*height*384); + + pStorage->numConcealedMbs = pStorage->picSizeInMbs; + + /* no filtering if whole picture concealed */ + for (i = 0; i < pStorage->picSizeInMbs; i++) + pStorage->mb[i].disableDeblockingFilterIdc = 1; + + return(HANTRO_OK); + } + + /* start from the row containing the first correct macroblock, conceal the + * row in question, all rows above that row and then continue downwards */ + mb = pStorage->mb + row * width; + for (j = col; j--;) + { + ConcealMb(mb+j, currImage, row, j, sliceType, refData); + mb[j].decoded = 1; + pStorage->numConcealedMbs++; + } + for (j = col + 1; j < width; j++) + { + if (!mb[j].decoded) + { + ConcealMb(mb+j, currImage, row, j, sliceType, refData); + mb[j].decoded = 1; + pStorage->numConcealedMbs++; + } + } + /* if previous row(s) could not be concealed -> conceal them now */ + if (row) + { + for (j = 0; j < width; j++) + { + i = row - 1; + mb = pStorage->mb + i*width + j; + do + { + ConcealMb(mb, currImage, i, j, sliceType, refData); + mb->decoded = 1; + pStorage->numConcealedMbs++; + mb -= width; + } while(i--); + } + } + + /* process rows below the one containing the first correct macroblock */ + for (i = row + 1; i < height; i++) + { + mb = pStorage->mb + i * width; + + for (j = 0; j < width; j++) + { + if (!mb[j].decoded) + { + ConcealMb(mb+j, currImage, i, j, sliceType, refData); + mb[j].decoded = 1; + pStorage->numConcealedMbs++; + } + } + } +#endif + + return(HANTRO_OK); +} + +/*------------------------------------------------------------------------------ + + Function name: ConcealMb + + Functional description: + Perform error concealment for one macroblock, location of the + macroblock in the picture indicated by row and col + +------------------------------------------------------------------------------*/ + +u32 ConcealMb(mbStorage_t *pMb, image_t *currImage, u32 row, u32 col, + u32 sliceType, u8 *refData) +{ +#ifndef OPTIMIZE_NO_DECODED_FLAG +/* Variables */ + + u32 i, j, comp; + u32 hor, ver; + u32 mbNum; + u32 width, height; + u8 *mbPos; + u8 data[384]; + u8 *pData; + i32 tmp; + i32 firstPhase[16]; + i32 *pTmp; + /* neighbours above, below, left and right */ + i32 a[4] = { 0,0,0,0 }, b[4], l[4] = { 0,0,0,0 }, r[4]; + u32 A, B, L, R; + u8 fillBuff[32*21 + 15 + 32]; + u8 *pFill; +/* Code */ + + ASSERT(pMb); + ASSERT(!pMb->decoded); + ASSERT(currImage); + ASSERT(col < currImage->width); + ASSERT(row < currImage->height); + + pFill = ALIGN(fillBuff, 16); + width = currImage->width; + height = currImage->height; + mbNum = row * width + col; + + h264bsdSetCurrImageMbPointers(currImage, mbNum); + + mbPos = currImage->data + row * 16 * width * 16 + col * 16; + A = B = L = R = HANTRO_FALSE; + + /* set qpY to 40 to enable some filtering in deblocking (stetson value) */ + pMb->qpY = 40; + pMb->disableDeblockingFilterIdc = 0; + /* mbType set to intra to perform filtering despite the values of other + * boundary strength determination fields */ + pMb->mbType = I_4x4; + pMb->filterOffsetA = 0; + pMb->filterOffsetB = 0; + pMb->chromaQpIndexOffset = 0; + + if (IS_I_SLICE(sliceType)) + H264SwDecMemset(data, 0, sizeof(data)); + else + { + mv_t mv = {0,0}; + image_t refImage; + refImage.width = width; + refImage.height = height; + refImage.data = refData; + image_t image; + image.width = width; + image.height = height; + image.data = data; + image.luma = data; + image.cb = data + 256; + image.cr = data + 256 + 64; + if (refImage.data) + { + h264bsdPredictSamples(&image, &mv, &refImage, + ((row*16) + ((col*16)<<16)), + 0x00001010, pFill); + h264bsdWriteMacroblock(currImage, data, 1); + + return(HANTRO_OK); + } + else + H264SwDecMemset(data, 0, sizeof(data)); + } + + H264SwDecMemset(firstPhase, 0, sizeof(firstPhase)); + + /* counter for number of neighbours used */ + j = 0; + hor = ver = 0; + if (row && (pMb-width)->decoded) + { + A = HANTRO_TRUE; + pData = mbPos - width*16; + a[0] = *pData++; a[0] += *pData++; a[0] += *pData++; a[0] += *pData++; + a[1] = *pData++; a[1] += *pData++; a[1] += *pData++; a[1] += *pData++; + a[2] = *pData++; a[2] += *pData++; a[2] += *pData++; a[2] += *pData++; + a[3] = *pData++; a[3] += *pData++; a[3] += *pData++; a[3] += *pData++; + j++; + hor++; + firstPhase[0] += a[0] + a[1] + a[2] + a[3]; + firstPhase[1] += a[0] + a[1] - a[2] - a[3]; + } + if ((row != height - 1) && (pMb+width)->decoded) + { + B = HANTRO_TRUE; + pData = mbPos + 16*width*16; + b[0] = *pData++; b[0] += *pData++; b[0] += *pData++; b[0] += *pData++; + b[1] = *pData++; b[1] += *pData++; b[1] += *pData++; b[1] += *pData++; + b[2] = *pData++; b[2] += *pData++; b[2] += *pData++; b[2] += *pData++; + b[3] = *pData++; b[3] += *pData++; b[3] += *pData++; b[3] += *pData++; + j++; + hor++; + firstPhase[0] += b[0] + b[1] + b[2] + b[3]; + firstPhase[1] += b[0] + b[1] - b[2] - b[3]; + } + if (col && (pMb-1)->decoded) + { + L = HANTRO_TRUE; + pData = mbPos - 1; + l[0] = pData[0]; l[0] += pData[16*width]; + l[0] += pData[32*width]; l[0] += pData[48*width]; + pData += 64*width; + l[1] = pData[0]; l[1] += pData[16*width]; + l[1] += pData[32*width]; l[1] += pData[48*width]; + pData += 64*width; + l[2] = pData[0]; l[2] += pData[16*width]; + l[2] += pData[32*width]; l[2] += pData[48*width]; + pData += 64*width; + l[3] = pData[0]; l[3] += pData[16*width]; + l[3] += pData[32*width]; l[3] += pData[48*width]; + j++; + ver++; + firstPhase[0] += l[0] + l[1] + l[2] + l[3]; + firstPhase[4] += l[0] + l[1] - l[2] - l[3]; + } + if ((col != width - 1) && (pMb+1)->decoded) + { + R = HANTRO_TRUE; + pData = mbPos + 16; + r[0] = pData[0]; r[0] += pData[16*width]; + r[0] += pData[32*width]; r[0] += pData[48*width]; + pData += 64*width; + r[1] = pData[0]; r[1] += pData[16*width]; + r[1] += pData[32*width]; r[1] += pData[48*width]; + pData += 64*width; + r[2] = pData[0]; r[2] += pData[16*width]; + r[2] += pData[32*width]; r[2] += pData[48*width]; + pData += 64*width; + r[3] = pData[0]; r[3] += pData[16*width]; + r[3] += pData[32*width]; r[3] += pData[48*width]; + j++; + ver++; + firstPhase[0] += r[0] + r[1] + r[2] + r[3]; + firstPhase[4] += r[0] + r[1] - r[2] - r[3]; + } + + /* at least one properly decoded neighbour available */ + // FIXME(rasky): for some reason, enabling this assert messes up with + // GCC warning to detect maybe-uninitialized uses in the following statements. + // Comment it out for now. + //ASSERT(j); + + /*lint -esym(644,l,r,a,b) variable initialized above */ + if (!hor && L && R) + firstPhase[1] = (l[0]+l[1]+l[2]+l[3]-r[0]-r[1]-r[2]-r[3]) >> 5; + else if (hor) + firstPhase[1] >>= (3+hor); + + if (!ver && A && B) + firstPhase[4] = (a[0]+a[1]+a[2]+a[3]-b[0]-b[1]-b[2]-b[3]) >> 5; + else if (ver) + firstPhase[4] >>= (3+ver); + + switch (j) + { + case 1: + firstPhase[0] >>= 4; + break; + + case 2: + firstPhase[0] >>= 5; + break; + + case 3: + /* approximate (firstPhase[0]*4/3)>>6 */ + firstPhase[0] = (21 * firstPhase[0]) >> 10; + break; + + default: /* 4 */ + firstPhase[0] >>= 6; + break; + + } + + + Transform(firstPhase); + + for (i = 0, pData = data, pTmp = firstPhase; i < 256;) + { + tmp = pTmp[(i & 0xF)>>2]; + /*lint -e734 CLIP1 macro results in value that fits into 8 bits */ + *pData++ = CLIP1(tmp); + /*lint +e734 */ + + i++; + if (!(i & 0x3F)) + pTmp += 4; + } + + /* chroma components */ + mbPos = currImage->data + width * height * 256 + + row * 8 * width * 8 + col * 8; + for (comp = 0; comp < 2; comp++) + { + + H264SwDecMemset(firstPhase, 0, sizeof(firstPhase)); + + /* counter for number of neighbours used */ + j = 0; + hor = ver = 0; + if (A) + { + pData = mbPos - width*8; + a[0] = *pData++; a[0] += *pData++; + a[1] = *pData++; a[1] += *pData++; + a[2] = *pData++; a[2] += *pData++; + a[3] = *pData++; a[3] += *pData++; + j++; + hor++; + firstPhase[0] += a[0] + a[1] + a[2] + a[3]; + firstPhase[1] += a[0] + a[1] - a[2] - a[3]; + } + if (B) + { + pData = mbPos + 8*width*8; + b[0] = *pData++; b[0] += *pData++; + b[1] = *pData++; b[1] += *pData++; + b[2] = *pData++; b[2] += *pData++; + b[3] = *pData++; b[3] += *pData++; + j++; + hor++; + firstPhase[0] += b[0] + b[1] + b[2] + b[3]; + firstPhase[1] += b[0] + b[1] - b[2] - b[3]; + } + if (L) + { + pData = mbPos - 1; + l[0] = pData[0]; l[0] += pData[8*width]; + pData += 16*width; + l[1] = pData[0]; l[1] += pData[8*width]; + pData += 16*width; + l[2] = pData[0]; l[2] += pData[8*width]; + pData += 16*width; + l[3] = pData[0]; l[3] += pData[8*width]; + j++; + ver++; + firstPhase[0] += l[0] + l[1] + l[2] + l[3]; + firstPhase[4] += l[0] + l[1] - l[2] - l[3]; + } + if (R) + { + pData = mbPos + 8; + r[0] = pData[0]; r[0] += pData[8*width]; + pData += 16*width; + r[1] = pData[0]; r[1] += pData[8*width]; + pData += 16*width; + r[2] = pData[0]; r[2] += pData[8*width]; + pData += 16*width; + r[3] = pData[0]; r[3] += pData[8*width]; + j++; + ver++; + firstPhase[0] += r[0] + r[1] + r[2] + r[3]; + firstPhase[4] += r[0] + r[1] - r[2] - r[3]; + } + if (!hor && L && R) + firstPhase[1] = (l[0]+l[1]+l[2]+l[3]-r[0]-r[1]-r[2]-r[3]) >> 4; + else if (hor) + firstPhase[1] >>= (2+hor); + + if (!ver && A && B) + firstPhase[4] = (a[0]+a[1]+a[2]+a[3]-b[0]-b[1]-b[2]-b[3]) >> 4; + else if (ver) + firstPhase[4] >>= (2+ver); + + switch (j) + { + case 1: + firstPhase[0] >>= 3; + break; + + case 2: + firstPhase[0] >>= 4; + break; + + case 3: + /* approximate (firstPhase[0]*4/3)>>5 */ + firstPhase[0] = (21 * firstPhase[0]) >> 9; + break; + + default: /* 4 */ + firstPhase[0] >>= 5; + break; + + } + + Transform(firstPhase); + + pData = data + 256 + comp*64; + for (i = 0, pTmp = firstPhase; i < 64;) + { + tmp = pTmp[(i & 0x7)>>1]; + /*lint -e734 CLIP1 macro results in value that fits into 8 bits */ + *pData++ = CLIP1(tmp); + /*lint +e734 */ + + i++; + if (!(i & 0xF)) + pTmp += 4; + } + + /* increment pointers for cr */ + mbPos += width * height * 64; + } + + h264bsdWriteMacroblock(currImage, data, 1); +#endif + + return(HANTRO_OK); + +} + + +/*------------------------------------------------------------------------------ + + Function name: Transform + + Functional description: + Simplified transform, assuming that only dc component and lowest + horizontal and lowest vertical component may be non-zero + +------------------------------------------------------------------------------*/ + +void Transform(i32 *data) +{ + + u32 col; + i32 tmp0, tmp1; + + if (!data[1] && !data[4]) + { + data[1] = data[2] = data[3] = data[4] = data[5] = + data[6] = data[7] = data[8] = data[9] = data[10] = + data[11] = data[12] = data[13] = data[14] = data[15] = data[0]; + return; + } + /* first horizontal transform for rows 0 and 1 */ + tmp0 = data[0]; + tmp1 = data[1]; + data[0] = tmp0 + tmp1; + data[1] = tmp0 + (tmp1>>1); + data[2] = tmp0 - (tmp1>>1); + data[3] = tmp0 - tmp1; + + tmp0 = data[4]; + data[5] = tmp0; + data[6] = tmp0; + data[7] = tmp0; + + /* then vertical transform */ + for (col = 4; col--; data++) + { + tmp0 = data[0]; + tmp1 = data[4]; + data[0] = tmp0 + tmp1; + data[4] = tmp0 + (tmp1>>1); + data[8] = tmp0 - (tmp1>>1); + data[12] = tmp0 - tmp1; + } + +} +/*lint +e702 */ + diff --git a/src/video/h264_decoder/h264bsd_conceal.h b/src/video/h264_decoder/h264bsd_conceal.h new file mode 100644 index 000000000..3134670c8 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_conceal.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_CONCEAL_H +#define H264SWDEC_CONCEAL_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_slice_header.h" +#include "h264bsd_storage.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +u32 h264bsdConceal(storage_t *pStorage, image_t *currImage, u32 sliceType); + +#endif /* #ifdef H264SWDEC_CONCEAL_H */ + diff --git a/src/video/h264_decoder/h264bsd_container.h b/src/video/h264_decoder/h264bsd_container.h new file mode 100644 index 000000000..284297523 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_container.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_CONTAINER_H +#define H264SWDEC_CONTAINER_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_storage.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/* String length for tracing */ +#define H264DEC_TRACE_STR_LEN 100 + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +typedef enum { + UNINITIALIZED, + INITIALIZED, + NEW_HEADERS +} decStat_enum; + +typedef struct +{ + decStat_enum decStat; + + u32 picNumber; + storage_t storage; +#ifdef H264DEC_TRACE + char str[H264DEC_TRACE_STR_LEN]; +#endif +} decContainer_t; + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +#endif /* #ifdef H264SWDEC_DECCONTAINER_H */ + diff --git a/src/video/h264_decoder/h264bsd_deblocking.c b/src/video/h264_decoder/h264bsd_deblocking.c new file mode 100644 index 000000000..40702db8e --- /dev/null +++ b/src/video/h264_decoder/h264bsd_deblocking.c @@ -0,0 +1,1123 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdFilterPicture + FilterVerLumaEdge + FilterHorLumaEdge + FilterHorLuma + FilterVerChromaEdge + FilterHorChromaEdge + FilterHorChroma + InnerBoundaryStrength + EdgeBoundaryStrength + GetBoundaryStrengths + IsSliceBoundaryOnLeft + IsSliceBoundaryOnTop + GetMbFilteringFlags + GetLumaEdgeThresholds + GetChromaEdgeThresholds + FilterLuma + FilterChroma + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_util.h" +#include "h264bsd_macroblock_layer.h" +#include "h264bsd_deblocking.h" +#include "h264bsd_dpb.h" + +#include "omxdl/omxtypes.h" +#include "omxdl/omxVC.h" +#include "omxdl/armVC.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/* Switch off the following Lint messages for this file: + * Info 701: Shift left of signed quantity (int) + * Info 702: Shift right of signed quantity (int) + */ +/*lint -e701 -e702 */ + +/* array of alpha values, from the standard */ +static const u8 alphas[52] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,4,5,6,7,8,9,10, + 12,13,15,17,20,22,25,28,32,36,40,45,50,56,63,71,80,90,101,113,127,144,162, + 182,203,226,255,255}; + +/* array of beta values, from the standard */ +static const u8 betas[52] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,3,3,3,3,4,4, + 4,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,14,14,15,15,16,16,17,17,18,18}; + + + +/* array of tc0 values, from the standard, each triplet corresponds to a + * column in the table. Indexing goes as tc0[indexA][bS] */ +static const u8 tc0[52][5] = { + {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, {0, 0, 0, 1, 0}, {0, 0, 0, 1, 0}, {0, 0, 0, 1, 0}, + {0, 0, 0, 1, 0}, {0, 0, 1, 1, 0}, {0, 0, 1, 1, 0}, {0, 1, 1, 1, 0}, + {0, 1, 1, 1, 0}, {0, 1, 1, 1, 0}, {0, 1, 1, 1, 0}, {0, 1, 1, 2, 0}, + {0, 1, 1, 2, 0}, {0, 1, 1, 2, 0}, {0, 1, 1, 2, 0}, {0, 1, 2, 3, 0}, + {0, 1, 2, 3, 0}, {0, 2, 2, 3, 0}, {0, 2, 2, 4, 0}, {0, 2, 3, 4, 0}, + {0, 2, 3, 4, 0}, {0, 3, 3, 5, 0}, {0, 3, 4, 6, 0}, {0, 3, 4, 6, 0}, + {0, 4, 5, 7, 0}, {0, 4, 5, 8, 0}, {0, 4, 6, 9, 0}, {0, 5, 7, 10, 0}, + {0, 6, 8, 11, 0}, {0, 6, 8, 13, 0}, {0, 7, 10, 14, 0}, + {0, 8, 11, 16, 0}, {0, 9, 12, 18, 0}, {0, 10, 13, 20, 0}, + {0, 11, 15, 23, 0}, {0, 13, 17, 25, 0} +}; + + + +#define FILTER_LEFT_EDGE 0x04 +#define FILTER_TOP_EDGE 0x02 +#define FILTER_INNER_EDGE 0x01 + + +/* clipping table defined in intra_prediction.c */ +extern const u8 h264bsdClip[]; + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +static u32 InnerBoundaryStrength(mbStorage_t *mb1, u32 i1, u32 i2); + +static u32 InnerBoundaryStrength2(mbStorage_t *mb1, u32 i1, u32 i2); +static u32 EdgeBoundaryStrengthLeft(mbStorage_t *mb1, mbStorage_t *mb2); +static u32 EdgeBoundaryStrengthTop(mbStorage_t *mb1, mbStorage_t *mb2); + +static u32 IsSliceBoundaryOnLeft(mbStorage_t *mb); + +static u32 IsSliceBoundaryOnTop(mbStorage_t *mb); + +static u32 GetMbFilteringFlags(mbStorage_t *mb); + + +static u32 GetBoundaryStrengths(mbStorage_t *mb, u8 (*bs)[16], u32 flags); + +static void GetLumaEdgeThresholds( + mbStorage_t *mb, + u8 (*alpha)[2], + u8 (*beta)[2], + u8 (*threshold)[16], + u8 (*bs)[16], + u32 filteringFlags ); + +static void GetChromaEdgeThresholds( + mbStorage_t *mb, + u8 (*alpha)[2], + u8 (*beta)[2], + u8 (*threshold)[8], + u8 (*bs)[16], + u32 filteringFlags, + i32 chromaQpIndexOffset); + + +/*------------------------------------------------------------------------------ + + Function: IsSliceBoundaryOnLeft + + Functional description: + Function to determine if there is a slice boundary on the left side + of a macroblock. + +------------------------------------------------------------------------------*/ +u32 IsSliceBoundaryOnLeft(mbStorage_t *mb) +{ + +/* Variables */ + +/* Code */ + + ASSERT(mb && mb->mbA); + + if (mb->sliceId != mb->mbA->sliceId) + return(HANTRO_TRUE); + else + return(HANTRO_FALSE); + +} + +/*------------------------------------------------------------------------------ + + Function: IsSliceBoundaryOnTop + + Functional description: + Function to determine if there is a slice boundary above the + current macroblock. + +------------------------------------------------------------------------------*/ +u32 IsSliceBoundaryOnTop(mbStorage_t *mb) +{ + +/* Variables */ + +/* Code */ + + ASSERT(mb && mb->mbB); + + if (mb->sliceId != mb->mbB->sliceId) + return(HANTRO_TRUE); + else + return(HANTRO_FALSE); + +} + +/*------------------------------------------------------------------------------ + + Function: GetMbFilteringFlags + + Functional description: + Function to determine which edges of a macroblock has to be + filtered. Output is a bit-wise OR of FILTER_LEFT_EDGE, + FILTER_TOP_EDGE and FILTER_INNER_EDGE, depending on which edges + shall be filtered. + +------------------------------------------------------------------------------*/ +u32 GetMbFilteringFlags(mbStorage_t *mb) +{ + +/* Variables */ + + u32 flags = 0; + +/* Code */ + + ASSERT(mb); + + /* nothing will be filtered if disableDeblockingFilterIdc == 1 */ + if (mb->disableDeblockingFilterIdc != 1) + { + flags |= FILTER_INNER_EDGE; + + /* filterLeftMbEdgeFlag, left mb is MB_A */ + if (mb->mbA && + ((mb->disableDeblockingFilterIdc != 2) || + !IsSliceBoundaryOnLeft(mb))) + flags |= FILTER_LEFT_EDGE; + + /* filterTopMbEdgeFlag */ + if (mb->mbB && + ((mb->disableDeblockingFilterIdc != 2) || + !IsSliceBoundaryOnTop(mb))) + flags |= FILTER_TOP_EDGE; + } + + return(flags); + +} + +/*------------------------------------------------------------------------------ + + Function: InnerBoundaryStrength + + Functional description: + Function to calculate boundary strength value bs for an inner + edge of a macroblock. Macroblock type is checked before this is + called -> no intra mb condition here. + +------------------------------------------------------------------------------*/ +u32 InnerBoundaryStrength(mbStorage_t *mb1, u32 ind1, u32 ind2) +{ + i32 tmp1, tmp2; + i32 mv1, mv2, mv3, mv4; + + tmp1 = mb1->totalCoeff[ind1]; + tmp2 = mb1->totalCoeff[ind2]; + mv1 = mb1->mv[ind1].hor; + mv2 = mb1->mv[ind2].hor; + mv3 = mb1->mv[ind1].ver; + mv4 = mb1->mv[ind2].ver; + + if (tmp1 || tmp2) + { + return 2; + } + else if ( (ABS(mv1 - mv2) >= 4) || (ABS(mv3 - mv4) >= 4) || + (mb1->refAddr[ind1 >> 2] != mb1->refAddr[ind2 >> 2]) ) + { + return 1; + } + else + return 0; +} + +/*------------------------------------------------------------------------------ + + Function: InnerBoundaryStrength2 + + Functional description: + Function to calculate boundary strength value bs for an inner + edge of a macroblock. The function is the same as + InnerBoundaryStrength but without checking totalCoeff. + +------------------------------------------------------------------------------*/ +u32 InnerBoundaryStrength2(mbStorage_t *mb1, u32 ind1, u32 ind2) +{ + i32 tmp1, tmp2, tmp3, tmp4; + + tmp1 = mb1->mv[ind1].hor; + tmp2 = mb1->mv[ind2].hor; + tmp3 = mb1->mv[ind1].ver; + tmp4 = mb1->mv[ind2].ver; + + if ( (ABS(tmp1 - tmp2) >= 4) || (ABS(tmp3 - tmp4) >= 4) || + (mb1->refAddr[ind1 >> 2] != mb1->refAddr[ind2 >> 2])) + { + return 1; + } + else + return 0; +} + + +/*------------------------------------------------------------------------------ + + Function: EdgeBoundaryStrengthTop + + Functional description: + Function to calculate boundary strength value bs for + top-most edge of a macroblock. Macroblock types are checked + before this is called -> no intra mb conditions here. + +------------------------------------------------------------------------------*/ +u32 EdgeBoundaryStrengthTop(mbStorage_t *mb1, mbStorage_t *mb2) +{ + u32 topBs = 0; + u32 tmp1, tmp2, tmp3, tmp4; + + tmp1 = mb1->totalCoeff[0]; + tmp2 = mb2->totalCoeff[10]; + tmp3 = mb1->totalCoeff[1]; + tmp4 = mb2->totalCoeff[11]; + if (tmp1 || tmp2) + { + topBs = 2<<0; + } + else if ((ABS(mb1->mv[0].hor - mb2->mv[10].hor) >= 4) || + (ABS(mb1->mv[0].ver - mb2->mv[10].ver) >= 4) || + (mb1->refAddr[0] != mb2->refAddr[10 >> 2])) + { + topBs = 1<<0; + } + tmp1 = mb1->totalCoeff[4]; + tmp2 = mb2->totalCoeff[14]; + if (tmp3 || tmp4) + { + topBs += 2<<8; + } + else if ((ABS(mb1->mv[1].hor - mb2->mv[11].hor) >= 4) || + (ABS(mb1->mv[1].ver - mb2->mv[11].ver) >= 4) || + (mb1->refAddr[0] != mb2->refAddr[11 >> 2])) + { + topBs += 1<<8; + } + tmp3 = mb1->totalCoeff[5]; + tmp4 = mb2->totalCoeff[15]; + if (tmp1 || tmp2) + { + topBs += 2<<16; + } + else if ((ABS(mb1->mv[4].hor - mb2->mv[14].hor) >= 4) || + (ABS(mb1->mv[4].ver - mb2->mv[14].ver) >= 4) || + (mb1->refAddr[4 >> 2] != mb2->refAddr[14 >> 2])) + { + topBs += 1<<16; + } + if (tmp3 || tmp4) + { + topBs += 2<<24; + } + else if ((ABS(mb1->mv[5].hor - mb2->mv[15].hor) >= 4) || + (ABS(mb1->mv[5].ver - mb2->mv[15].ver) >= 4) || + (mb1->refAddr[5 >> 2] != mb2->refAddr[15 >> 2])) + { + topBs += 1<<24; + } + + return topBs; +} + +/*------------------------------------------------------------------------------ + + Function: EdgeBoundaryStrengthLeft + + Functional description: + Function to calculate boundary strength value bs for left- + edge of a macroblock. Macroblock types are checked + before this is called -> no intra mb conditions here. + +------------------------------------------------------------------------------*/ +u32 EdgeBoundaryStrengthLeft(mbStorage_t *mb1, mbStorage_t *mb2) +{ + u32 leftBs = 0; + u32 tmp1, tmp2, tmp3, tmp4; + + tmp1 = mb1->totalCoeff[0]; + tmp2 = mb2->totalCoeff[5]; + tmp3 = mb1->totalCoeff[2]; + tmp4 = mb2->totalCoeff[7]; + + if (tmp1 || tmp2) + { + leftBs = 2<<0; + } + else if ((ABS(mb1->mv[0].hor - mb2->mv[5].hor) >= 4) || + (ABS(mb1->mv[0].ver - mb2->mv[5].ver) >= 4) || + (mb1->refAddr[0] != mb2->refAddr[5 >> 2])) + { + leftBs = 1<<0; + } + tmp1 = mb1->totalCoeff[8]; + tmp2 = mb2->totalCoeff[13]; + if (tmp3 || tmp4) + { + leftBs += 2<<8; + } + else if ((ABS(mb1->mv[2].hor - mb2->mv[7].hor) >= 4) || + (ABS(mb1->mv[2].ver - mb2->mv[7].ver) >= 4) || + (mb1->refAddr[0] != mb2->refAddr[7 >> 2])) + { + leftBs += 1<<8; + } + tmp3 = mb1->totalCoeff[10]; + tmp4 = mb2->totalCoeff[15]; + if (tmp1 || tmp2) + { + leftBs += 2<<16; + } + else if ((ABS(mb1->mv[8].hor - mb2->mv[13].hor) >= 4) || + (ABS(mb1->mv[8].ver - mb2->mv[13].ver) >= 4) || + (mb1->refAddr[8 >> 2] != mb2->refAddr[13 >> 2])) + { + leftBs += 1<<16; + } + if (tmp3 || tmp4) + { + leftBs += 2<<24; + } + else if ((ABS(mb1->mv[10].hor - mb2->mv[15].hor) >= 4) || + (ABS(mb1->mv[10].ver - mb2->mv[15].ver) >= 4) || + (mb1->refAddr[10 >> 2] != mb2->refAddr[15 >> 2])) + { + leftBs += 1<<24; + } + + return leftBs; +} + + +/*------------------------------------------------------------------------------ + + Function: h264bsdFilterPicture + + Functional description: + Perform deblocking filtering for a picture. Filter does not copy + the original picture anywhere but filtering is performed directly + on the original image. Parameters controlling the filtering process + are computed based on information in macroblock structures of the + filtered macroblock, macroblock above and macroblock on the left of + the filtered one. + + Inputs: + image pointer to image to be filtered + mb pointer to macroblock data structure of the top-left + macroblock of the picture + + Outputs: + image filtered image stored here + + Returns: + none + +------------------------------------------------------------------------------*/ + +/*lint --e{550} Symbol not accessed */ +void h264bsdFilterPicture( + image_t *image, + mbStorage_t *mb) +{ + +/* Variables */ + + u32 flags; + u32 picSizeInMbs, mbRow, mbCol; + u32 picWidthInMbs; + u8 *data; + mbStorage_t *pMb; + u8 bS[2][16]; + u8 thresholdLuma[2][16]; + u8 thresholdChroma[2][8]; + u8 alpha[2][2]; + u8 beta[2][2]; + OMXResult res; + +/* Code */ + + ASSERT(image); + ASSERT(mb); + ASSERT(image->data); + ASSERT(image->width); + ASSERT(image->height); + + picWidthInMbs = image->width; + data = image->data; + picSizeInMbs = picWidthInMbs * image->height; + + pMb = mb; + + for (mbRow = 0, mbCol = 0; mbRow < image->height; pMb++) + { + flags = GetMbFilteringFlags(pMb); + + if (flags) + { + /* GetBoundaryStrengths function returns non-zero value if any of + * the bS values for the macroblock being processed was non-zero */ + if (GetBoundaryStrengths(pMb, bS, flags)) + { + + /* Luma */ + GetLumaEdgeThresholds(pMb,alpha,beta,thresholdLuma,bS,flags); + data = image->data + mbRow * picWidthInMbs * 256 + mbCol * 16; + + res = omxVCM4P10_FilterDeblockingLuma_VerEdge_I( data, + (OMX_S32)(picWidthInMbs*16), + (const OMX_U8*)alpha, + (const OMX_U8*)beta, + (const OMX_U8*)thresholdLuma, + (const OMX_U8*)bS ); + + res = omxVCM4P10_FilterDeblockingLuma_HorEdge_I( data, + (OMX_S32)(picWidthInMbs*16), + (const OMX_U8*)alpha+2, + (const OMX_U8*)beta+2, + (const OMX_U8*)thresholdLuma+16, + (const OMX_U8*)bS+16 ); + /* Cb */ + GetChromaEdgeThresholds(pMb, alpha, beta, thresholdChroma, + bS, flags, pMb->chromaQpIndexOffset); + data = image->data + picSizeInMbs * 256 + + mbRow * picWidthInMbs * 64 + mbCol * 8; + + res = omxVCM4P10_FilterDeblockingChroma_VerEdge_I( data, + (OMX_S32)(picWidthInMbs*8), + (const OMX_U8*)alpha, + (const OMX_U8*)beta, + (const OMX_U8*)thresholdChroma, + (const OMX_U8*)bS ); + res = omxVCM4P10_FilterDeblockingChroma_HorEdge_I( data, + (OMX_S32)(picWidthInMbs*8), + (const OMX_U8*)alpha+2, + (const OMX_U8*)beta+2, + (const OMX_U8*)thresholdChroma+8, + (const OMX_U8*)bS+16 ); + /* Cr */ + data += (picSizeInMbs * 64); + res = omxVCM4P10_FilterDeblockingChroma_VerEdge_I( data, + (OMX_S32)(picWidthInMbs*8), + (const OMX_U8*)alpha, + (const OMX_U8*)beta, + (const OMX_U8*)thresholdChroma, + (const OMX_U8*)bS ); + res = omxVCM4P10_FilterDeblockingChroma_HorEdge_I( data, + (OMX_S32)(picWidthInMbs*8), + (const OMX_U8*)alpha+2, + (const OMX_U8*)beta+2, + (const OMX_U8*)thresholdChroma+8, + (const OMX_U8*)bS+16 ); + } + } + + mbCol++; + if (mbCol == picWidthInMbs) + { + mbCol = 0; + mbRow++; + } + } + + // RASKY: FIXME + (void)res; +} + +/*------------------------------------------------------------------------------ + + Function: GetBoundaryStrengths + + Functional description: + Function to calculate boundary strengths for all edges of a + macroblock. Function returns HANTRO_TRUE if any of the bS values for + the macroblock had non-zero value, HANTRO_FALSE otherwise. + +------------------------------------------------------------------------------*/ +u32 GetBoundaryStrengths(mbStorage_t *mb, u8 (*bS)[16], u32 flags) +{ + +/* Variables */ + + /* this flag is set HANTRO_TRUE as soon as any boundary strength value is + * non-zero */ + u32 nonZeroBs = HANTRO_FALSE; + u32 *pTmp; + u32 tmp1, tmp2, isIntraMb; + +/* Code */ + + ASSERT(mb); + ASSERT(bS); + ASSERT(flags); + + isIntraMb = IS_INTRA_MB(*mb); + + /* top edges */ + pTmp = (u32*)&bS[1][0]; + if (flags & FILTER_TOP_EDGE) + { + if (isIntraMb || IS_INTRA_MB(*mb->mbB)) + { + *pTmp = 0x04040404; + nonZeroBs = HANTRO_TRUE; + } + else + { + *pTmp = EdgeBoundaryStrengthTop(mb, mb->mbB); + if (*pTmp) + nonZeroBs = HANTRO_TRUE; + } + } + else + { + *pTmp = 0; + } + + /* left edges */ + pTmp = (u32*)&bS[0][0]; + if (flags & FILTER_LEFT_EDGE) + { + if (isIntraMb || IS_INTRA_MB(*mb->mbA)) + { + /*bS[0][0] = bS[0][1] = bS[0][2] = bS[0][3] = 4;*/ + *pTmp = 0x04040404; + nonZeroBs = HANTRO_TRUE; + } + else + { + *pTmp = EdgeBoundaryStrengthLeft(mb, mb->mbA); + if (!nonZeroBs && *pTmp) + nonZeroBs = HANTRO_TRUE; + } + } + else + { + *pTmp = 0; + } + + /* inner edges */ + if (isIntraMb) + { + pTmp++; + *pTmp++ = 0x03030303; + *pTmp++ = 0x03030303; + *pTmp++ = 0x03030303; + pTmp++; + *pTmp++ = 0x03030303; + *pTmp++ = 0x03030303; + *pTmp = 0x03030303; + + nonZeroBs = HANTRO_TRUE; + } + else + { + pTmp = (u32*)mb->totalCoeff; + + /* 16x16 inter mb -> ref addresses or motion vectors cannot differ, + * only check if either of the blocks contain coefficients */ + if (h264bsdNumMbPart(mb->mbType) == 1) + { + tmp1 = *pTmp++; + tmp2 = *pTmp++; + bS[1][4] = (tmp1 & 0x00FF00FF) ? 2 : 0; /* [2] || [0] */ + bS[1][5] = (tmp1 & 0xFF00FF00) ? 2 : 0; /* [3] || [1] */ + bS[0][4] = (tmp1 & 0x0000FFFF) ? 2 : 0; /* [1] || [0] */ + bS[0][5] = (tmp1 & 0xFFFF0000) ? 2 : 0; /* [3] || [2] */ + + tmp1 = *pTmp++; + bS[1][6] = (tmp2 & 0x00FF00FF) ? 2 : 0; /* [6] || [4] */ + bS[1][7] = (tmp2 & 0xFF00FF00) ? 2 : 0; /* [7] || [5] */ + bS[0][12] = (tmp2 & 0x0000FFFF) ? 2 : 0; /* [5] || [4] */ + bS[0][13] = (tmp2 & 0xFFFF0000) ? 2 : 0; /* [7] || [6] */ + tmp2 = *pTmp; + bS[1][12] = (tmp1 & 0x00FF00FF) ? 2 : 0; /* [10] || [8] */ + bS[1][13] = (tmp1 & 0xFF00FF00) ? 2 : 0; /* [11] || [9] */ + bS[0][6] = (tmp1 & 0x0000FFFF) ? 2 : 0; /* [9] || [8] */ + bS[0][7] = (tmp1 & 0xFFFF0000) ? 2 : 0; /* [11] || [10] */ + + bS[1][14] = (tmp2 & 0x00FF00FF) ? 2 : 0; /* [14] || [12] */ + bS[1][15] = (tmp2 & 0xFF00FF00) ? 2 : 0; /* [15] || [13] */ + bS[0][14] = (tmp2 & 0x0000FFFF) ? 2 : 0; /* [13] || [12] */ + bS[0][15] = (tmp2 & 0xFFFF0000) ? 2 : 0; /* [15] || [14] */ + + { + u32 tmp3, tmp4; + + tmp1 = mb->totalCoeff[8]; + tmp2 = mb->totalCoeff[2]; + tmp3 = mb->totalCoeff[9]; + tmp4 = mb->totalCoeff[3]; + + bS[1][8] = tmp1 || tmp2 ? 2 : 0; + tmp1 = mb->totalCoeff[12]; + tmp2 = mb->totalCoeff[6]; + bS[1][9] = tmp3 || tmp4 ? 2 : 0; + tmp3 = mb->totalCoeff[13]; + tmp4 = mb->totalCoeff[7]; + bS[1][10] = tmp1 || tmp2 ? 2 : 0; + tmp1 = mb->totalCoeff[4]; + tmp2 = mb->totalCoeff[1]; + bS[1][11] = tmp3 || tmp4 ? 2 : 0; + tmp3 = mb->totalCoeff[6]; + tmp4 = mb->totalCoeff[3]; + bS[0][8] = tmp1 || tmp2 ? 2 : 0; + tmp1 = mb->totalCoeff[12]; + tmp2 = mb->totalCoeff[9]; + bS[0][9] = tmp3 || tmp4 ? 2 : 0; + tmp3 = mb->totalCoeff[14]; + tmp4 = mb->totalCoeff[11]; + bS[0][10] = tmp1 || tmp2 ? 2 : 0; + bS[0][11] = tmp3 || tmp4 ? 2 : 0; + } + } + + /* 16x8 inter mb -> ref addresses and motion vectors can be different + * only for the middle horizontal edge, for the other top edges it is + * enough to check whether the blocks contain coefficients or not. The + * same applies to all internal left edges. */ + else if (mb->mbType == P_L0_L0_16x8) + { + tmp1 = *pTmp++; + tmp2 = *pTmp++; + bS[1][4] = (tmp1 & 0x00FF00FF) ? 2 : 0; /* [2] || [0] */ + bS[1][5] = (tmp1 & 0xFF00FF00) ? 2 : 0; /* [3] || [1] */ + bS[0][4] = (tmp1 & 0x0000FFFF) ? 2 : 0; /* [1] || [0] */ + bS[0][5] = (tmp1 & 0xFFFF0000) ? 2 : 0; /* [3] || [2] */ + tmp1 = *pTmp++; + bS[1][6] = (tmp2 & 0x00FF00FF) ? 2 : 0; /* [6] || [4] */ + bS[1][7] = (tmp2 & 0xFF00FF00) ? 2 : 0; /* [7] || [5] */ + bS[0][12] = (tmp2 & 0x0000FFFF) ? 2 : 0; /* [5] || [4] */ + bS[0][13] = (tmp2 & 0xFFFF0000) ? 2 : 0; /* [7] || [6] */ + tmp2 = *pTmp; + bS[1][12] = (tmp1 & 0x00FF00FF) ? 2 : 0; /* [10] || [8] */ + bS[1][13] = (tmp1 & 0xFF00FF00) ? 2 : 0; /* [11] || [9] */ + bS[0][6] = (tmp1 & 0x0000FFFF) ? 2 : 0; /* [9] || [8] */ + bS[0][7] = (tmp1 & 0xFFFF0000) ? 2 : 0; /* [11] || [10] */ + + bS[1][14] = (tmp2 & 0x00FF00FF) ? 2 : 0; /* [14] || [12] */ + bS[1][15] = (tmp2 & 0xFF00FF00) ? 2 : 0; /* [15] || [13] */ + bS[0][14] = (tmp2 & 0x0000FFFF) ? 2 : 0; /* [13] || [12] */ + bS[0][15] = (tmp2 & 0xFFFF0000) ? 2 : 0; /* [15] || [14] */ + + bS[1][8] = (u8)InnerBoundaryStrength(mb, 8, 2); + bS[1][9] = (u8)InnerBoundaryStrength(mb, 9, 3); + bS[1][10] = (u8)InnerBoundaryStrength(mb, 12, 6); + bS[1][11] = (u8)InnerBoundaryStrength(mb, 13, 7); + + { + u32 tmp3, tmp4; + + tmp1 = mb->totalCoeff[4]; + tmp2 = mb->totalCoeff[1]; + tmp3 = mb->totalCoeff[6]; + tmp4 = mb->totalCoeff[3]; + bS[0][8] = tmp1 || tmp2 ? 2 : 0; + tmp1 = mb->totalCoeff[12]; + tmp2 = mb->totalCoeff[9]; + bS[0][9] = tmp3 || tmp4 ? 2 : 0; + tmp3 = mb->totalCoeff[14]; + tmp4 = mb->totalCoeff[11]; + bS[0][10] = tmp1 || tmp2 ? 2 : 0; + bS[0][11] = tmp3 || tmp4 ? 2 : 0; + } + } + /* 8x16 inter mb -> ref addresses and motion vectors can be different + * only for the middle vertical edge, for the other left edges it is + * enough to check whether the blocks contain coefficients or not. The + * same applies to all internal top edges. */ + else if (mb->mbType == P_L0_L0_8x16) + { + tmp1 = *pTmp++; + tmp2 = *pTmp++; + bS[1][4] = (tmp1 & 0x00FF00FF) ? 2 : 0; /* [2] || [0] */ + bS[1][5] = (tmp1 & 0xFF00FF00) ? 2 : 0; /* [3] || [1] */ + bS[0][4] = (tmp1 & 0x0000FFFF) ? 2 : 0; /* [1] || [0] */ + bS[0][5] = (tmp1 & 0xFFFF0000) ? 2 : 0; /* [3] || [2] */ + tmp1 = *pTmp++; + bS[1][6] = (tmp2 & 0x00FF00FF) ? 2 : 0; /* [6] || [4] */ + bS[1][7] = (tmp2 & 0xFF00FF00) ? 2 : 0; /* [7] || [5] */ + bS[0][12] = (tmp2 & 0x0000FFFF) ? 2 : 0; /* [5] || [4] */ + bS[0][13] = (tmp2 & 0xFFFF0000) ? 2 : 0; /* [7] || [6] */ + tmp2 = *pTmp; + bS[1][12] = (tmp1 & 0x00FF00FF) ? 2 : 0; /* [10] || [8] */ + bS[1][13] = (tmp1 & 0xFF00FF00) ? 2 : 0; /* [11] || [9] */ + bS[0][6] = (tmp1 & 0x0000FFFF) ? 2 : 0; /* [9] || [8] */ + bS[0][7] = (tmp1 & 0xFFFF0000) ? 2 : 0; /* [11] || [10] */ + + bS[1][14] = (tmp2 & 0x00FF00FF) ? 2 : 0; /* [14] || [12] */ + bS[1][15] = (tmp2 & 0xFF00FF00) ? 2 : 0; /* [15] || [13] */ + bS[0][14] = (tmp2 & 0x0000FFFF) ? 2 : 0; /* [13] || [12] */ + bS[0][15] = (tmp2 & 0xFFFF0000) ? 2 : 0; /* [15] || [14] */ + + bS[0][8] = (u8)InnerBoundaryStrength(mb, 4, 1); + bS[0][9] = (u8)InnerBoundaryStrength(mb, 6, 3); + bS[0][10] = (u8)InnerBoundaryStrength(mb, 12, 9); + bS[0][11] = (u8)InnerBoundaryStrength(mb, 14, 11); + + { + u32 tmp3, tmp4; + + tmp1 = mb->totalCoeff[8]; + tmp2 = mb->totalCoeff[2]; + tmp3 = mb->totalCoeff[9]; + tmp4 = mb->totalCoeff[3]; + bS[1][8] = tmp1 || tmp2 ? 2 : 0; + tmp1 = mb->totalCoeff[12]; + tmp2 = mb->totalCoeff[6]; + bS[1][9] = tmp3 || tmp4 ? 2 : 0; + tmp3 = mb->totalCoeff[13]; + tmp4 = mb->totalCoeff[7]; + bS[1][10] = tmp1 || tmp2 ? 2 : 0; + bS[1][11] = tmp3 || tmp4 ? 2 : 0; + } + } + else + { + tmp1 = *pTmp++; + bS[1][4] = (tmp1 & 0x00FF00FF) ? 2 : (u8)InnerBoundaryStrength2(mb, 2, 0); + bS[1][5] = (tmp1 & 0xFF00FF00) ? 2 : (u8)InnerBoundaryStrength2(mb, 3, 1); + bS[0][4] = (tmp1 & 0x0000FFFF) ? 2 : (u8)InnerBoundaryStrength2(mb, 1, 0); + bS[0][5] = (tmp1 & 0xFFFF0000) ? 2 : (u8)InnerBoundaryStrength2(mb, 3, 2); + tmp1 = *pTmp++; + bS[1][6] = (tmp1 & 0x00FF00FF) ? 2 : (u8)InnerBoundaryStrength2(mb, 6, 4); + bS[1][7] = (tmp1 & 0xFF00FF00) ? 2 : (u8)InnerBoundaryStrength2(mb, 7, 5); + bS[0][12] = (tmp1 & 0x0000FFFF) ? 2 : (u8)InnerBoundaryStrength2(mb, 5, 4); + bS[0][13] = (tmp1 & 0xFFFF0000) ? 2 : (u8)InnerBoundaryStrength2(mb, 7, 6); + tmp1 = *pTmp++; + bS[1][12] = (tmp1 & 0x00FF00FF) ? 2 : (u8)InnerBoundaryStrength2(mb, 10, 8); + bS[1][13] = (tmp1 & 0xFF00FF00) ? 2 : (u8)InnerBoundaryStrength2(mb, 11, 9); + bS[0][6] = (tmp1 & 0x0000FFFF) ? 2 : (u8)InnerBoundaryStrength2(mb, 9, 8); + bS[0][7] = (tmp1 & 0xFFFF0000) ? 2 : (u8)InnerBoundaryStrength2(mb, 11, 10); + tmp1 = *pTmp; + bS[1][14] = (tmp1 & 0x00FF00FF) ? 2 : (u8)InnerBoundaryStrength2(mb, 14, 12); + bS[1][15] = (tmp1 & 0xFF00FF00) ? 2 : (u8)InnerBoundaryStrength2(mb, 15, 13); + bS[0][14] = (tmp1 & 0x0000FFFF) ? 2 : (u8)InnerBoundaryStrength2(mb, 13, 12); + bS[0][15] = (tmp1 & 0xFFFF0000) ? 2 : (u8)InnerBoundaryStrength2(mb, 15, 14); + + bS[1][8] = (u8)InnerBoundaryStrength(mb, 8, 2); + bS[1][9] = (u8)InnerBoundaryStrength(mb, 9, 3); + bS[1][10] = (u8)InnerBoundaryStrength(mb, 12, 6); + bS[1][11] = (u8)InnerBoundaryStrength(mb, 13, 7); + + bS[0][8] = (u8)InnerBoundaryStrength(mb, 4, 1); + bS[0][9] = (u8)InnerBoundaryStrength(mb, 6, 3); + bS[0][10] = (u8)InnerBoundaryStrength(mb, 12, 9); + bS[0][11] = (u8)InnerBoundaryStrength(mb, 14, 11); + } + pTmp = (u32*)&bS[0][0]; + if (!nonZeroBs && (pTmp[1] || pTmp[2] || pTmp[3] || + pTmp[5] || pTmp[6] || pTmp[7]) ) + { + nonZeroBs = HANTRO_TRUE; + } + } + + return(nonZeroBs); + +} + +/*------------------------------------------------------------------------------ + + Function: GetLumaEdgeThresholds + + Functional description: + Compute alpha, beta and tc0 thresholds for inner, left and top + luma edges of a macroblock. + +------------------------------------------------------------------------------*/ +void GetLumaEdgeThresholds( + mbStorage_t *mb, + u8 (*alpha)[2], + u8 (*beta)[2], + u8 (*threshold)[16], + u8 (*bs)[16], + u32 filteringFlags ) +{ + +/* Variables */ + + u32 indexA, indexB; + u32 qpAv, qp, qpTmp; + u32 i; + +/* Code */ + + ASSERT(threshold); + ASSERT(bs); + ASSERT(beta); + ASSERT(alpha); + ASSERT(mb); + + qp = mb->qpY; + + indexA = (u32)CLIP3(0, 51, (i32)qp + mb->filterOffsetA); + indexB = (u32)CLIP3(0, 51, (i32)qp + mb->filterOffsetB); + + /* Internal edge values */ + alpha[0][1] = alphas[indexA]; + alpha[1][1] = alphas[indexA]; + alpha[1][0] = alphas[indexA]; + alpha[0][0] = alphas[indexA]; + beta[0][1] = betas[indexB]; + beta[1][1] = betas[indexB]; + beta[1][0] = betas[indexB]; + beta[0][0] = betas[indexB]; + + /* vertical scan order */ + for (i = 0; i < 2; i++) + { + u32 t1, t2; + + t1 = bs[i][0]; + t2 = bs[i][1]; + threshold[i][0] = (t1) ? tc0[indexA][t1] : 0; + t1 = bs[i][2]; + threshold[i][1] = (t2) ? tc0[indexA][t2] : 0; + t2 = bs[i][3]; + threshold[i][2] = (t1) ? tc0[indexA][t1] : 0; + t1 = bs[i][4]; + threshold[i][3] = (t2) ? tc0[indexA][t2] : 0; + t2 = bs[i][5]; + threshold[i][4] = (t1) ? tc0[indexA][t1] : 0; + t1 = bs[i][6]; + threshold[i][5] = (t2) ? tc0[indexA][t2] : 0; + t2 = bs[i][7]; + threshold[i][6] = (t1) ? tc0[indexA][t1] : 0; + t1 = bs[i][8]; + threshold[i][7] = (t2) ? tc0[indexA][t2] : 0; + t2 = bs[i][9]; + threshold[i][8] = (t1) ? tc0[indexA][t1] : 0; + t1 = bs[i][10]; + threshold[i][9] = (t2) ? tc0[indexA][t2] : 0; + t2 = bs[i][11]; + threshold[i][10] = (t1) ? tc0[indexA][t1] : 0; + t1 = bs[i][12]; + threshold[i][11] = (t2) ? tc0[indexA][t2] : 0; + t2 = bs[i][13]; + threshold[i][12] = (t1) ? tc0[indexA][t1] : 0; + t1 = bs[i][14]; + threshold[i][13] = (t2) ? tc0[indexA][t2] : 0; + t2 = bs[i][15]; + threshold[i][14] = (t1) ? tc0[indexA][t1] : 0; + threshold[i][15] = (t2) ? tc0[indexA][t2] : 0; + } + + if (filteringFlags & FILTER_TOP_EDGE) + { + qpTmp = mb->mbB->qpY; + if (qpTmp != qp) + { + u32 t1, t2, t3, t4; + qpAv = (qp + qpTmp + 1) >> 1; + + indexA = (u32)CLIP3(0, 51, (i32)qpAv + mb->filterOffsetA); + indexB = (u32)CLIP3(0, 51, (i32)qpAv + mb->filterOffsetB); + + alpha[1][0] = alphas[indexA]; + beta[1][0] = betas[indexB]; + t1 = bs[1][0]; + t2 = bs[1][1]; + t3 = bs[1][2]; + t4 = bs[1][3]; + threshold[1][0] = (t1 && (t1 < 4)) ? tc0[indexA][t1] : 0; + threshold[1][1] = (t2 && (t2 < 4)) ? tc0[indexA][t2] : 0; + threshold[1][2] = (t3 && (t3 < 4)) ? tc0[indexA][t3] : 0; + threshold[1][3] = (t4 && (t4 < 4)) ? tc0[indexA][t4] : 0; + } + } + if (filteringFlags & FILTER_LEFT_EDGE) + { + qpTmp = mb->mbA->qpY; + if (qpTmp != qp) + { + qpAv = (qp + qpTmp + 1) >> 1; + + indexA = (u32)CLIP3(0, 51, (i32)qpAv + mb->filterOffsetA); + indexB = (u32)CLIP3(0, 51, (i32)qpAv + mb->filterOffsetB); + + alpha[0][0] = alphas[indexA]; + beta[0][0] = betas[indexB]; + threshold[0][0] = (bs[0][0] && (bs[0][0] < 4)) ? tc0[indexA][bs[0][0]] : 0; + threshold[0][1] = (bs[0][1] && (bs[0][1] < 4)) ? tc0[indexA][bs[0][1]] : 0; + threshold[0][2] = (bs[0][2] && (bs[0][2] < 4)) ? tc0[indexA][bs[0][2]] : 0; + threshold[0][3] = (bs[0][3] && (bs[0][3] < 4)) ? tc0[indexA][bs[0][3]] : 0; + } + } + +} + +/*------------------------------------------------------------------------------ + + Function: GetChromaEdgeThresholds + + Functional description: + Compute alpha, beta and tc0 thresholds for inner, left and top + chroma edges of a macroblock. + +------------------------------------------------------------------------------*/ +void GetChromaEdgeThresholds( + mbStorage_t *mb, + u8 (*alpha)[2], + u8 (*beta)[2], + u8 (*threshold)[8], + u8 (*bs)[16], + u32 filteringFlags, + i32 chromaQpIndexOffset) +{ + +/* Variables */ + + u32 indexA, indexB; + u32 qpAv, qp, qpTmp; + u32 i; + +/* Code */ + + ASSERT(threshold); + ASSERT(bs); + ASSERT(beta); + ASSERT(alpha); + ASSERT(mb); + ASSERT(mb); + + qp = mb->qpY; + qp = h264bsdQpC[CLIP3(0, 51, (i32)qp + chromaQpIndexOffset)]; + + indexA = (u32)CLIP3(0, 51, (i32)qp + mb->filterOffsetA); + indexB = (u32)CLIP3(0, 51, (i32)qp + mb->filterOffsetB); + + alpha[0][1] = alphas[indexA]; + alpha[1][1] = alphas[indexA]; + alpha[1][0] = alphas[indexA]; + alpha[0][0] = alphas[indexA]; + beta[0][1] = betas[indexB]; + beta[1][1] = betas[indexB]; + beta[1][0] = betas[indexB]; + beta[0][0] = betas[indexB]; + + for (i = 0; i < 2; i++) + { + u32 t1, t2; + + t1 = bs[i][0]; + t2 = bs[i][1]; + threshold[i][0] = (t1) ? tc0[indexA][t1] : 0; + t1 = bs[i][2]; + threshold[i][1] = (t2) ? tc0[indexA][t2] : 0; + t2 = bs[i][3]; + threshold[i][2] = (t1) ? tc0[indexA][t1] : 0; + t1 = bs[i][8]; + threshold[i][3] = (t2) ? tc0[indexA][t2] : 0; + t2 = bs[i][9]; + threshold[i][4] = (t1) ? tc0[indexA][t1] : 0; + t1 = bs[i][10]; + threshold[i][5] = (t2) ? tc0[indexA][t2] : 0; + t2 = bs[i][11]; + threshold[i][6] = (t1) ? tc0[indexA][t1] : 0; + threshold[i][7] = (t2) ? tc0[indexA][t2] : 0; + } + + if (filteringFlags & FILTER_TOP_EDGE) + { + qpTmp = mb->mbB->qpY; + if (qpTmp != mb->qpY) + { + u32 t1, t2, t3, t4; + qpTmp = h264bsdQpC[CLIP3(0, 51, (i32)qpTmp + chromaQpIndexOffset)]; + qpAv = (qp + qpTmp + 1) >> 1; + + indexA = (u32)CLIP3(0, 51, (i32)qpAv + mb->filterOffsetA); + indexB = (u32)CLIP3(0, 51, (i32)qpAv + mb->filterOffsetB); + + alpha[1][0] = alphas[indexA]; + beta[1][0] = betas[indexB]; + + t1 = bs[1][0]; + t2 = bs[1][1]; + t3 = bs[1][2]; + t4 = bs[1][3]; + threshold[1][0] = (t1) ? tc0[indexA][t1] : 0; + threshold[1][1] = (t2) ? tc0[indexA][t2] : 0; + threshold[1][2] = (t3) ? tc0[indexA][t3] : 0; + threshold[1][3] = (t4) ? tc0[indexA][t4] : 0; + } + } + if (filteringFlags & FILTER_LEFT_EDGE) + { + qpTmp = mb->mbA->qpY; + if (qpTmp != mb->qpY) + { + + qpTmp = h264bsdQpC[CLIP3(0, 51, (i32)qpTmp + chromaQpIndexOffset)]; + qpAv = (qp + qpTmp + 1) >> 1; + + indexA = (u32)CLIP3(0, 51, (i32)qpAv + mb->filterOffsetA); + indexB = (u32)CLIP3(0, 51, (i32)qpAv + mb->filterOffsetB); + + alpha[0][0] = alphas[indexA]; + beta[0][0] = betas[indexB]; + threshold[0][0] = (bs[0][0]) ? tc0[indexA][bs[0][0]] : 0; + threshold[0][1] = (bs[0][1]) ? tc0[indexA][bs[0][1]] : 0; + threshold[0][2] = (bs[0][2]) ? tc0[indexA][bs[0][2]] : 0; + threshold[0][3] = (bs[0][3]) ? tc0[indexA][bs[0][3]] : 0; + } + } + +} + + +/*lint +e701 +e702 */ + diff --git a/src/video/h264_decoder/h264bsd_deblocking.h b/src/video/h264_decoder/h264bsd_deblocking.h new file mode 100644 index 000000000..2571ddaf9 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_deblocking.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_DEBLOCKING_H +#define H264SWDEC_DEBLOCKING_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_image.h" +#include "h264bsd_macroblock_layer.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +void h264bsdFilterPicture( + image_t *image, + mbStorage_t *mb); + +#endif /* #ifdef H264SWDEC_DEBLOCKING_H */ + diff --git a/src/video/h264_decoder/h264bsd_decoder.c b/src/video/h264_decoder/h264bsd_decoder.c new file mode 100644 index 000000000..39af7a626 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_decoder.c @@ -0,0 +1,1221 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdInit + h264bsdDecode + h264bsdShutdown + h264bsdCurrentImage + h264bsdNextOutputPicture + h264bsdPicWidth + h264bsdPicHeight + h264bsdFlushBuffer + h264bsdCheckValidParamSets + h264bsdVideoRange + h264bsdMatrixCoefficients + h264bsdCroppingParams + +------------------------------------------------------------------------------*/ + +#include "opttarget.h" + + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_decoder.h" +#include "h264bsd_nal_unit.h" +#include "h264bsd_byte_stream.h" +#include "h264bsd_seq_param_set.h" +#include "h264bsd_pic_param_set.h" +#include "h264bsd_slice_header.h" +#include "h264bsd_slice_data.h" +#include "h264bsd_neighbour.h" +#include "h264bsd_util.h" +#include "h264bsd_dpb.h" +#include "h264bsd_deblocking.h" +#include "h264bsd_conceal.h" + +#ifdef EMIT_IMAGE_ASAP +u32 sliceData[18] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; +extern void extern_emit_image(u8 *buffer, u32 width, u32 height, u32* sliceData); +#endif + +#include "../profile.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +static void h264bsdDecodeSeiMessages(storage_t *pStorage, strmData_t *strm); +static const u8* h264bsdStreamCurrentBytePtr(strmData_t *strm); +static u32 h264bsdStreamBytesRemaining(strmData_t *strm); +static u32 h264bsdIsRbspTrailingBits(const u8 *data, u32 len); + +/*------------------------------------------------------------------------------ + + Function name: h264bsdInit + + Functional description: + Initialize the decoder. + + Inputs: + noOutputReordering flag to indicate the decoder that it does not + have to perform reordering of display images. + + Outputs: + pStorage pointer to initialized storage structure + + Returns: + none + +------------------------------------------------------------------------------*/ + +u32 h264bsdInit(storage_t *pStorage, u32 noOutputReordering) +{ + +/* Variables */ +#if 0 + u32 size; +#endif +/* Code */ + + ASSERT(pStorage); + + h264bsdInitStorage(pStorage); +#if 0 + /* allocate mbLayer to be next multiple of 64 to enable use of + * specific NEON optimized "memset" for clearing the structure */ + size = (sizeof(macroblockLayer_t) + 63) & ~0x3F; + + pStorage->mbLayers = (macroblockLayer_t*)H264SwDecMalloc(size*NUM_PARALLEL_MACROBLOCKS); + if (!pStorage->mbLayers) + return HANTRO_NOK; +#endif + if (noOutputReordering) + pStorage->noReordering = HANTRO_TRUE; + + return HANTRO_OK; +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdSetNumBufferedPics + + Functional description: + Configure how many fully decoded pictures can be cached in the DPB + output buffer as a "cushion" for the client. + + This affects DPB memory sizing and therefore must be called before + the DPB is allocated (which happens during SPS/PPS activation). + + Inputs: + pStorage pointer to storage data structure + numPics number of buffered output pictures (0 disables buffering) + +------------------------------------------------------------------------------*/ + +void h264bsdSetNumBufferedPics(storage_t *pStorage, u32 numPics) +{ + ASSERT(pStorage); + /* Must be set before DPB allocation (DPB is allocated on SPS/PPS activation). */ + ASSERT(pStorage->dpb->buffer == NULL); + pStorage->maxNumBufferedPics = numPics; +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdSetSeiCallback + + Functional description: + Configure an optional callback for SEI payloads. The callback is + called once for each SEI payload in a NAL unit, after emulation + prevention bytes have been removed. + +------------------------------------------------------------------------------*/ + +void h264bsdSetSeiCallback(storage_t *pStorage, + h264bsdSeiCallback callback, void *ctx) +{ + ASSERT(pStorage); + pStorage->seiCallback = callback; + pStorage->seiCallbackCtx = ctx; +} + +static const u8* h264bsdStreamCurrentBytePtr(strmData_t *strm) +{ +#ifdef H264BSD_N64 + return (const u8*)(u32)(strm->pCurr >> 3); +#else + return strm->pStrmCurrPos; +#endif +} + +static u32 h264bsdStreamBytesRemaining(strmData_t *strm) +{ +#ifdef H264BSD_N64 + return (u32)((strm->pEnd - strm->pCurr) >> 3); +#else + return strm->strmBuffSize - (strm->strmBuffReadBits >> 3); +#endif +} + +static u32 h264bsdIsRbspTrailingBits(const u8 *data, u32 len) +{ + u32 i; + if (!len || data[0] != 0x80) + return HANTRO_FALSE; + for (i = 1; i < len; i++) + if (data[i] != 0) + return HANTRO_FALSE; + return HANTRO_TRUE; +} + +static void h264bsdDecodeSeiMessages(storage_t *pStorage, strmData_t *strm) +{ + const u8 *data; + u32 len, off; + + if (!pStorage->seiCallback) + return; + + data = h264bsdStreamCurrentBytePtr(strm); + len = h264bsdStreamBytesRemaining(strm); + off = 0; + + while (off < len) + { + u32 payloadType = 0; + u32 payloadSize = 0; + + if (h264bsdIsRbspTrailingBits(data + off, len - off)) + break; + + while (off < len && data[off] == 0xFF) + { + payloadType += 255; + off++; + } + if (off >= len) + break; + payloadType += data[off++]; + + while (off < len && data[off] == 0xFF) + { + payloadSize += 255; + off++; + } + if (off >= len) + break; + payloadSize += data[off++]; + + if (payloadSize > len - off) + break; + + pStorage->seiCallback(pStorage->seiCallbackCtx, + payloadType, data + off, payloadSize); + off += payloadSize; + } +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdDecode + + Functional description: + Decode a NAL unit. This function calls other modules to perform + tasks like + * extract and decode NAL unit from the byte stream + * decode parameter sets + * decode slice header and slice data + * conceal errors in the picture + * perform deblocking filtering + + This function contains top level control logic of the decoder. + + Inputs: + pStorage pointer to storage data structure + byteStrm pointer to stream buffer given by application + len length of the buffer in bytes + picId identifier for a picture, assigned by the + application + + Outputs: + readBytes number of bytes read from the stream is stored + here + + Returns: + H264BSD_RDY decoding finished, nothing special + H264BSD_PIC_RDY decoding of a picture finished + H264BSD_HDRS_RDY param sets activated, information like + picture dimensions etc can be read + H264BSD_ERROR error in decoding + H264BSD_PARAM_SET_ERROR serius error in decoding, failed to + activate param sets + +------------------------------------------------------------------------------*/ + + +u32 h264bsdDecodePartial(storage_t *pStorage, u8 *byteStrm, u32 len, u32 picId, + u32 *readBytes, u32 mbBudget) +{ + +/* Variables */ + + u32 tmp, ppsId, spsId; + i32 picOrderCnt; + nalUnit_t nalUnit; + seqParamSet_t seqParamSet; + picParamSet_t picParamSet; + strmData_t strm; + u32 accessUnitBoundaryFlag = HANTRO_FALSE; + u32 picReady = HANTRO_FALSE; + +/* Code */ + + /*ASSERT(pStorage); + ASSERT(byteStrm); + ASSERT(len); + ASSERT(readBytes);*/ + + pStorage->sliceMbBudget = mbBudget; + + /* Resume a timesliced slice: stream bit position and MB loop locals are + * already in storage; do not re-extract or re-parse the NAL. */ + if (pStorage->sliceDataPending) + { + ASSERT(byteStrm == pStorage->prevBufPointer); + strm = pStorage->strm[0]; + *readBytes = 0; + + tmp = h264bsdDecodeSliceData(&strm, pStorage, + pStorage->currImage, pStorage->sliceHeader); + if (tmp != HANTRO_OK) + { + EPRINT("SLICE_DATA"); + pStorage->sliceDataPending = 0; +#ifndef OPTIMIZE_NO_DECODED_FLAG + h264bsdMarkSliceCorrupted(pStorage, + pStorage->sliceHeader->firstMbInSlice); +#endif + return(H264BSD_ERROR); + } + + pStorage->strm[0] = strm; + if (pStorage->sliceDataPending) + { + /* Still mid-slice: keep the caller's buffer pointer stable. */ + pStorage->prevBufNotFinished = HANTRO_TRUE; + return(H264BSD_RDY); + } + + /* Slice finished: consume the NAL bytes now. */ + *readBytes = pStorage->prevBytesConsumed; + pStorage->prevBufNotFinished = HANTRO_FALSE; + +#ifndef SINGLE_SLICE + if (h264bsdIsEndOfPicture(pStorage)) + { + pStorage->skipRedundantSlices = HANTRO_TRUE; +#endif + picReady = HANTRO_TRUE; +#ifndef SINGLE_SLICE + } +#endif + goto picture_done; + } + + /* if previous buffer was not finished and same pointer given -> skip NAL + * unit extraction */ + if (pStorage->prevBufNotFinished && byteStrm == pStorage->prevBufPointer) + { + strm = pStorage->strm[0]; + #ifdef H264BSD_N64 + // rasky: FIXME: is this really required? + strm.pCurr = (u64)(u32)strm.pStart << 3; + #else + strm.pStrmCurrPos = strm.pStrmBuffStart; + strm.strmBuffReadBits = strm.bitPosInWord = 0; + #endif + *readBytes = pStorage->prevBytesConsumed; + } + else + { + PROFILE_START(PS_H264_NAL); + tmp = h264bsdExtractNalUnit(byteStrm, len, &strm, readBytes); + PROFILE_STOP(PS_H264_NAL); + if (tmp != HANTRO_OK) + { + EPRINT("BYTE_STREAM"); + return(H264BSD_ERROR); + } + /* store stream */ + pStorage->strm[0] = strm; + pStorage->prevBytesConsumed = *readBytes; + pStorage->prevBufPointer = byteStrm; + } + pStorage->prevBufNotFinished = HANTRO_FALSE; + + PROFILE_START(PS_H264_NAL); + tmp = h264bsdDecodeNalUnit(&strm, &nalUnit); + PROFILE_STOP(PS_H264_NAL); + if (tmp != HANTRO_OK) + { + EPRINT("NAL_UNIT"); + return(H264BSD_ERROR); + } + +#ifndef SINGLE_SLICE + /* Discard unspecified, reserved, SPS extension and auxiliary picture slices */ + if(nalUnit.nalUnitType == 0 || nalUnit.nalUnitType >= 13) + { + DEBUG(("DISCARDED NAL (UNSPECIFIED, REGISTERED, SPS ext or AUX slice)\n")); + return(H264BSD_RDY); + } + tmp = h264bsdCheckAccessUnitBoundary( + &strm, + &nalUnit, + pStorage, + &accessUnitBoundaryFlag); + if (tmp != HANTRO_OK) + { + EPRINT("ACCESS UNIT BOUNDARY CHECK"); + if (tmp == PARAM_SET_ERROR) + return(H264BSD_PARAM_SET_ERROR); + else + return(H264BSD_ERROR); + } + + if ( accessUnitBoundaryFlag ) + { + DEBUG(("Access unit boundary\n")); + /* conceal if picture started and param sets activated */ + if (pStorage->picStarted && pStorage->activeSps != NULL) + { + DEBUG(("CONCEALING...")); + + /* return error if second phase of + * initialization is not completed */ + if (pStorage->pendingActivation) + { + EPRINT("Pending activation not completed"); + return (H264BSD_ERROR); + } + + if (!pStorage->validSliceInAccessUnit) + { + pStorage->currImage->data = + h264bsdAllocateDpbImage(pStorage->dpb); + h264bsdInitRefPicList(pStorage->dpb); + tmp = h264bsdConceal(pStorage, pStorage->currImage, P_SLICE); + } + else + tmp = h264bsdConceal(pStorage, pStorage->currImage, + pStorage->sliceHeader->sliceType); + + picReady = HANTRO_TRUE; + + /* current NAL unit should be decoded on next activation -> set + * readBytes to 0 */ + *readBytes = 0; + pStorage->prevBufNotFinished = HANTRO_TRUE; + DEBUG(("...DONE\n")); + } + else + { + pStorage->validSliceInAccessUnit = HANTRO_FALSE; + } + pStorage->skipRedundantSlices = HANTRO_FALSE; + } +#endif + + if (!picReady) + { + switch (nalUnit.nalUnitType) + { + case NAL_SEQ_PARAM_SET: + DEBUG(("SEQ PARAM SET\n")); + tmp = h264bsdDecodeSeqParamSet(&strm, &seqParamSet); + if (tmp != HANTRO_OK) + { + EPRINT("SEQ_PARAM_SET"); + FREE(seqParamSet.offsetForRefFrame); + FREE(seqParamSet.vuiParameters); + return(H264BSD_ERROR); + } + tmp = h264bsdStoreSeqParamSet(pStorage, &seqParamSet); + break; + + case NAL_PIC_PARAM_SET: + DEBUG(("PIC PARAM SET\n")); + tmp = h264bsdDecodePicParamSet(&strm, &picParamSet); + if (tmp != HANTRO_OK) + { + EPRINT("PIC_PARAM_SET"); + FREE(picParamSet.runLength); + FREE(picParamSet.topLeft); + FREE(picParamSet.bottomRight); + FREE(picParamSet.sliceGroupId); + return(H264BSD_ERROR); + } + tmp = h264bsdStorePicParamSet(pStorage, &picParamSet); + break; + + case NAL_CODED_SLICE_IDR: + DEBUG(("IDR ")); + /* fall through */ + case NAL_CODED_SLICE: + DEBUG(("SLICE HEADER\n")); + + /* picture successfully finished and still decoding same old + * access unit -> no need to decode redundant slices */ + if (pStorage->skipRedundantSlices) + return(H264BSD_RDY); + + pStorage->picStarted = HANTRO_TRUE; + + if (h264bsdIsStartOfPicture(pStorage)) + { + pStorage->numConcealedMbs = 0; + pStorage->currentPicId = picId; + + tmp = h264bsdCheckPpsId(&strm, &ppsId); + ASSERT(tmp == HANTRO_OK); + /* store old activeSpsId and return headers ready + * indication if activeSps changes */ + spsId = pStorage->activeSpsId; + tmp = h264bsdActivateParamSets(pStorage, ppsId, + IS_IDR_NAL_UNIT(&nalUnit) ? + HANTRO_TRUE : HANTRO_FALSE); + if (tmp != HANTRO_OK) + { + EPRINT("Param set activation"); + pStorage->activePpsId = MAX_NUM_PIC_PARAM_SETS; + pStorage->activePps = NULL; + pStorage->activeSpsId = MAX_NUM_SEQ_PARAM_SETS; + pStorage->activeSps = NULL; + pStorage->pendingActivation = HANTRO_FALSE; + + if(tmp == MEMORY_ALLOCATION_ERROR) + { + return H264BSD_MEMALLOC_ERROR; + } + else + return(H264BSD_PARAM_SET_ERROR); + } + + if (spsId != pStorage->activeSpsId) + { + seqParamSet_t *oldSPS = NULL; + seqParamSet_t *newSPS = pStorage->activeSps; + u32 noOutputOfPriorPicsFlag = 1; + + if(pStorage->oldSpsId < MAX_NUM_SEQ_PARAM_SETS) + { + oldSPS = pStorage->sps[pStorage->oldSpsId]; + } + + *readBytes = 0; + pStorage->prevBufNotFinished = HANTRO_TRUE; + + + if(nalUnit.nalUnitType == NAL_CODED_SLICE_IDR) + { + tmp = + h264bsdCheckPriorPicsFlag(&noOutputOfPriorPicsFlag, + &strm, newSPS, + pStorage->activePps, + nalUnit.nalUnitType); + } + else + { + tmp = HANTRO_NOK; + } + + if((tmp != HANTRO_OK) || + (noOutputOfPriorPicsFlag != 0) || + (pStorage->dpb->noReordering) || + (oldSPS == NULL) || + (oldSPS->picWidthInMbs != newSPS->picWidthInMbs) || + (oldSPS->picHeightInMbs != newSPS->picHeightInMbs) || + (oldSPS->maxDpbSize != newSPS->maxDpbSize)) + { + pStorage->dpb->flushed = 0; + } + else + { + h264bsdFlushDpb(pStorage->dpb); + } + + pStorage->oldSpsId = pStorage->activeSpsId; + + return(H264BSD_HDRS_RDY); + } + } + + /* return error if second phase of + * initialization is not completed */ + if (pStorage->pendingActivation) + { + EPRINT("Pending activation not completed"); + return (H264BSD_ERROR); + } + tmp = h264bsdDecodeSliceHeader(&strm, pStorage->sliceHeader + 1, + pStorage->activeSps, pStorage->activePps, &nalUnit); + if (tmp != HANTRO_OK) + { + EPRINT("SLICE_HEADER"); + return(H264BSD_ERROR); + } + if (h264bsdIsStartOfPicture(pStorage)) + { + if (!IS_IDR_NAL_UNIT(&nalUnit)) + { + tmp = h264bsdCheckGapsInFrameNum(pStorage->dpb, + pStorage->sliceHeader[1].frameNum, + nalUnit.nalRefIdc != 0 ? + HANTRO_TRUE : HANTRO_FALSE, + pStorage->activeSps-> + gapsInFrameNumValueAllowedFlag); + if (tmp != HANTRO_OK) + { + EPRINT("Gaps in frame num"); + return(H264BSD_ERROR); + } + } + pStorage->currImage->data = + h264bsdAllocateDpbImage(pStorage->dpb); + } + + /* store slice header to storage if successfully decoded */ + pStorage->sliceHeader[0] = pStorage->sliceHeader[1]; + pStorage->validSliceInAccessUnit = HANTRO_TRUE; + pStorage->prevNalUnit[0] = nalUnit; + + h264bsdComputeSliceGroupMap(pStorage, + pStorage->sliceHeader->sliceGroupChangeCycle); + + h264bsdInitRefPicList(pStorage->dpb); + tmp = h264bsdReorderRefPicList(pStorage->dpb, + &pStorage->sliceHeader->refPicListReordering, + pStorage->sliceHeader->frameNum, + pStorage->sliceHeader->numRefIdxL0Active); + if (tmp != HANTRO_OK) + { + EPRINT("Reordering"); + return(H264BSD_ERROR); + } + + DEBUG(("SLICE DATA, FIRST %d\n", + pStorage->sliceHeader->firstMbInSlice)); + tmp = h264bsdDecodeSliceData(&strm, pStorage, + pStorage->currImage, pStorage->sliceHeader); + pStorage->strm[0] = strm; + if (pStorage->sliceDataPending) + { + /* Mid-slice timeslice: do not consume NAL bytes yet. */ + *readBytes = 0; + pStorage->prevBufNotFinished = HANTRO_TRUE; + return(H264BSD_RDY); + } + // in single slice mode every slice is a ready picture +#ifndef SINGLE_SLICE + if (tmp != HANTRO_OK) + { + EPRINT("SLICE_DATA"); +#ifndef OPTIMIZE_NO_DECODED_FLAG + h264bsdMarkSliceCorrupted(pStorage, + pStorage->sliceHeader->firstMbInSlice); +#endif + return(H264BSD_ERROR); + }; + + if (h264bsdIsEndOfPicture(pStorage)) + { +#endif + picReady = HANTRO_TRUE; +#ifndef SINGLE_SLICE + pStorage->skipRedundantSlices = HANTRO_TRUE; + }; +#endif + break; + + case NAL_SEI: + h264bsdDecodeSeiMessages(pStorage, &strm); + break; + + default: + DEBUG(("NOT IMPLEMENTED YET %d\n",nalUnit.nalUnitType)); + } + } + +picture_done: + if (picReady) + { + #ifndef H264BSD_N64 + // Deblocking filter currently not supported on N64. Even if it's + // disabled during encoding, it's useless to go through all macroblocks + // and find out that there's nothing to do, so let's just skip the + // call altogether. + h264bsdFilterPicture(pStorage->currImage, pStorage->mb); + #endif + +#ifdef EMIT_IMAGE_ASAP + + /*if (gLastSlice == 1){ + h264bsdWriteSliceMbData( + pStorage->currImage, + pStorage->sliceHeader->firstMbInSlice, + (pStorage->currImage->width * pStorage->currImage->height) - 1, + sliceData + ); + }else{*/ + h264bsdWriteSliceMbData( + pStorage->currImage, + pStorage->sliceHeader->firstMbInSlice, + pStorage->sliceHeader->firstMbInSlice + pStorage->slice->numDecodedMbs - 1, + sliceData + ); + //}; + + //sliceData[18] = pStorage->sliceHeader->firstMbInSlice; + //sliceData[19] = pStorage->sliceHeader->firstMbInSlice + pStorage->slice->numDecodedMbs - 1; + + extern_emit_image( + pStorage->currImage->data, + pStorage->currImage->width * 16, + pStorage->currImage->height * 16, + sliceData + ); +#endif + + h264bsdResetStorage(pStorage); + + picOrderCnt = h264bsdDecodePicOrderCnt(pStorage->poc, + pStorage->activeSps, pStorage->sliceHeader, pStorage->prevNalUnit); + + if (pStorage->validSliceInAccessUnit) + { + if (pStorage->prevNalUnit->nalRefIdc) + { + tmp = h264bsdMarkDecRefPic(pStorage->dpb, + &pStorage->sliceHeader->decRefPicMarking, + pStorage->currImage, pStorage->sliceHeader->frameNum, + picOrderCnt, + IS_IDR_NAL_UNIT(pStorage->prevNalUnit) ? + HANTRO_TRUE : HANTRO_FALSE, + pStorage->currentPicId, pStorage->numConcealedMbs); + } + /* non-reference picture, just store for possible display + * reordering */ + else + { + tmp = h264bsdMarkDecRefPic(pStorage->dpb, NULL, + pStorage->currImage, pStorage->sliceHeader->frameNum, + picOrderCnt, + IS_IDR_NAL_UNIT(pStorage->prevNalUnit) ? + HANTRO_TRUE : HANTRO_FALSE, + pStorage->currentPicId, pStorage->numConcealedMbs); + } + } + + pStorage->picStarted = HANTRO_FALSE; + pStorage->validSliceInAccessUnit = HANTRO_FALSE; + + return(H264BSD_PIC_RDY); + } + else + return(H264BSD_RDY); + +} + +u32 h264bsdDecode(storage_t *pStorage, u8 *byteStrm, u32 len, u32 picId, + u32 *readBytes) +{ + return h264bsdDecodePartial(pStorage, byteStrm, len, picId, readBytes, 0); +} + +u32 h264bsdIsSlicePending(storage_t *pStorage) +{ + ASSERT(pStorage); + return pStorage->sliceDataPending ? HANTRO_TRUE : HANTRO_FALSE; +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdShutdown + + Functional description: + Shutdown a decoder instance. Function frees all the memories + allocated for the decoder instance. + + Inputs: + pStorage pointer to storage data structure + + Returns: + none + + +------------------------------------------------------------------------------*/ + +void h264bsdShutdown(storage_t *pStorage) +{ + +/* Variables */ + + u32 i; + +/* Code */ + + ASSERT(pStorage); + + for (i = 0; i < MAX_NUM_SEQ_PARAM_SETS; i++) + { + if (pStorage->sps[i]) + { + FREE(pStorage->sps[i]->offsetForRefFrame); + FREE(pStorage->sps[i]->vuiParameters); + FREE(pStorage->sps[i]); + } + } + + for (i = 0; i < MAX_NUM_PIC_PARAM_SETS; i++) + { + if (pStorage->pps[i]) + { + FREE(pStorage->pps[i]->runLength); + FREE(pStorage->pps[i]->topLeft); + FREE(pStorage->pps[i]->bottomRight); + FREE(pStorage->pps[i]->sliceGroupId); + FREE(pStorage->pps[i]); + } + } + + #if 0 + FREE(pStorage->mbLayers); + #endif + FREE(pStorage->mb); + FREE(pStorage->sliceGroupMap); + + h264bsdFreeDpb(pStorage->dpb); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdNextOutputPicture + + Functional description: + Get next output picture in display order. + + Inputs: + pStorage pointer to storage data structure + + Outputs: + picId identifier of the picture will be stored here + isIdrPic IDR flag of the picture will be stored here + numErrMbs number of concealed macroblocks in the picture + will be stored here + + Returns: + pointer to the picture data + NULL if no pictures available for display + +------------------------------------------------------------------------------*/ + +u8* h264bsdNextOutputPicture(storage_t *pStorage, u32 *picId, u32 *isIdrPic, + u32 *numErrMbs) +{ + +/* Variables */ + + dpbOutPicture_t *pOut; + +/* Code */ + + ASSERT(pStorage); + + pOut = h264bsdDpbOutputPicture(pStorage->dpb); + + if (pOut != NULL) + { + *picId = pOut->picId; + *isIdrPic = pOut->isIdr; + *numErrMbs = pOut->numErrMbs; + return (pOut->data); + } + else + return(NULL); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdPicWidth + + Functional description: + Get width of the picture in macroblocks + + Inputs: + pStorage pointer to storage data structure + + Outputs: + none + + Returns: + picture width + 0 if parameters sets not yet activated + +------------------------------------------------------------------------------*/ + +u32 h264bsdPicWidth(storage_t *pStorage) +{ + +/* Variables */ + +/* Code */ + + ASSERT(pStorage); + + if (pStorage->activeSps) + return(pStorage->activeSps->picWidthInMbs); + else + return(0); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdPicHeight + + Functional description: + Get height of the picture in macroblocks + + Inputs: + pStorage pointer to storage data structure + + Outputs: + none + + Returns: + picture width + 0 if parameters sets not yet activated + +------------------------------------------------------------------------------*/ + +u32 h264bsdPicHeight(storage_t *pStorage) +{ + +/* Variables */ + +/* Code */ + + ASSERT(pStorage); + + if (pStorage->activeSps) + return(pStorage->activeSps->picHeightInMbs); + else + return(0); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdFlushBuffer + + Functional description: + Flush the decoded picture buffer, see dpb.c for details + + Inputs: + pStorage pointer to storage data structure + +------------------------------------------------------------------------------*/ + +void h264bsdFlushBuffer(storage_t *pStorage) +{ + +/* Variables */ + +/* Code */ + + ASSERT(pStorage); + + h264bsdFlushDpb(pStorage->dpb); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdCheckValidParamSets + + Functional description: + Check if any valid parameter set combinations (SPS/PPS) exists. + + Inputs: + pStorage pointer to storage structure + + Returns: + 1 at least one valid SPS/PPS combination found + 0 no valid param set combinations found + + +------------------------------------------------------------------------------*/ + +u32 h264bsdCheckValidParamSets(storage_t *pStorage) +{ + +/* Variables */ + +/* Code */ + + ASSERT(pStorage); + + return(h264bsdValidParamSets(pStorage) == HANTRO_OK ? 1 : 0); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdVideoRange + + Functional description: + Get value of video_full_range_flag received in the VUI data. + + Inputs: + pStorage pointer to storage structure + + Returns: + 1 video_full_range_flag received and value is 1 + 0 otherwise + +------------------------------------------------------------------------------*/ + +u32 h264bsdVideoRange(storage_t *pStorage) +{ + +/* Variables */ + +/* Code */ + + ASSERT(pStorage); + + if (pStorage->activeSps && pStorage->activeSps->vuiParametersPresentFlag && + pStorage->activeSps->vuiParameters && + pStorage->activeSps->vuiParameters->videoSignalTypePresentFlag && + pStorage->activeSps->vuiParameters->videoFullRangeFlag) + return(1); + else /* default value of video_full_range_flag is 0 */ + return(0); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdMatrixCoefficients + + Functional description: + Get value of matrix_coefficients received in the VUI data + + Inputs: + pStorage pointer to storage structure + + Outputs: + value of matrix_coefficients if received + 2 otherwise (this is the default value) + +------------------------------------------------------------------------------*/ + +u32 h264bsdMatrixCoefficients(storage_t *pStorage) +{ + +/* Variables */ + +/* Code */ + + ASSERT(pStorage); + + if (pStorage->activeSps && pStorage->activeSps->vuiParametersPresentFlag && + pStorage->activeSps->vuiParameters && + pStorage->activeSps->vuiParameters->videoSignalTypePresentFlag && + pStorage->activeSps->vuiParameters->colourDescriptionPresentFlag) + return(pStorage->activeSps->vuiParameters->matrixCoefficients); + else /* default unspecified */ + return(2); + +} + +/*------------------------------------------------------------------------------ + + Function: hh264bsdCroppingParams + + Functional description: + Get cropping parameters of the active SPS + + Inputs: + pStorage pointer to storage structure + + Outputs: + croppingFlag flag indicating if cropping params present is + stored here + leftOffset cropping left offset in pixels is stored here + width width of the image after cropping is stored here + topOffset cropping top offset in pixels is stored here + height height of the image after cropping is stored here + + Returns: + none + +------------------------------------------------------------------------------*/ + +void h264bsdCroppingParams(storage_t *pStorage, u32 *croppingFlag, + u32 *leftOffset, u32 *width, u32 *topOffset, u32 *height) +{ + +/* Variables */ + +/* Code */ + + ASSERT(pStorage); + + if (pStorage->activeSps && pStorage->activeSps->frameCroppingFlag) + { + *croppingFlag = 1; + *leftOffset = 2 * pStorage->activeSps->frameCropLeftOffset; + *width = 16 * pStorage->activeSps->picWidthInMbs - + 2 * (pStorage->activeSps->frameCropLeftOffset + + pStorage->activeSps->frameCropRightOffset); + *topOffset = 2 * pStorage->activeSps->frameCropTopOffset; + *height = 16 * pStorage->activeSps->picHeightInMbs - + 2 * (pStorage->activeSps->frameCropTopOffset + + pStorage->activeSps->frameCropBottomOffset); + } + else + { + *croppingFlag = 0; + *leftOffset = 0; + *width = 0; + *topOffset = 0; + *height = 0; + } + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdSampleAspectRatio + + Functional description: + Get aspect ratio received in the VUI data + + Inputs: + pStorage pointer to storage structure + + Outputs: + sarWidth sample aspect ratio height + sarHeight sample aspect ratio width + +------------------------------------------------------------------------------*/ + +void h264bsdSampleAspectRatio(storage_t *pStorage, u32 *sarWidth, u32 *sarHeight) +{ + +/* Variables */ + u32 w = 1; + u32 h = 1; +/* Code */ + + ASSERT(pStorage); + + + if (pStorage->activeSps && + pStorage->activeSps->vuiParametersPresentFlag && + pStorage->activeSps->vuiParameters && + pStorage->activeSps->vuiParameters->aspectRatioPresentFlag ) + { + switch (pStorage->activeSps->vuiParameters->aspectRatioIdc) + { + case ASPECT_RATIO_UNSPECIFIED: w = 0; h = 0; break; + case ASPECT_RATIO_1_1: w = 1; h = 1; break; + case ASPECT_RATIO_12_11: w = 12; h = 11; break; + case ASPECT_RATIO_10_11: w = 10; h = 11; break; + case ASPECT_RATIO_16_11: w = 16; h = 11; break; + case ASPECT_RATIO_40_33: w = 40; h = 33; break; + case ASPECT_RATIO_24_11: w = 24; h = 11; break; + case ASPECT_RATIO_20_11: w = 20; h = 11; break; + case ASPECT_RATIO_32_11: w = 32; h = 11; break; + case ASPECT_RATIO_80_33: w = 80; h = 33; break; + case ASPECT_RATIO_18_11: w = 18; h = 11; break; + case ASPECT_RATIO_15_11: w = 15; h = 11; break; + case ASPECT_RATIO_64_33: w = 64; h = 33; break; + case ASPECT_RATIO_160_99: w = 160; h = 99; break; + case ASPECT_RATIO_EXTENDED_SAR: + w = pStorage->activeSps->vuiParameters->sarWidth; + h = pStorage->activeSps->vuiParameters->sarHeight; + if ((w == 0) || (h == 0)) + w = h = 0; + break; + default: + w = 0; + h = 0; + break; + } + } + + /* set aspect ratio*/ + *sarWidth = w; + *sarHeight = h; + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdProfile + + Functional description: + Get profile information from active SPS + + Inputs: + pStorage pointer to storage structure + + Outputs: + profile current profile + +------------------------------------------------------------------------------*/ +u32 h264bsdProfile(storage_t *pStorage) +{ + if (pStorage->activeSps) + return pStorage->activeSps->profileIdc; + else + return 0; +} + diff --git a/src/video/h264_decoder/h264bsd_decoder.h b/src/video/h264_decoder/h264bsd_decoder.h new file mode 100644 index 000000000..a6340cdf5 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_decoder.h @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_DECODER_H +#define H264SWDEC_DECODER_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_storage.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/* enumerated return values of the functions */ +enum { + H264BSD_RDY, + H264BSD_PIC_RDY, + H264BSD_HDRS_RDY, + H264BSD_ERROR, + H264BSD_PARAM_SET_ERROR, + H264BSD_MEMALLOC_ERROR +}; + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +u32 h264bsdInit(storage_t *pStorage, u32 noOutputReordering); + +/** + * @brief Decode stream data, optionally timesliced by macroblock count + * + * Same as h264bsdDecode, but stops after decoding at most @p mbBudget + * macroblocks of the current slice (0 = no limit, finish the whole slice / + * NAL as before). When the budget is exhausted mid-slice the function returns + * H264BSD_RDY with *readBytes == 0; call again with the same buffer pointer + * to continue. Use h264bsdIsSlicePending() to query that state. + */ +u32 h264bsdDecodePartial(storage_t *pStorage, u8 *byteStrm, u32 len, u32 picId, + u32 *readBytes, u32 mbBudget); + +/** @brief Decode one NAL / finish the current slice (unlimited MB budget) */ +u32 h264bsdDecode(storage_t *pStorage, u8 *byteStrm, u32 len, u32 picId, + u32 *readBytes); + +/** @brief True while a previous h264bsdDecodePartial() stopped mid-slice */ +u32 h264bsdIsSlicePending(storage_t *pStorage); + +void h264bsdShutdown(storage_t *pStorage); + +/** + * @brief Configure how many fully decoded pictures can be cached in the DPB output buffer + * + * This must be called before decoding starts (before the first parameter sets + * activation / DPB initialization), as it affects DPB memory sizing. + * + * @param pStorage decoder storage + * @param numPics number of buffered output pictures (0 = disabled) + */ +void h264bsdSetNumBufferedPics(storage_t *pStorage, u32 numPics); + +/** + * @brief Configure a callback for decoded SEI payloads + * + * The callback is called once for each SEI payload after emulation-prevention + * bytes have been removed. Pass NULL to disable SEI callbacks. + * + * @param pStorage decoder storage + * @param callback callback invoked for each SEI payload, or NULL + * @param ctx user context passed back to the callback + */ +void h264bsdSetSeiCallback(storage_t *pStorage, + h264bsdSeiCallback callback, void *ctx); + +u8* h264bsdNextOutputPicture(storage_t *pStorage, u32 *picId, u32 *isIdrPic, + u32 *numErrMbs); + +u32 h264bsdPicWidth(storage_t *pStorage); +u32 h264bsdPicHeight(storage_t *pStorage); +u32 h264bsdVideoRange(storage_t *pStorage); +u32 h264bsdMatrixCoefficients(storage_t *pStorage); +void h264bsdCroppingParams(storage_t *pStorage, u32 *croppingFlag, + u32 *left, u32 *width, u32 *top, u32 *height); +void h264bsdSampleAspectRatio(storage_t *pStorage, + u32 *sarWidth, u32 *sarHeight); +u32 h264bsdCheckValidParamSets(storage_t *pStorage); + +void h264bsdFlushBuffer(storage_t *pStorage); + +u32 h264bsdProfile(storage_t *pStorage); + +#endif /* #ifdef H264SWDEC_DECODER_H */ + diff --git a/src/video/h264_decoder/h264bsd_dpb.c b/src/video/h264_decoder/h264bsd_dpb.c new file mode 100644 index 000000000..1bd1092df --- /dev/null +++ b/src/video/h264_decoder/h264bsd_dpb.c @@ -0,0 +1,1756 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + ComparePictures + h264bsdReorderRefPicList + Mmcop1 + Mmcop2 + Mmcop3 + Mmcop4 + Mmcop5 + Mmcop6 + h264bsdMarkDecRefPic + h264bsdGetRefPicData + h264bsdAllocateDpbImage + SlidingWindowRefPicMarking + h264bsdInitDpb + h264bsdResetDpb + h264bsdInitRefPicList + FindDpbPic + SetPicNums + h264bsdCheckGapsInFrameNum + FindSmallestPicOrderCnt + OutputPicture + h264bsdDpbOutputPicture + h264bsdFlushDpb + h264bsdFreeDpb + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_cfg.h" +#include "h264bsd_dpb.h" +#include "h264bsd_slice_header.h" +#include "h264bsd_image.h" +#include "h264bsd_util.h" +#include "basetype.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/* macros to determine picture status. Note that IS_SHORT_TERM macro returns + * true also for non-existing pictures because non-existing pictures are + * regarded short term pictures according to H.264 standard */ +#define IS_REFERENCE(a) ((a).status) +#define IS_EXISTING(a) ((a).status > NON_EXISTING) +#define IS_SHORT_TERM(a) \ + ((a).status == NON_EXISTING || (a).status == SHORT_TERM) +#define IS_LONG_TERM(a) ((a).status == LONG_TERM) + +/* macro to set a picture unused for reference */ +#define SET_UNUSED(a) (a).status = UNUSED; + +#define MAX_NUM_REF_IDX_L0_ACTIVE 16 + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +static i32 ComparePictures(const void *ptr1, const void *ptr2); + +static u32 Mmcop1(dpbStorage_t *dpb, u32 currPicNum, u32 differenceOfPicNums); + +static u32 Mmcop2(dpbStorage_t *dpb, u32 longTermPicNum); + +static u32 Mmcop3(dpbStorage_t *dpb, u32 currPicNum, u32 differenceOfPicNums, + u32 longTermFrameIdx); + +static u32 Mmcop4(dpbStorage_t *dpb, u32 maxLongTermFrameIdx); + +static u32 Mmcop5(dpbStorage_t *dpb); + +static u32 Mmcop6(dpbStorage_t *dpb, u32 frameNum, i32 picOrderCnt, + u32 longTermFrameIdx); + +static u32 SlidingWindowRefPicMarking(dpbStorage_t *dpb); + +static i32 FindDpbPic(dpbStorage_t *dpb, i32 picNum, u32 isShortTerm); + +static void SetPicNums(dpbStorage_t *dpb, u32 currFrameNum); + +static dpbPicture_t* FindSmallestPicOrderCnt(dpbStorage_t *dpb); + +static u32 OutputPicture(dpbStorage_t *dpb); + +static void ShellSort(dpbPicture_t *pPic, u32 num); + +/*------------------------------------------------------------------------------ + + Function: ComparePictures + + Functional description: + Function to compare dpb pictures, used by the ShellSort() function. + Order of the pictures after sorting shall be as follows: + 1) short term reference pictures starting with the largest + picNum + 2) long term reference pictures starting with the smallest + longTermPicNum + 3) pictures unused for reference but needed for display + 4) other pictures + + Returns: + -1 pic 1 is greater than pic 2 + 0 equal from comparison point of view + 1 pic 2 is greater then pic 1 + +------------------------------------------------------------------------------*/ + +static i32 ComparePictures(const void *ptr1, const void *ptr2) +{ + +/* Variables */ + + dpbPicture_t *pic1, *pic2; + +/* Code */ + + ASSERT(ptr1); + ASSERT(ptr2); + + pic1 = (dpbPicture_t*)ptr1; + pic2 = (dpbPicture_t*)ptr2; + + /* pictures in the output buffer are not available for processing, + put them at the beginning of the array. */ + if (pic1->inOutputBuf && !pic2->inOutputBuf) + return(-1); + else if (!pic1->inOutputBuf && pic2->inOutputBuf) + return(1); + + /* both are non-reference pictures, check if needed for display */ + if (!IS_REFERENCE(*pic1) && !IS_REFERENCE(*pic2)) + { + if (pic1->toBeDisplayed && !pic2->toBeDisplayed) + return(-1); + else if (!pic1->toBeDisplayed && pic2->toBeDisplayed) + return(1); + else + return(0); + } + /* only pic 1 needed for reference -> greater */ + else if (!IS_REFERENCE(*pic2)) + return(-1); + /* only pic 2 needed for reference -> greater */ + else if (!IS_REFERENCE(*pic1)) + return(1); + /* both are short term reference pictures -> check picNum */ + else if (IS_SHORT_TERM(*pic1) && IS_SHORT_TERM(*pic2)) + { + if (pic1->picNum > pic2->picNum) + return(-1); + else if (pic1->picNum < pic2->picNum) + return(1); + else + return(0); + } + /* only pic 1 is short term -> greater */ + else if (IS_SHORT_TERM(*pic1)) + return(-1); + /* only pic 2 is short term -> greater */ + else if (IS_SHORT_TERM(*pic2)) + return(1); + /* both are long term reference pictures -> check picNum (contains the + * longTermPicNum */ + else + { + if (pic1->picNum > pic2->picNum) + return(1); + else if (pic1->picNum < pic2->picNum) + return(-1); + else + return(0); + } +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdReorderRefPicList + + Functional description: + Function to perform reference picture list reordering based on + reordering commands received in the slice header. See details + of the process in the H.264 standard. + + Inputs: + dpb pointer to dpb storage structure + order pointer to reordering commands + currFrameNum current frame number + numRefIdxActive number of active reference indices for current + picture + + Outputs: + dpb 'list' field of the structure reordered + + Returns: + HANTRO_OK success + HANTRO_NOK if non-existing pictures referred to in the + reordering commands + +------------------------------------------------------------------------------*/ + +u32 h264bsdReorderRefPicList( + dpbStorage_t *dpb, + refPicListReordering_t *order, + u32 currFrameNum, + u32 numRefIdxActive) +{ + +/* Variables */ + + u32 i, j, k, picNumPred, refIdx; + i32 picNum, picNumNoWrap, index; + u32 isShortTerm; + +/* Code */ + + ASSERT(order); + ASSERT(currFrameNum <= dpb->maxFrameNum); + ASSERT(numRefIdxActive <= MAX_NUM_REF_IDX_L0_ACTIVE); + + /* set dpb picture numbers for sorting */ + SetPicNums(dpb, currFrameNum); + + if (!order->refPicListReorderingFlagL0) + return(HANTRO_OK); + + refIdx = 0; + picNumPred = currFrameNum; + + i = 0; + while (order->command[i].reorderingOfPicNumsIdc < 3) + { + /* short term */ + if (order->command[i].reorderingOfPicNumsIdc < 2) + { + if (order->command[i].reorderingOfPicNumsIdc == 0) + { + picNumNoWrap = + (i32)picNumPred - (i32)order->command[i].absDiffPicNum; + if (picNumNoWrap < 0) + picNumNoWrap += (i32)dpb->maxFrameNum; + } + else + { + picNumNoWrap = + (i32)(picNumPred + order->command[i].absDiffPicNum); + if (picNumNoWrap >= (i32)dpb->maxFrameNum) + picNumNoWrap -= (i32)dpb->maxFrameNum; + } + picNumPred = (u32)picNumNoWrap; + picNum = picNumNoWrap; + if ((u32)picNumNoWrap > currFrameNum) + picNum -= (i32)dpb->maxFrameNum; + isShortTerm = HANTRO_TRUE; + } + /* long term */ + else + { + picNum = (i32)order->command[i].longTermPicNum; + isShortTerm = HANTRO_FALSE; + + } + /* find corresponding picture from dpb */ + index = FindDpbPic(dpb, picNum, isShortTerm); + if (index < 0 || !IS_EXISTING(dpb->buffer[index])) + return(HANTRO_NOK); + + /* shift pictures */ + for (j = numRefIdxActive; j > refIdx; j--) + dpb->list[j] = dpb->list[j-1]; + /* put picture into the list */ + dpb->list[refIdx++] = &dpb->buffer[index]; + /* remove later references to the same picture */ + for (j = k = refIdx; j <= numRefIdxActive; j++) + if(dpb->list[j] != &dpb->buffer[index]) + dpb->list[k++] = dpb->list[j]; + + i++; + } + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: Mmcop1 + + Functional description: + Function to mark a short-term reference picture unused for + reference, memory_management_control_operation equal to 1 + + Returns: + HANTRO_OK success + HANTRO_NOK failure, picture does not exist in the buffer + +------------------------------------------------------------------------------*/ + +static u32 Mmcop1(dpbStorage_t *dpb, u32 currPicNum, u32 differenceOfPicNums) +{ + +/* Variables */ + + i32 index, picNum; + +/* Code */ + + ASSERT(currPicNum < dpb->maxFrameNum); + + picNum = (i32)currPicNum - (i32)differenceOfPicNums; + + index = FindDpbPic(dpb, picNum, HANTRO_TRUE); + if (index < 0) + return(HANTRO_NOK); + + SET_UNUSED(dpb->buffer[index]); + dpb->numRefFrames--; + if (!dpb->buffer[index].toBeDisplayed) + dpb->fullness--; + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: Mmcop2 + + Functional description: + Function to mark a long-term reference picture unused for + reference, memory_management_control_operation equal to 2 + + Returns: + HANTRO_OK success + HANTRO_NOK failure, picture does not exist in the buffer + +------------------------------------------------------------------------------*/ + +static u32 Mmcop2(dpbStorage_t *dpb, u32 longTermPicNum) +{ + +/* Variables */ + + i32 index; + +/* Code */ + + index = FindDpbPic(dpb, (i32)longTermPicNum, HANTRO_FALSE); + if (index < 0) + return(HANTRO_NOK); + + SET_UNUSED(dpb->buffer[index]); + dpb->numRefFrames--; + if (!dpb->buffer[index].toBeDisplayed) + dpb->fullness--; + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: Mmcop3 + + Functional description: + Function to assing a longTermFrameIdx to a short-term reference + frame (i.e. to change it to a long-term reference picture), + memory_management_control_operation equal to 3 + + Returns: + HANTRO_OK success + HANTRO_NOK failure, short-term picture does not exist in the + buffer or is a non-existing picture, or invalid + longTermFrameIdx given + +------------------------------------------------------------------------------*/ + +static u32 Mmcop3(dpbStorage_t *dpb, u32 currPicNum, u32 differenceOfPicNums, + u32 longTermFrameIdx) +{ + +/* Variables */ + + i32 index, picNum; + u32 i; + +/* Code */ + + ASSERT(dpb); + ASSERT(currPicNum < dpb->maxFrameNum); + + if ( (dpb->maxLongTermFrameIdx == NO_LONG_TERM_FRAME_INDICES) || + (longTermFrameIdx > dpb->maxLongTermFrameIdx) ) + return(HANTRO_NOK); + + /* check if a long term picture with the same longTermFrameIdx already + * exist and remove it if necessary */ + for (i = 0; i < dpb->maxRefFrames; i++) + if (IS_LONG_TERM(dpb->buffer[i]) && + (u32)dpb->buffer[i].picNum == longTermFrameIdx) + { + SET_UNUSED(dpb->buffer[i]); + dpb->numRefFrames--; + if (!dpb->buffer[i].toBeDisplayed) + dpb->fullness--; + break; + } + + picNum = (i32)currPicNum - (i32)differenceOfPicNums; + + index = FindDpbPic(dpb, picNum, HANTRO_TRUE); + if (index < 0) + return(HANTRO_NOK); + if (!IS_EXISTING(dpb->buffer[index])) + return(HANTRO_NOK); + + dpb->buffer[index].status = LONG_TERM; + dpb->buffer[index].picNum = (i32)longTermFrameIdx; + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: Mmcop4 + + Functional description: + Function to set maxLongTermFrameIdx, + memory_management_control_operation equal to 4 + + Returns: + HANTRO_OK success + +------------------------------------------------------------------------------*/ + +static u32 Mmcop4(dpbStorage_t *dpb, u32 maxLongTermFrameIdx) +{ + +/* Variables */ + + u32 i; + +/* Code */ + + dpb->maxLongTermFrameIdx = maxLongTermFrameIdx; + + for (i = 0; i < dpb->maxRefFrames; i++) + if (IS_LONG_TERM(dpb->buffer[i]) && + ( ((u32)dpb->buffer[i].picNum > maxLongTermFrameIdx) || + (dpb->maxLongTermFrameIdx == NO_LONG_TERM_FRAME_INDICES) ) ) + { + SET_UNUSED(dpb->buffer[i]); + dpb->numRefFrames--; + if (!dpb->buffer[i].toBeDisplayed) + dpb->fullness--; + } + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: Mmcop5 + + Functional description: + Function to mark all reference pictures unused for reference and + set maxLongTermFrameIdx to NO_LONG_TERM_FRAME_INDICES, + memory_management_control_operation equal to 5. Function flushes + the buffer and places all pictures that are needed for display into + the output buffer. + + Returns: + HANTRO_OK success + +------------------------------------------------------------------------------*/ + +static u32 Mmcop5(dpbStorage_t *dpb) +{ + +/* Variables */ + + u32 i; + +/* Code */ + + for (i = 0; i < dpb->dpbSize; i++) + { + if (IS_REFERENCE(dpb->buffer[i])) + { + SET_UNUSED(dpb->buffer[i]); + if (!dpb->buffer[i].toBeDisplayed) + dpb->fullness--; + } + } + + /* output all pictures */ + while (OutputPicture(dpb) == HANTRO_OK) + ; + dpb->numRefFrames = 0; + dpb->maxLongTermFrameIdx = NO_LONG_TERM_FRAME_INDICES; + dpb->prevRefFrameNum = 0; + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: Mmcop6 + + Functional description: + Function to assign longTermFrameIdx to the current picture, + memory_management_control_operation equal to 6 + + Returns: + HANTRO_OK success + HANTRO_NOK invalid longTermFrameIdx or no room for current + picture in the buffer + +------------------------------------------------------------------------------*/ + +static u32 Mmcop6(dpbStorage_t *dpb, u32 frameNum, i32 picOrderCnt, + u32 longTermFrameIdx) +{ + +/* Variables */ + + u32 i; + +/* Code */ + + ASSERT(frameNum < dpb->maxFrameNum); + + if ( (dpb->maxLongTermFrameIdx == NO_LONG_TERM_FRAME_INDICES) || + (longTermFrameIdx > dpb->maxLongTermFrameIdx) ) + return(HANTRO_NOK); + + /* check if a long term picture with the same longTermFrameIdx already + * exist and remove it if necessary */ + for (i = 0; i < dpb->maxRefFrames; i++) + if (IS_LONG_TERM(dpb->buffer[i]) && + (u32)dpb->buffer[i].picNum == longTermFrameIdx) + { + SET_UNUSED(dpb->buffer[i]); + dpb->numRefFrames--; + if (!dpb->buffer[i].toBeDisplayed) + dpb->fullness--; + break; + } + + if (dpb->numRefFrames < dpb->maxRefFrames) + { + dpb->currentOut->frameNum = frameNum; + dpb->currentOut->picNum = (i32)longTermFrameIdx; + dpb->currentOut->picOrderCnt = picOrderCnt; + dpb->currentOut->status = LONG_TERM; + if (dpb->noReordering) + dpb->currentOut->toBeDisplayed = HANTRO_FALSE; + else + dpb->currentOut->toBeDisplayed = HANTRO_TRUE; + dpb->numRefFrames++; + dpb->fullness++; + return(HANTRO_OK); + } + /* if there is no room, return an error */ + else + return(HANTRO_NOK); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdMarkDecRefPic + + Functional description: + Function to perform reference picture marking process. This + function should be called both for reference and non-reference + pictures. Non-reference pictures shall have mark pointer set to + NULL. + + Inputs: + dpb pointer to the DPB data structure + mark pointer to reference picture marking commands + image pointer to current picture to be placed in the buffer + frameNum frame number of the current picture + picOrderCnt picture order count for the current picture + isIdr flag to indicate if the current picture is an + IDR picture + currentPicId identifier for the current picture, from the + application, stored along with the picture + numErrMbs number of concealed macroblocks in the current + picture, stored along with the picture + + Outputs: + dpb 'buffer' modified, possible output frames placed into + 'outBuf' + + Returns: + HANTRO_OK success + HANTRO_NOK failure + +------------------------------------------------------------------------------*/ + +u32 h264bsdMarkDecRefPic( + dpbStorage_t *dpb, + decRefPicMarking_t *mark, + image_t *image, + u32 frameNum, + i32 picOrderCnt, + u32 isIdr, + u32 currentPicId, + u32 numErrMbs) +{ + +/* Variables */ + + u32 i, status; + u32 markedAsLongTerm; + u32 toBeDisplayed; + +/* Code */ + + ASSERT(dpb); + ASSERT(mark || !isIdr); + ASSERT(!isIdr || (frameNum == 0 && picOrderCnt == 0)); + ASSERT(frameNum < dpb->maxFrameNum); + + if (image->data != dpb->currentOut->data) + { + EPRINT("TRYING TO MARK NON-ALLOCATED IMAGE"); + return(HANTRO_NOK); + } + + dpb->lastContainsMmco5 = HANTRO_FALSE; + status = HANTRO_OK; + + toBeDisplayed = dpb->noReordering ? HANTRO_FALSE : HANTRO_TRUE; + + /* non-reference picture, stored for display reordering purposes */ + if (mark == NULL) + { + dpb->currentOut->status = UNUSED; + dpb->currentOut->frameNum = frameNum; + dpb->currentOut->picNum = (i32)frameNum; + dpb->currentOut->picOrderCnt = picOrderCnt; + dpb->currentOut->toBeDisplayed = toBeDisplayed; + if (!dpb->noReordering) + dpb->fullness++; + } + /* IDR picture */ + else if (isIdr) + { + if (dpb->maxNumBufferedPics == 0) { + /* h264bsdCheckGapsInFrameNum not called for IDR pictures -> have to + * reset numOut and outIndex here */ + dpb->numOut = dpb->outIndex = 0; + } + + /* flush the buffer */ + Mmcop5(dpb); + /* if noOutputOfPriorPicsFlag was set -> the pictures preceding the + * IDR picture shall not be output -> set output buffer empty */ + if (mark->noOutputOfPriorPicsFlag || dpb->noReordering) + { + if (dpb->maxNumBufferedPics == 0) { + dpb->numOut = 0; + dpb->outIndex = 0; + } else { + /* TODO(rasky): in baseline profile, noReordering should always be TRUE. + * If it was FALSE, I don't know how to handle noOutputOfPriorPicsFlag, + * because we keep pictures in output buffer as cache for the decoder, + * so we can't throw all of them away. */ + ASSERT(!(!dpb->noReordering && mark->noOutputOfPriorPicsFlag)); + } + } + + if (mark->longTermReferenceFlag) + { + dpb->currentOut->status = LONG_TERM; + dpb->maxLongTermFrameIdx = 0; + } + else + { + dpb->currentOut->status = SHORT_TERM; + dpb->maxLongTermFrameIdx = NO_LONG_TERM_FRAME_INDICES; + } + dpb->currentOut->frameNum = 0; + dpb->currentOut->picNum = 0; + dpb->currentOut->picOrderCnt = 0; + dpb->currentOut->toBeDisplayed = toBeDisplayed; + dpb->fullness = 1; + dpb->numRefFrames = 1; + } + /* reference picture */ + else + { + markedAsLongTerm = HANTRO_FALSE; + if (mark->adaptiveRefPicMarkingModeFlag) + { + i = 0; + while (mark->operation[i].memoryManagementControlOperation) + { + switch (mark->operation[i].memoryManagementControlOperation) + { + case 1: + status = Mmcop1( + dpb, + frameNum, + mark->operation[i].differenceOfPicNums); + break; + + case 2: + status = Mmcop2(dpb, mark->operation[i].longTermPicNum); + break; + + case 3: + status = Mmcop3( + dpb, + frameNum, + mark->operation[i].differenceOfPicNums, + mark->operation[i].longTermFrameIdx); + break; + + case 4: + status = Mmcop4( + dpb, + mark->operation[i].maxLongTermFrameIdx); + break; + + case 5: + status = Mmcop5(dpb); + dpb->lastContainsMmco5 = HANTRO_TRUE; + frameNum = 0; + break; + + case 6: + status = Mmcop6( + dpb, + frameNum, + picOrderCnt, + mark->operation[i].longTermFrameIdx); + if (status == HANTRO_OK) + markedAsLongTerm = HANTRO_TRUE; + break; + + default: /* invalid memory management control operation */ + status = HANTRO_NOK; + break; + } + if (status != HANTRO_OK) + { + break; + } + i++; + } + } + else + { + status = SlidingWindowRefPicMarking(dpb); + } + /* if current picture was not marked as long-term reference by + * memory management control operation 6 -> mark current as short + * term and insert it into dpb (if there is room) */ + if (!markedAsLongTerm) + { + if (dpb->numRefFrames < dpb->maxRefFrames) + { + dpb->currentOut->frameNum = frameNum; + dpb->currentOut->picNum = (i32)frameNum; + dpb->currentOut->picOrderCnt = picOrderCnt; + dpb->currentOut->status = SHORT_TERM; + dpb->currentOut->toBeDisplayed = toBeDisplayed; + dpb->fullness++; + dpb->numRefFrames++; + } + /* no room */ + else + { + status = HANTRO_NOK; + } + } + } + + dpb->currentOut->isIdr = isIdr; + dpb->currentOut->picId = currentPicId; + dpb->currentOut->numErrMbs = numErrMbs; + + /* dpb was initialized to not to reorder the pictures -> output current + * picture immediately */ + if (dpb->noReordering) + { + if (dpb->numOut >= dpb->dpbSize + 1 && dpb->outIndex) { + int left = dpb->numOut - dpb->outIndex; + for (int j = 0; j < left; j++) + dpb->outBuf[j] = dpb->outBuf[dpb->outIndex + j]; + dpb->numOut = left; + dpb->outIndex = 0; + } + ASSERT(dpb->numOut < dpb->dpbSize+1); + dpb->outBuf[dpb->numOut].data = dpb->currentOut->data; + dpb->outBuf[dpb->numOut].isIdr = dpb->currentOut->isIdr; + dpb->outBuf[dpb->numOut].picId = dpb->currentOut->picId; + dpb->outBuf[dpb->numOut].numErrMbs = dpb->currentOut->numErrMbs; + dpb->currentOut->inOutputBuf = 1; + dpb->numOut++; + } + else + { + /* output pictures if buffer full */ + while (dpb->fullness > dpb->dpbSize) + { + i = OutputPicture(dpb); + ASSERT(i == HANTRO_OK); + } + } + + /* sort dpb */ + ShellSort(dpb->buffer, dpb->dpbSize+1); + + return(status); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdGetRefPicData + + Functional description: + Function to get reference picture data from the reference picture + list + + Returns: + pointer to desired reference picture data + NULL if invalid index or non-existing picture referred + +------------------------------------------------------------------------------*/ + +u8* h264bsdGetRefPicData(dpbStorage_t *dpb, u32 index) +{ + +/* Variables */ + +/* Code */ + + if(index > MAX_NUM_REF_IDX_L0_ACTIVE || dpb->list[index] == NULL) + return(NULL); + else if(!IS_EXISTING(*dpb->list[index])) + return(NULL); + else + return(dpb->list[index]->data); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdAllocateDpbImage + + Functional description: + function to allocate memory for a image. This function does not + really allocate any memory but reserves one of the buffer + positions for decoding of current picture + + Returns: + pointer to memory area for the image + + +------------------------------------------------------------------------------*/ + +u8* h264bsdAllocateDpbImage(dpbStorage_t *dpb) +{ + +/* Variables */ + +/* Code */ + + ASSERT( !dpb->buffer[dpb->dpbSize].toBeDisplayed && + !IS_REFERENCE(dpb->buffer[dpb->dpbSize]) ); + ASSERT(dpb->fullness <= dpb->dpbSize); + + dpb->currentOut = dpb->buffer + dpb->dpbSize; + ASSERT(dpb->currentOut->inOutputBuf == 0); + + return(dpb->currentOut->data); + +} + +/*------------------------------------------------------------------------------ + + Function: SlidingWindowRefPicMarking + + Functional description: + Function to perform sliding window refence picture marking process. + + Outputs: + HANTRO_OK success + HANTRO_NOK failure, no short-term reference frame found that + could be marked unused + + +------------------------------------------------------------------------------*/ + +static u32 SlidingWindowRefPicMarking(dpbStorage_t *dpb) +{ + +/* Variables */ + + i32 index, picNum; + u32 i; + +/* Code */ + + if (dpb->numRefFrames < dpb->maxRefFrames) + { + return(HANTRO_OK); + } + else + { + index = -1; + picNum = 0; + /* find the oldest short term picture */ + for (i = 0; i < dpb->numRefFrames; i++) + if (IS_SHORT_TERM(dpb->buffer[i])) + if (dpb->buffer[i].picNum < picNum || index == -1) + { + index = (i32)i; + picNum = dpb->buffer[i].picNum; + } + if (index >= 0) + { + SET_UNUSED(dpb->buffer[index]); + dpb->numRefFrames--; + if (!dpb->buffer[index].toBeDisplayed) + dpb->fullness--; + + return(HANTRO_OK); + } + } + + return(HANTRO_NOK); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdInitDpb + + Functional description: + Function to initialize DPB. Reserves memories for the buffer, + reference picture list and output buffer. dpbSize indicates + the maximum DPB size indicated by the levelIdc in the stream. + If noReordering flag is FALSE the DPB stores dpbSize pictures + for display reordering purposes. On the other hand, if the + flag is TRUE the DPB only stores maxRefFrames reference pictures + and outputs all the pictures immediately. + + Inputs: + picSizeInMbs picture size in macroblocks + dpbSize size of the DPB (number of pictures) + maxRefFrames max number of reference frames + maxFrameNum max frame number + noReordering flag to indicate that DPB does not have to + prepare to reorder frames for display + + Outputs: + dpb pointer to dpb data storage + + Returns: + HANTRO_OK success + MEMORY_ALLOCATION_ERROR if memory allocation failed + +------------------------------------------------------------------------------*/ + +u32 h264bsdInitDpb( + dpbStorage_t *dpb, + u32 picSizeInMbs, + u32 dpbSize, + u32 maxRefFrames, + u32 maxFrameNum, + u32 noReordering, + u32 maxNumBufferedPics) +{ + +/* Variables */ + + u32 i; + +/* Code */ + + ASSERT(picSizeInMbs); + ASSERT(maxRefFrames <= MAX_NUM_REF_PICS); + ASSERT(maxRefFrames <= dpbSize); + ASSERT(maxFrameNum); + ASSERT(dpbSize); + + dpb->maxLongTermFrameIdx = NO_LONG_TERM_FRAME_INDICES; + dpb->maxRefFrames = MAX(maxRefFrames, 1); + if (noReordering) + dpb->dpbSize = dpb->maxRefFrames; + else + dpb->dpbSize = dpbSize; + dpb->maxFrameNum = maxFrameNum; + dpb->noReordering = noReordering; + dpb->fullness = 0; + dpb->numRefFrames = 0; + dpb->prevRefFrameNum = 0; + + dpb->maxNumBufferedPics = maxNumBufferedPics; + /* Add maxNumBufferedPics to have enough space in the DPB for buffered pictures. */ + dpb->dpbSize += dpb->maxNumBufferedPics; + + ALLOCATE(dpb->buffer, dpb->dpbSize + 1, dpbPicture_t); + if (dpb->buffer == NULL) + return(MEMORY_ALLOCATION_ERROR); + H264SwDecMemset(dpb->buffer, 0, + (dpb->dpbSize + 1)*sizeof(dpbPicture_t)); + for (i = 0; i < dpb->dpbSize + 1; i++) + { + /* Allocate needed amount of memory, which is: + * image size + 32 + 15, where 32 cames from the fact that in ARM OpenMax + * DL implementation Functions may read beyond the end of an array, + * by a maximum of 32 bytes. And +15 cames for the need to align memory + * to 16-byte boundary */ + ALLOCATE_PIXELS(dpb->buffer[i].pAllocatedData, (picSizeInMbs*384 + 32+15), u8); + if (dpb->buffer[i].pAllocatedData == NULL) + return(MEMORY_ALLOCATION_ERROR); + + dpb->buffer[i].data = ALIGN(dpb->buffer[i].pAllocatedData, 16); + } + + ALLOCATE(dpb->list, MAX_NUM_REF_IDX_L0_ACTIVE + 1, dpbPicture_t*); + ALLOCATE(dpb->outBuf, dpb->dpbSize+1, dpbOutPicture_t); + + if (dpb->list == NULL || dpb->outBuf == NULL) + return(MEMORY_ALLOCATION_ERROR); + + H264SwDecMemset(dpb->list, 0, + ((MAX_NUM_REF_IDX_L0_ACTIVE + 1) * sizeof(dpbPicture_t*)) ); + + dpb->numOut = dpb->outIndex = 0; + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdResetDpb + + Functional description: + Function to reset DPB. This function should be called when an IDR + slice (other than the first) activates new sequence parameter set. + Function calls h264bsdFreeDpb to free old allocated memories and + h264bsdInitDpb to re-initialize the DPB. Same inputs, outputs and + returns as for h264bsdInitDpb. + +------------------------------------------------------------------------------*/ + +u32 h264bsdResetDpb( + dpbStorage_t *dpb, + u32 picSizeInMbs, + u32 dpbSize, + u32 maxRefFrames, + u32 maxFrameNum, + u32 noReordering, + u32 maxNumBufferedPics) +{ + +/* Code */ + + ASSERT(picSizeInMbs); + ASSERT(maxRefFrames <= MAX_NUM_REF_PICS); + ASSERT(maxRefFrames <= dpbSize); + ASSERT(maxFrameNum); + ASSERT(dpbSize); + + h264bsdFreeDpb(dpb); + + return h264bsdInitDpb(dpb, picSizeInMbs, dpbSize, maxRefFrames, + maxFrameNum, noReordering, maxNumBufferedPics); +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdInitRefPicList + + Functional description: + Function to initialize reference picture list. Function just + sets pointers in the list according to pictures in the buffer. + The buffer is assumed to contain pictures sorted according to + what the H.264 standard says about initial reference picture list. + + Inputs: + dpb pointer to dpb data structure + + Outputs: + dpb 'list' field initialized + + Returns: + none + +------------------------------------------------------------------------------*/ + +void h264bsdInitRefPicList(dpbStorage_t *dpb) +{ + +/* Variables */ + + u32 i; + +/* Code */ + + for (i = 0; i < dpb->numRefFrames; i++) + dpb->list[i] = &dpb->buffer[i]; + +} + +/*------------------------------------------------------------------------------ + + Function: FindDpbPic + + Functional description: + Function to find a reference picture from the buffer. The picture + to be found is identified by picNum and isShortTerm flag. + + Returns: + index of the picture in the buffer + -1 if the specified picture was not found in the buffer + +------------------------------------------------------------------------------*/ + +static i32 FindDpbPic(dpbStorage_t *dpb, i32 picNum, u32 isShortTerm) +{ + +/* Variables */ + + u32 i = 0; + u32 found = HANTRO_FALSE; + +/* Code */ + + if (isShortTerm) + { + while (i < dpb->maxRefFrames && !found) + { + if (IS_SHORT_TERM(dpb->buffer[i]) && + dpb->buffer[i].picNum == picNum) + found = HANTRO_TRUE; + else + i++; + } + } + else + { + ASSERT(picNum >= 0); + while (i < dpb->maxRefFrames && !found) + { + if (IS_LONG_TERM(dpb->buffer[i]) && + dpb->buffer[i].picNum == picNum) + found = HANTRO_TRUE; + else + i++; + } + } + + if (found) + return((i32)i); + else + return(-1); + +} + +/*------------------------------------------------------------------------------ + + Function: SetPicNums + + Functional description: + Function to set picNum values for short-term pictures in the + buffer. Numbering of pictures is based on frame numbers and as + frame numbers are modulo maxFrameNum -> frame numbers of older + pictures in the buffer may be bigger than the currFrameNum. + picNums will be set so that current frame has the largest picNum + and all the short-term frames in the buffer will get smaller picNum + representing their "distance" from the current frame. This + function kind of maps the modulo arithmetic back to normal. + +------------------------------------------------------------------------------*/ + +static void SetPicNums(dpbStorage_t *dpb, u32 currFrameNum) +{ + +/* Variables */ + + u32 i; + i32 frameNumWrap; + +/* Code */ + + ASSERT(dpb); + ASSERT(currFrameNum < dpb->maxFrameNum); + + for (i = 0; i < dpb->numRefFrames; i++) + if (IS_SHORT_TERM(dpb->buffer[i])) + { + if (dpb->buffer[i].frameNum > currFrameNum) + frameNumWrap = + (i32)dpb->buffer[i].frameNum - (i32)dpb->maxFrameNum; + else + frameNumWrap = (i32)dpb->buffer[i].frameNum; + dpb->buffer[i].picNum = frameNumWrap; + } + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdCheckGapsInFrameNum + + Functional description: + Function to check gaps in frame_num and generate non-existing + (short term) reference pictures if necessary. This function should + be called only for non-IDR pictures. + + Inputs: + dpb pointer to dpb data structure + frameNum frame number of the current picture + isRefPic flag to indicate if current picture is a reference or + non-reference picture + gapsAllowed Flag which indicates active SPS stance on whether + to allow gaps + + Outputs: + dpb 'buffer' possibly modified by inserting non-existing + pictures with sliding window marking process + + Returns: + HANTRO_OK success + HANTRO_NOK error in sliding window reference picture marking or + frameNum equal to previous reference frame used for + a reference picture + +------------------------------------------------------------------------------*/ + +u32 h264bsdCheckGapsInFrameNum(dpbStorage_t *dpb, u32 frameNum, u32 isRefPic, + u32 gapsAllowed) +{ + +/* Variables */ + + u32 unUsedShortTermFrameNum; + u8 *tmp; + +/* Code */ + + ASSERT(dpb); + ASSERT(dpb->fullness <= dpb->dpbSize); + ASSERT(frameNum < dpb->maxFrameNum); + + if (dpb->maxNumBufferedPics == 0) { + dpb->numOut = 0; + dpb->outIndex = 0; + } + + if(!gapsAllowed) + return(HANTRO_OK); + + if ( (frameNum != dpb->prevRefFrameNum) && + (frameNum != ((dpb->prevRefFrameNum + 1) % dpb->maxFrameNum))) + { + + unUsedShortTermFrameNum = (dpb->prevRefFrameNum + 1) % dpb->maxFrameNum; + + /* store data pointer of last buffer position to be used as next + * "allocated" data pointer if last buffer position after this process + * contains data pointer located in outBuf (buffer placed in the output + * shall not be overwritten by the current picture) */ + tmp = dpb->buffer[dpb->dpbSize].data; + do + { + SetPicNums(dpb, unUsedShortTermFrameNum); + + if (SlidingWindowRefPicMarking(dpb) != HANTRO_OK) + { + return(HANTRO_NOK); + } + + /* output pictures if buffer full */ + while (dpb->fullness >= dpb->dpbSize) + { + ASSERT(!dpb->noReordering); + u32 ret = OutputPicture(dpb); + (void)ret; + ASSERT(ret == HANTRO_OK); + } + + /* add to end of list */ + ASSERT( !dpb->buffer[dpb->dpbSize].toBeDisplayed && + !IS_REFERENCE(dpb->buffer[dpb->dpbSize]) ); + dpb->buffer[dpb->dpbSize].status = NON_EXISTING; + dpb->buffer[dpb->dpbSize].frameNum = unUsedShortTermFrameNum; + dpb->buffer[dpb->dpbSize].picNum = (i32)unUsedShortTermFrameNum; + dpb->buffer[dpb->dpbSize].picOrderCnt = 0; + dpb->buffer[dpb->dpbSize].toBeDisplayed = HANTRO_FALSE; + dpb->fullness++; + dpb->numRefFrames++; + + /* sort the buffer */ + ShellSort(dpb->buffer, dpb->dpbSize+1); + + unUsedShortTermFrameNum = (unUsedShortTermFrameNum + 1) % + dpb->maxFrameNum; + + } while (unUsedShortTermFrameNum != frameNum); + + /* pictures placed in output buffer -> check that 'data' in + * buffer position dpbSize is not in the output buffer (this will be + * "allocated" by h264bsdAllocateDpbImage). If it is -> exchange data + * pointer with the one stored in the beginning */ + if (dpb->numOut) + { + u32 i; + + for (i = 0; i < dpb->numOut; i++) + { + if (dpb->outBuf[i].data == dpb->buffer[dpb->dpbSize].data) + { + /* find buffer position containing data pointer stored in + * tmp */ + for (i = 0; i < dpb->dpbSize; i++) + { + if (dpb->buffer[i].data == tmp) + { + dpb->buffer[i].data = + dpb->buffer[dpb->dpbSize].data; + dpb->buffer[dpb->dpbSize].data = tmp; + break; + } + } + ASSERT(i < dpb->dpbSize); + break; + } + } + } + } + /* frameNum for reference pictures shall not be the same as for previous + * reference picture, otherwise accesses to pictures in the buffer cannot + * be solved unambiguously */ + else if (isRefPic && frameNum == dpb->prevRefFrameNum) + { + return(HANTRO_NOK); + } + + /* save current frame_num in prevRefFrameNum. For non-reference frame + * prevFrameNum is set to frame number of last non-existing frame above */ + if (isRefPic) + dpb->prevRefFrameNum = frameNum; + else if (frameNum != dpb->prevRefFrameNum) + { + dpb->prevRefFrameNum = + (frameNum + dpb->maxFrameNum - 1) % dpb->maxFrameNum; + } + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: FindSmallestPicOrderCnt + + Functional description: + Function to find picture with smallest picture order count. This + will be the next picture in display order. + + Returns: + pointer to the picture, NULL if no pictures to be displayed + +------------------------------------------------------------------------------*/ + +dpbPicture_t* FindSmallestPicOrderCnt(dpbStorage_t *dpb) +{ + +/* Variables */ + + u32 i; + i32 picOrderCnt; + dpbPicture_t *tmp; + +/* Code */ + + ASSERT(dpb); + + picOrderCnt = 0x7FFFFFFF; + tmp = NULL; + + for (i = 0; i <= dpb->dpbSize; i++) + { + if (dpb->buffer[i].toBeDisplayed && + (dpb->buffer[i].picOrderCnt < picOrderCnt)) + { + tmp = dpb->buffer + i; + picOrderCnt = dpb->buffer[i].picOrderCnt; + } + } + + return(tmp); + +} + +/*------------------------------------------------------------------------------ + + Function: OutputPicture + + Functional description: + Function to put next display order picture into the output buffer. + + Returns: + HANTRO_OK success + HANTRO_NOK no pictures to display + +------------------------------------------------------------------------------*/ + +u32 OutputPicture(dpbStorage_t *dpb) +{ + +/* Variables */ + + dpbPicture_t *tmp; + +/* Code */ + + ASSERT(dpb); + + if (dpb->noReordering) + return(HANTRO_NOK); + + tmp = FindSmallestPicOrderCnt(dpb); + + /* no pictures to be displayed */ + if (tmp == NULL) + return(HANTRO_NOK); + + dpb->outBuf[dpb->numOut].data = tmp->data; + dpb->outBuf[dpb->numOut].isIdr = tmp->isIdr; + dpb->outBuf[dpb->numOut].picId = tmp->picId; + dpb->outBuf[dpb->numOut].numErrMbs = tmp->numErrMbs; + dpb->numOut++; + + /* Record that this picture is currently in the output buffer and cannot + be used for decoding */ + tmp->inOutputBuf = 1; + + tmp->toBeDisplayed = HANTRO_FALSE; + if (!IS_REFERENCE(*tmp)) + { + dpb->fullness--; + } + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdDpbNumOutputPictures + + Functional description: + Function to get the number of pictures that are available in the + output buffer. + + Return: + Number of pictures that are ready to be displayed. + +------------------------------------------------------------------------------*/ + +u32 h264bsdDpbNumOutputPictures(dpbStorage_t *dpb) +{ +/* Variables */ + +/* Code */ + + ASSERT(dpb); + + return dpb->numOut - dpb->outIndex; +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdDpbOutputPicture + + Functional description: + Function to get next display order picture from the output buffer. + + Return: + pointer to output picture structure, NULL if no pictures to + display + +------------------------------------------------------------------------------*/ + +dpbOutPicture_t* h264bsdDpbOutputPicture(dpbStorage_t *dpb) +{ + +/* Variables */ + +/* Code */ + + ASSERT(dpb); + + /* Recompact the output buffer, removing pictures that were already + extracted. We don't do this every time to avoid wasting time. */ + if (dpb->outIndex && (dpb->outIndex == dpb->numOut || + dpb->outIndex >= (dpb->maxNumBufferedPics ? dpb->maxNumBufferedPics : 1))) { + int left = dpb->numOut - dpb->outIndex; + for (int i = 0; i < left; i++) + dpb->outBuf[i] = dpb->outBuf[dpb->outIndex+i]; + dpb->outIndex = 0; + dpb->numOut = left; + } + + /* If there's at least one picture in the output buffer, extract it. + NOTE: the extracted picture remains "locked" (inOutputBuf=1) until the + client explicitly releases it via h264bsdDpbReleasePicture(). This is + required so that background decoding (eg: via video_poll()) cannot + overwrite the picture memory while the client is still using it. */ + if (dpb->outIndex < dpb->numOut) { + dpbOutPicture_t* out = dpb->outBuf + dpb->outIndex++; + + return(out); + } + else + return(NULL); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdDpbReleasePicture + + Functional description: + Release an output picture previously obtained via + h264bsdDpbOutputPicture() (and thus h264bsdNextOutputPicture()). + + When output buffering is enabled (maxNumBufferedPics > 0), pictures placed in the output buffer + are marked as "inOutputBuf" so that their underlying memory cannot + be reused for decoding. This function clears that flag, making the + picture memory available again for decoding. + + Inputs: + dpb pointer to dpb data structure + data pointer to the picture data previously returned by the API + +------------------------------------------------------------------------------*/ + +void h264bsdDpbReleasePicture(dpbStorage_t *dpb, const u8 *data) +{ + ASSERT(dpb); + ASSERT(data); + + int found = 0; + for (int i = 0; i < (int)dpb->dpbSize + 1; i++) { + if (dpb->buffer[i].data == data) { + ASSERT(dpb->buffer[i].inOutputBuf == 1); + dpb->buffer[i].inOutputBuf = 0; + found = 1; + break; + } + } + (void)found; + ASSERT(found); +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdFlushDpb + + Functional description: + Function to flush the DPB. Function puts all pictures needed for + display into the output buffer. This function shall be called in + the end of the stream to obtain pictures buffered for display + re-ordering purposes. + +------------------------------------------------------------------------------*/ + +void h264bsdFlushDpb(dpbStorage_t *dpb) +{ + + /* don't do anything if buffer not reserved */ + if (dpb->buffer) + { + dpb->flushed = 1; + /* output all pictures */ + while (OutputPicture(dpb) == HANTRO_OK) + ; + } + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdClearDpb + + Functional description: + Function to reset DPB logical state without freeing allocated + buffers. All picture slots are marked unused and counters are + reset. Pixel data allocations and DPB configuration (dpbSize, + maxRefFrames, etc.) are preserved. Intended for use during + video rewind/loop. + +------------------------------------------------------------------------------*/ + +void h264bsdClearDpb(dpbStorage_t *dpb) +{ + +/* Variables */ + + u32 i; + +/* Code */ + + ASSERT(dpb); + + if (dpb->buffer) + { + for (i = 0; i < dpb->dpbSize + 1; i++) + { + /* keep data and pAllocatedData pointers intact */ + dpb->buffer[i].picNum = 0; + dpb->buffer[i].frameNum = 0; + dpb->buffer[i].picOrderCnt = 0; + dpb->buffer[i].status = UNUSED; + dpb->buffer[i].toBeDisplayed = 0; + dpb->buffer[i].inOutputBuf = 0; + dpb->buffer[i].picId = 0; + dpb->buffer[i].numErrMbs = 0; + dpb->buffer[i].isIdr = 0; + } + } + + dpb->currentOut = NULL; + dpb->numOut = 0; + dpb->outIndex = 0; + dpb->numRefFrames = 0; + dpb->fullness = 0; + dpb->prevRefFrameNum = 0; + dpb->lastContainsMmco5 = 0; + dpb->flushed = 0; + dpb->maxLongTermFrameIdx = NO_LONG_TERM_FRAME_INDICES; + + if (dpb->list) + { + H264SwDecMemset(dpb->list, 0, + (MAX_NUM_REF_IDX_L0_ACTIVE + 1) * sizeof(dpbPicture_t*)); + } +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdFreeDpb + + Functional description: + Function to free memories reserved for the DPB. + +------------------------------------------------------------------------------*/ + +void h264bsdFreeDpb(dpbStorage_t *dpb) +{ + +/* Variables */ + + u32 i; + +/* Code */ + + ASSERT(dpb); + + if (dpb->buffer) + { + for (i = 0; i < dpb->dpbSize+1; i++) + { + FREE_PIXELS(dpb->buffer[i].pAllocatedData); + } + } + FREE(dpb->buffer); + FREE(dpb->list); + FREE(dpb->outBuf); + +} + +/*------------------------------------------------------------------------------ + + Function: ShellSort + + Functional description: + Sort pictures in the buffer. Function implements Shell's method, + i.e. diminishing increment sort. See e.g. "Numerical Recipes in C" + for more information. + +------------------------------------------------------------------------------*/ + +static void ShellSort(dpbPicture_t *pPic, u32 num) +{ + + u32 i, j; + u32 step; + dpbPicture_t tmpPic; + + step = 7; + + while (step) + { + for (i = step; i < num; i++) + { + tmpPic = pPic[i]; + j = i; + while (j >= step && ComparePictures(pPic + j - step, &tmpPic) > 0) + { + pPic[j] = pPic[j-step]; + j -= step; + } + pPic[j] = tmpPic; + } + step >>= 1; + } + +} + diff --git a/src/video/h264_decoder/h264bsd_dpb.h b/src/video/h264_decoder/h264bsd_dpb.h new file mode 100644 index 000000000..63a9bb939 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_dpb.h @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_DPB_H +#define H264SWDEC_DPB_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_slice_header.h" +#include "h264bsd_image.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/* enumeration to represent status of buffered image */ +typedef enum { + UNUSED = 0, + NON_EXISTING, + SHORT_TERM, + LONG_TERM +} dpbPictureStatus_e; + +/* structure to represent a buffered picture */ +typedef struct { + u8 *data; /* 16-byte aligned pointer of pAllocatedData */ + u8 *pAllocatedData; /* allocated picture pointer; (size + 15) bytes */ + i32 picNum; + u32 frameNum; + i32 picOrderCnt; + dpbPictureStatus_e status; + u32 toBeDisplayed; + u32 inOutputBuf; + u32 picId; + u32 numErrMbs; + u32 isIdr; +} dpbPicture_t; + +/* structure to represent display image output from the buffer */ +typedef struct { + u8 *data; + u32 picId; + u32 numErrMbs; + u32 isIdr; +} dpbOutPicture_t; + +/* structure to represent DPB */ +typedef struct { + dpbPicture_t *buffer; /* array of pictures (number of elements = dpbSize+1) */ + dpbPicture_t **list; + dpbPicture_t *currentOut; + dpbOutPicture_t *outBuf; /* array of output pictures (dpbSize+1) */ + u32 numOut; + u32 outIndex; + u32 maxRefFrames; + u32 dpbSize; + u32 maxNumBufferedPics; /* rasky: max number of buffered pictures */ + u32 maxFrameNum; + u32 maxLongTermFrameIdx; + u32 numRefFrames; + u32 fullness; + u32 prevRefFrameNum; + u32 lastContainsMmco5; + u32 noReordering; + u32 flushed; +} dpbStorage_t; + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +u32 h264bsdInitDpb( + dpbStorage_t *dpb, + u32 picSizeInMbs, + u32 dpbSize, + u32 numRefFrames, + u32 maxFrameNum, + u32 noReordering, + u32 maxNumBufferedPics); + +u32 h264bsdResetDpb( + dpbStorage_t *dpb, + u32 picSizeInMbs, + u32 dpbSize, + u32 numRefFrames, + u32 maxFrameNum, + u32 noReordering, + u32 maxNumBufferedPics); + +void h264bsdInitRefPicList(dpbStorage_t *dpb); + +u8* h264bsdAllocateDpbImage(dpbStorage_t *dpb); + +u8* h264bsdGetRefPicData(dpbStorage_t *dpb, u32 index); + +u32 h264bsdReorderRefPicList( + dpbStorage_t *dpb, + refPicListReordering_t *order, + u32 currFrameNum, + u32 numRefIdxActive); + +u32 h264bsdMarkDecRefPic( + dpbStorage_t *dpb, + decRefPicMarking_t *mark, + image_t *image, + u32 frameNum, + i32 picOrderCnt, + u32 isIdr, + u32 picId, + u32 numErrMbs); + +u32 h264bsdCheckGapsInFrameNum(dpbStorage_t *dpb, u32 frameNum, u32 isRefPic, + u32 gapsAllowed); + +dpbOutPicture_t* h264bsdDpbOutputPicture(dpbStorage_t *dpb); + +u32 h264bsdDpbNumOutputPictures(dpbStorage_t *dpb); + +void h264bsdDpbReleasePicture(dpbStorage_t *dpb, const u8 *data); + +void h264bsdFlushDpb(dpbStorage_t *dpb); + +void h264bsdClearDpb(dpbStorage_t *dpb); + +void h264bsdFreeDpb(dpbStorage_t *dpb); + +#endif /* #ifdef H264SWDEC_DPB_H */ + diff --git a/src/video/h264_decoder/h264bsd_image.c b/src/video/h264_decoder/h264bsd_image.c new file mode 100644 index 000000000..733a7ace9 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_image.c @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdWriteMacroblock + h264bsdWriteOutputBlocks + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_image.h" +#include "h264bsd_util.h" +#include "h264bsd_neighbour.h" + +#ifdef H264BSD_N64 +#include "../rsph264_internal.h" +#endif + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/* x- and y-coordinates for each block, defined in h264bsd_intra_prediction.c */ +extern const u32 h264bsdBlockX[]; +extern const u32 h264bsdBlockY[]; + +/* clipping table, defined in h264bsd_intra_prediction.c */ +extern const u8 h264bsdClip[]; + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + + + +/*------------------------------------------------------------------------------ + + Function: h264bsdWriteMacroblock + + Functional description: + Write one macroblock into the image. Both luma and chroma + components will be written at the same time. + + Inputs: + data pointer to macroblock data to be written, 256 values for + luma followed by 64 values for both chroma components + + Outputs: + image pointer to the image where the macroblock will be written + + Returns: + none + +------------------------------------------------------------------------------*/ +#ifndef H264DEC_NEON +void h264bsdWriteMacroblock(image_t *image, u8 *data, int flush) +{ +#ifdef H264BSD_N64 + int flags = flush ? 0 : RSPH264_CACHE_SKIP_ALL; + rsph264_queue_write_macroblock(flags, + data, image->luma, image->cb, image->cr, image->width); + // rsph264_sync(); + return; +#else // N64 +/* Variables */ + + u32 i; + u32 width; + u32 *lum, *cb, *cr; + u32 *ptr; + u32 tmp1, tmp2; + +/* Code */ + + ASSERT(image); + ASSERT(data); + ASSERT(!((u32)data&0x3)); + + width = image->width; + + /*lint -save -e826 lum, cb and cr used to copy 4 bytes at the time, disable + * "area too small" info message */ + lum = (u32*)image->luma; + cb = (u32*)image->cb; + cr = (u32*)image->cr; + ASSERT(!((u32)lum&0x3)); + ASSERT(!((u32)cb&0x3)); + ASSERT(!((u32)cr&0x3)); + + ptr = (u32*)data; + + width *= 4; + for (i = 16; i ; i--) + { + tmp1 = *ptr++; + tmp2 = *ptr++; + *lum++ = tmp1; + *lum++ = tmp2; + tmp1 = *ptr++; + tmp2 = *ptr++; + *lum++ = tmp1; + *lum++ = tmp2; + lum += width-4; + } + + width >>= 1; + for (i = 8; i ; i--) + { + tmp1 = *ptr++; + tmp2 = *ptr++; + *cb++ = tmp1; + *cb++ = tmp2; + cb += width-2; + } + + for (i = 8; i ; i--) + { + tmp1 = *ptr++; + tmp2 = *ptr++; + *cr++ = tmp1; + *cr++ = tmp2; + cr += width-2; + } +#endif // N64 +} +#endif + + diff --git a/src/video/h264_decoder/h264bsd_image.h b/src/video/h264_decoder/h264bsd_image.h new file mode 100644 index 000000000..6307321a7 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_image.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_IMAGE_H +#define H264SWDEC_IMAGE_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +typedef struct +{ + u8 *data; + u32 width; + u32 height; + /* current MB's components */ + u8 *luma; + u8 *cb; + u8 *cr; +} image_t; + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +void h264bsdWriteMacroblock(image_t *image, u8 *data, int flush); + +#endif /* #ifdef H264SWDEC_IMAGE_H */ + diff --git a/src/video/h264_decoder/h264bsd_inter_prediction.c b/src/video/h264_decoder/h264bsd_inter_prediction.c new file mode 100644 index 000000000..1490264a2 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_inter_prediction.c @@ -0,0 +1,1148 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdInterPrediction + MvPrediction16x16 + MvPrediction16x8 + MvPrediction8x16 + MvPrediction8x8 + MvPrediction + MedianFilter + GetInterNeighbour + GetPredictionMv + +------------------------------------------------------------------------------*/ + +#include "opttarget.h" + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_inter_prediction.h" +#include "h264bsd_neighbour.h" +#include "h264bsd_util.h" +#include "h264bsd_reconstruct.h" +#include "h264bsd_dpb.h" + +#ifdef H264BSD_N64 +#include "../fastcache.h" + +// We can substitute the whole h264bsdPredictSamples with our +// RSP-based implementation of inter-prediction with overfill. +#define h264bsdPredictSamples(currImage,mv,refPic,colAndRow,part,pFill) \ + n64PredictSamples(currImage,mv,refPic,colAndRow,part) + +static inline void n64PredictSamples(image_t *img, mv_t *mv, image_t *refPic, u32 colAndRow, u32 part); +static inline void n64SetWeights(const sliceHeader_t *pSliceHeader, u32 refIdx); + +#endif + + + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +typedef struct +{ + u32 available; + u32 refIndex; + mv_t mv; +} interNeighbour_t; + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +static u32 MvPrediction16x16(mbStorage_t *pMb, mbPred_t *mbPred, + dpbStorage_t *dpb); +static u32 MvPrediction16x8(mbStorage_t *pMb, mbPred_t *mbPred, + dpbStorage_t *dpb); +static u32 MvPrediction8x16(mbStorage_t *pMb, mbPred_t *mbPred, + dpbStorage_t *dpb); +static u32 MvPrediction8x8(mbStorage_t *pMb, subMbPred_t *subMbPred, + dpbStorage_t *dpb); +static u32 MvPrediction(mbStorage_t *pMb, subMbPred_t *subMbPred, + u32 mbPartIdx, u32 subMbPartIdx); +static i32 MedianFilter(i32 a, i32 b, i32 c); + +static void GetInterNeighbour(u32 sliceId, mbStorage_t *nMb, + interNeighbour_t *n, u32 index); +static void GetPredictionMv(mv_t *mv, interNeighbour_t *a, u32 refIndex); + +#if !H264BSD_N64 + +static void ApplyWeightPart(image_t *currImage, + const sliceHeader_t *pSliceHeader, u32 part, u32 refIdx) +{ + u32 x, y; + u32 partX, partY, partWidth, partHeight; + u32 lumaPitch, chromaPitch; + u32 lumaDenom, chromaDenom; + i32 lumaRound, chromaRound; + i32 lumaWeight, lumaOffset; + i32 chromaWeightCb, chromaWeightCr; + i32 chromaOffsetCb, chromaOffsetCr; + u8 *pLuma, *pCb, *pCr; + + if (pSliceHeader == NULL || !pSliceHeader->weightedPredFlag) + return; + if (refIdx >= MAX_NUM_REF_PICS) + return; + + partX = (part & 0xFF000000) >> 24; + partY = (part & 0x00FF0000) >> 16; + partWidth = (part & 0x0000FF00) >> 8; + partHeight = (part & 0x000000FF); + + lumaDenom = pSliceHeader->predWeightTable.lumaLog2WeightDenom; + chromaDenom = pSliceHeader->predWeightTable.chromaLog2WeightDenom; + lumaRound = lumaDenom ? (1 << (lumaDenom - 1)) : 0; + chromaRound = chromaDenom ? (1 << (chromaDenom - 1)) : 0; + + lumaWeight = pSliceHeader->predWeightTable.lumaWeightL0[refIdx]; + lumaOffset = pSliceHeader->predWeightTable.lumaOffsetL0[refIdx]; + chromaWeightCb = pSliceHeader->predWeightTable.chromaWeightL0[refIdx][0]; + chromaWeightCr = pSliceHeader->predWeightTable.chromaWeightL0[refIdx][1]; + chromaOffsetCb = pSliceHeader->predWeightTable.chromaOffsetL0[refIdx][0]; + chromaOffsetCr = pSliceHeader->predWeightTable.chromaOffsetL0[refIdx][1]; + + lumaPitch = currImage->width * 16; + pLuma = currImage->luma + lumaPitch * partY + partX; + if (lumaWeight != (1 << lumaDenom) || lumaOffset != 0) + { + for (y = 0; y < partHeight; y++) + { + u8 *row = pLuma + y * lumaPitch; + for (x = 0; x < partWidth; x++) + { + i32 pred = row[x]; + i32 weighted = ((lumaWeight * pred + lumaRound) >> lumaDenom) + + lumaOffset; + row[x] = CLIP1(weighted); + } + } + } + + chromaPitch = currImage->width * 8; + pCb = currImage->cb + (partY >> 1) * chromaPitch + (partX >> 1); + pCr = currImage->cr + (partY >> 1) * chromaPitch + (partX >> 1); + if (chromaWeightCb != (1 << chromaDenom) || chromaOffsetCb != 0 || + chromaWeightCr != (1 << chromaDenom) || chromaOffsetCr != 0) + { + for (y = 0; y < (partHeight >> 1); y++) + { + u8 *rowCb = pCb + y * chromaPitch; + u8 *rowCr = pCr + y * chromaPitch; + for (x = 0; x < (partWidth >> 1); x++) + { + i32 predCb = rowCb[x]; + i32 predCr = rowCr[x]; + i32 weightedCb = ((chromaWeightCb * predCb + chromaRound) >> + chromaDenom) + chromaOffsetCb; + i32 weightedCr = ((chromaWeightCr * predCr + chromaRound) >> + chromaDenom) + chromaOffsetCr; + rowCb[x] = CLIP1(weightedCb); + rowCr[x] = CLIP1(weightedCr); + } + } + } +} +#endif + +static inline void PredictPart(image_t *currImage, mv_t *pMv, image_t *refImage, + u32 colAndRow, u32 part, u8 *pFill, u32 refIdx, + const sliceHeader_t *pSliceHeader) +{ +#if H264BSD_N64 + /* the RSP applies the weights as part of the prediction task, so the + * coefficients must be configured before queueing it */ + n64SetWeights(pSliceHeader, refIdx); +#endif + h264bsdPredictSamples(currImage, pMv, refImage, colAndRow, part, pFill); +#if !H264BSD_N64 + ApplyWeightPart(currImage, pSliceHeader, part, refIdx); +#endif +} + +static const neighbour_t N_A_SUB_PART[4][4][4] = { + { { {MB_A,5}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_A,5}, {MB_A,7}, {MB_NA,0}, {MB_NA,0} }, + { {MB_A,5}, {MB_CURR,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_A,5}, {MB_CURR,0}, {MB_A,7}, {MB_CURR,2} } }, + + { { {MB_CURR,1}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,1}, {MB_CURR,3}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,1}, {MB_CURR,4}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,1}, {MB_CURR,4}, {MB_CURR,3}, {MB_CURR,6} } }, + + { { {MB_A,13}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_A,13}, {MB_A,15}, {MB_NA,0}, {MB_NA,0} }, + { {MB_A,13}, {MB_CURR,8}, {MB_NA,0}, {MB_NA,0} }, + { {MB_A,13}, {MB_CURR,8}, {MB_A,15}, {MB_CURR,10} } }, + + { { {MB_CURR,9}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,9}, {MB_CURR,11}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,9}, {MB_CURR,12}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,9}, {MB_CURR,12}, {MB_CURR,11}, {MB_CURR,14} } } }; + +static const neighbour_t N_B_SUB_PART[4][4][4] = { + { { {MB_B,10}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_B,10}, {MB_CURR,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_B,10}, {MB_B,11}, {MB_NA,0}, {MB_NA,0} }, + { {MB_B,10}, {MB_B,11}, {MB_CURR,0}, {MB_CURR,1} } }, + + { { {MB_B,14}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_B,14}, {MB_CURR,4}, {MB_NA,0}, {MB_NA,0} }, + { {MB_B,14}, {MB_B,15}, {MB_NA,0}, {MB_NA,0} }, + { {MB_B,14}, {MB_B,15}, {MB_CURR,4}, {MB_CURR,5} } }, + + { { {MB_CURR,2}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,2}, {MB_CURR,8}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,2}, {MB_CURR,3}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,2}, {MB_CURR,3}, {MB_CURR,8}, {MB_CURR,9} } }, + + { { {MB_CURR,6}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,6}, {MB_CURR,12}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,6}, {MB_CURR,7}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,6}, {MB_CURR,7}, {MB_CURR,12}, {MB_CURR,13} } } }; + +static const neighbour_t N_C_SUB_PART[4][4][4] = { + { { {MB_B,14}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_B,14}, {MB_NA,4}, {MB_NA,0}, {MB_NA,0} }, + { {MB_B,11}, {MB_B,14}, {MB_NA,0}, {MB_NA,0} }, + { {MB_B,11}, {MB_B,14}, {MB_CURR,1}, {MB_NA,4} } }, + + { { {MB_C,10}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_C,10}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_B,15}, {MB_C,10}, {MB_NA,0}, {MB_NA,0} }, + { {MB_B,15}, {MB_C,10}, {MB_CURR,5}, {MB_NA,0} } }, + + { { {MB_CURR,6}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,6}, {MB_NA,12}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,3}, {MB_CURR,6}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,3}, {MB_CURR,6}, {MB_CURR,9}, {MB_NA,12} } }, + + { { {MB_NA,2}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_NA,2}, {MB_NA,8}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,7}, {MB_NA,2}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,7}, {MB_NA,2}, {MB_CURR,13}, {MB_NA,8} } } }; + +static const neighbour_t N_D_SUB_PART[4][4][4] = { + { { {MB_D,15}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_D,15}, {MB_A,5}, {MB_NA,0}, {MB_NA,0} }, + { {MB_D,15}, {MB_B,10}, {MB_NA,0}, {MB_NA,0} }, + { {MB_D,15}, {MB_B,10}, {MB_A,5}, {MB_CURR,0} } }, + + { { {MB_B,11}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_B,11}, {MB_CURR,1}, {MB_NA,0}, {MB_NA,0} }, + { {MB_B,11}, {MB_B,14}, {MB_NA,0}, {MB_NA,0} }, + { {MB_B,11}, {MB_B,14}, {MB_CURR,1}, {MB_CURR,4} } }, + + { { {MB_A,7}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_A,7}, {MB_A,13}, {MB_NA,0}, {MB_NA,0} }, + { {MB_A,7}, {MB_CURR,2}, {MB_NA,0}, {MB_NA,0} }, + { {MB_A,7}, {MB_CURR,2}, {MB_A,13}, {MB_CURR,8} } }, + + { { {MB_CURR,3}, {MB_NA,0}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,3}, {MB_CURR,9}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,3}, {MB_CURR,6}, {MB_NA,0}, {MB_NA,0} }, + { {MB_CURR,3}, {MB_CURR,6}, {MB_CURR,9}, {MB_CURR,12} } } }; + + + +#ifdef H264BSD_N64 +#include "../rsph264_internal.h" + +/* Packing the coefficients of one reference index reads six arrays scattered + * over the ~400 bytes of predWeightTable_t, which costs a handful of cache + * misses. The result only depends on the slice, so it is packed once per slice + * into packedWeights, where one entry is 12 bytes: the reference index used by + * most partitions then costs a single miss. Zero entries means no weighting, + * either because the slice does not use it or because the index is out of the + * range coded in the slice header. */ +void h264bsdPrepareWeights(sliceHeader_t *pSliceHeader) { + + const predWeightTable_t *pWeights; + u32 i, num, lumaDenom, chromaDenom; + + if (pSliceHeader == NULL) + return; + + pSliceHeader->numPackedWeights = 0; + if (!pSliceHeader->weightedPredFlag) + return; + + pWeights = &pSliceHeader->predWeightTable; + lumaDenom = pWeights->lumaLog2WeightDenom; + chromaDenom = pWeights->chromaLog2WeightDenom; + + num = pSliceHeader->numRefIdxL0Active; + if (num > MAX_NUM_REF_PICS) num = MAX_NUM_REF_PICS; + + for (i = 0; i < num; i++) { + pSliceHeader->packedWeights[i][0] = rsph264_weight_pack(pWeights->lumaWeightL0[i], + pWeights->lumaOffsetL0[i], lumaDenom); + pSliceHeader->packedWeights[i][1] = rsph264_weight_pack(pWeights->chromaWeightL0[i][0], + pWeights->chromaOffsetL0[i][0], chromaDenom); + pSliceHeader->packedWeights[i][2] = rsph264_weight_pack(pWeights->chromaWeightL0[i][1], + pWeights->chromaOffsetL0[i][1], chromaDenom); + } + pSliceHeader->numPackedWeights = num; +} + +static inline void n64SetWeights( + const sliceHeader_t *pSliceHeader, + u32 refIdx) { + + const u32 *w; + + if (refIdx >= pSliceHeader->numPackedWeights) { + rsph264_queue_set_weights_if_changed(RSPH264_WEIGHT_IDENTITY, + RSPH264_WEIGHT_IDENTITY, RSPH264_WEIGHT_IDENTITY); + return; + } + + w = pSliceHeader->packedWeights[refIdx]; + rsph264_queue_set_weights_if_changed(w[0], w[1], w[2]); +} + +static inline void n64PredictSamples( + image_t *img, + mv_t *mv, + image_t *refPic, + u32 colAndRow, + u32 part) { + + uint32_t partX = (part & 0xFF000000) >> 24; + uint32_t partY = (part & 0x00FF0000) >> 16; + uint32_t partWidth = (part & 0x0000FF00) >> 8; + uint32_t partHeight = (part & 0x000000FF); + + uint32_t partPos = (partX<<16) | (partY); + + uint32_t lumaPitch = img->width*16; + uint32_t chromaPitch = img->width*8; + + rsph264_queue_interpolate_all_overfill(RSPH264_CACHE_SKIP_ALL, + refPic->data, refPic->width*16, + img->luma + lumaPitch*partY + partX, + img->cb + chromaPitch*partY/2 + partX/2, + img->cr + chromaPitch*partY/2 + partX/2, + img->width*16, + (refPic->width*16) << 16 | (refPic->height*16), + (partWidth << 16) | partHeight, + *(uint32_t*)mv, colAndRow + partPos + ); +} + +#endif + +/*------------------------------------------------------------------------------ + + Function: h264bsdInterPrediction + + Functional description: + Processes one inter macroblock. Performs motion vector prediction + and reconstructs prediction macroblock. Writes the final macroblock + (prediction + residual) into the output image (currImage) + + Inputs: + pMb pointer to macroblock specific information + pMbLayer pointer to current macroblock data from stream + dpb pointer to decoded picture buffer + mbNum current macroblock number + currImage pointer to output image + data pointer where predicted macroblock will be stored + + Outputs: + pMb structure is updated with current macroblock + currImage current macroblock is written into image + data prediction is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK error in motion vector prediction + +------------------------------------------------------------------------------*/ +u32 h264bsdInterPrediction(mbStorage_t *pMb, macroblockLayer_t *pMbLayer, + dpbStorage_t *dpb, u32 mbNum, image_t *currImage, + const sliceHeader_t *pSliceHeader) +{ + PROFILE_START(PS_H264_INTERPRED); + +/* Variables */ + + u32 i; + u32 x, y; + u32 colAndRow; + subMbPartMode_e subPartMode; + image_t refImage; + #ifdef H264BSD_N64 + #if 0 + static u8 fillBuff[2][16384]; + static int fillBufIdx = 0; + #endif + #else + u8 fillBuff[32*21 + 15 + 32]; + #endif + u8 *pFill = NULL; + u32 tmp; +/* Code */ + + ASSERT(pMb); + ASSERT(h264bsdMbPartPredMode(pMb->mbType) == PRED_MODE_INTER); + ASSERT(pMbLayer); + + /* 16-byte alignment */ + #ifdef H264BSD_N64 + #if 0 + // Alternate two buffers, in case RSP is still processing + // the previous macroblock. + pFill = ALIGN(fillBuff[fillBufIdx], 16); + fillBufIdx = 1-fillBufIdx; + #endif + #else + pFill = ALIGN(fillBuff, 16); + #endif + + /* set row bits 15:0 */ + colAndRow = mbNum / currImage->width; + /*set col to bits 31:16 */ + colAndRow += (mbNum - colAndRow * currImage->width) << 16; + colAndRow <<= 4; + + refImage.width = currImage->width; + refImage.height = currImage->height; + + switch (pMb->mbType) + { + case P_Skip: + case P_L0_16x16: + if (MvPrediction16x16(pMb, &pMbLayer->mbPred, dpb) != HANTRO_OK) { + PROFILE_STOP(PS_H264_INTERPRED); + return(HANTRO_NOK); + } + refImage.data = pMb->refAddr[0]; + tmp = (0<<24) + (0<<16) + (16<<8) + 16; + PredictPart(currImage, pMb->mv, &refImage, + colAndRow, tmp, pFill, pMb->refPic[0], pSliceHeader); + #ifdef H264BSD_N64 + pFill += 800; + #endif + break; + + case P_L0_L0_16x8: + if ( MvPrediction16x8(pMb, &pMbLayer->mbPred, dpb) != HANTRO_OK){ + PROFILE_STOP(PS_H264_INTERPRED); + return(HANTRO_NOK); + } + refImage.data = pMb->refAddr[0]; + tmp = (0<<24) + (0<<16) + (16<<8) + 8; + PredictPart(currImage, pMb->mv, &refImage, + colAndRow, tmp, pFill, pMb->refPic[0], pSliceHeader); + #ifdef H264BSD_N64 + pFill += 800; + #endif + + refImage.data = pMb->refAddr[2]; + tmp = (0<<24) + (8<<16) + (16<<8) + 8; + PredictPart(currImage, pMb->mv+8, &refImage, + colAndRow, tmp, pFill, pMb->refPic[2], pSliceHeader); + #ifdef H264BSD_N64 + pFill += 800; + #endif + break; + + case P_L0_L0_8x16: + if ( MvPrediction8x16(pMb, &pMbLayer->mbPred, dpb) != HANTRO_OK) { + PROFILE_STOP(PS_H264_INTERPRED); + return(HANTRO_NOK); + } + refImage.data = pMb->refAddr[0]; + tmp = (0<<24) + (0<<16) + (8<<8) + 16; + PredictPart(currImage, pMb->mv, &refImage, + colAndRow, tmp, pFill, pMb->refPic[0], pSliceHeader); + #ifdef H264BSD_N64 + pFill += 800; + #endif + refImage.data = pMb->refAddr[1]; + tmp = (8<<24) + (0<<16) + (8<<8) + 16; + PredictPart(currImage, pMb->mv+4, &refImage, + colAndRow, tmp, pFill, pMb->refPic[1], pSliceHeader); + #ifdef H264BSD_N64 + pFill += 800; + #endif + break; + + default: /* P_8x8 and P_8x8ref0 */ + if ( MvPrediction8x8(pMb, &pMbLayer->subMbPred, dpb) != HANTRO_OK) { + PROFILE_STOP(PS_H264_INTERPRED); + return(HANTRO_NOK); + } + for (i = 0; i < 4; i++) + { + refImage.data = pMb->refAddr[i]; + subPartMode = + h264bsdSubMbPartMode(pMbLayer->subMbPred.subMbType[i]); + x = i & 0x1 ? 8 : 0; + y = i < 2 ? 0 : 8; + switch (subPartMode) + { + case MB_SP_8x8: + tmp = (x<<24) + (y<<16) + (8<<8) + 8; + PredictPart(currImage, pMb->mv+4*i, &refImage, + colAndRow, tmp, pFill, pMb->refPic[i], + pSliceHeader); + #ifdef H264BSD_N64 + pFill += 800; + #endif + break; + + case MB_SP_8x4: + tmp = (x<<24) + (y<<16) + (8<<8) + 4; + PredictPart(currImage, pMb->mv+4*i, &refImage, + colAndRow, tmp, pFill, pMb->refPic[i], + pSliceHeader); + #ifdef H264BSD_N64 + pFill += 800; + #endif + tmp = (x<<24) + ((y+4)<<16) + (8<<8) + 4; + PredictPart(currImage, pMb->mv+4*i+2, &refImage, + colAndRow, tmp, pFill, pMb->refPic[i], + pSliceHeader); + #ifdef H264BSD_N64 + pFill += 800; + #endif + break; + + case MB_SP_4x8: + tmp = (x<<24) + (y<<16) + (4<<8) + 8; + PredictPart(currImage, pMb->mv+4*i, &refImage, + colAndRow, tmp, pFill, pMb->refPic[i], + pSliceHeader); + #ifdef H264BSD_N64 + pFill += 800; + #endif + tmp = ((x+4)<<24) + (y<<16) + (4<<8) + 8; + PredictPart(currImage, pMb->mv+4*i+1, &refImage, + colAndRow, tmp, pFill, pMb->refPic[i], + pSliceHeader); + #ifdef H264BSD_N64 + pFill += 800; + #endif + break; + + default: + tmp = (x<<24) + (y<<16) + (4<<8) + 4; + PredictPart(currImage, pMb->mv+4*i, &refImage, + colAndRow, tmp, pFill, pMb->refPic[i], + pSliceHeader); + #ifdef H264BSD_N64 + pFill += 800; + #endif + tmp = ((x+4)<<24) + (y<<16) + (4<<8) + 4; + PredictPart(currImage, pMb->mv+4*i+1, &refImage, + colAndRow, tmp, pFill, pMb->refPic[i], + pSliceHeader); + #ifdef H264BSD_N64 + pFill += 800; + #endif + tmp = (x<<24) + ((y+4)<<16) + (4<<8) + 4; + PredictPart(currImage, pMb->mv+4*i+2, &refImage, + colAndRow, tmp, pFill, pMb->refPic[i], + pSliceHeader); + #ifdef H264BSD_N64 + pFill += 800; + #endif + tmp = ((x+4)<<24) + ((y+4)<<16) + (4<<8) + 4; + PredictPart(currImage, pMb->mv+4*i+3, &refImage, + colAndRow, tmp, pFill, pMb->refPic[i], + pSliceHeader); + #ifdef H264BSD_N64 + pFill += 800; + #endif + break; + } + } + break; + } + PROFILE_STOP(PS_H264_INTERPRED); + + /* if decoded flag > 1 -> mb has already been successfully decoded and + * written to output -> do not write again */ +#ifndef OPTIMIZE_NO_DECODED_FLAG + if (pMb->decoded > 1) + return HANTRO_OK; +#endif + + return(HANTRO_OK); +} + + +/*------------------------------------------------------------------------------ + + Function: MvPrediction16x16 + + Functional description: + Motion vector prediction for 16x16 partition mode + +------------------------------------------------------------------------------*/ + +u32 MvPrediction16x16(mbStorage_t *pMb, mbPred_t *mbPred, dpbStorage_t *dpb) +{ + +/* Variables */ + + mv_t mv; + mv_t mvPred; + interNeighbour_t a[3]; /* A, B, C */ + u32 refIndex; + u8 *tmp; + +/* Code */ + + refIndex = mbPred->refIdxL0[0]; + + GetInterNeighbour(pMb->sliceId, pMb->mbA, a, 5); + GetInterNeighbour(pMb->sliceId, pMb->mbB, a+1, 10); + if (pMb->mbType == P_Skip && + (!a[0].available || !a[1].available || + ( a[0].refIndex == 0 && a[0].mv.hor == 0 && a[0].mv.ver == 0 ) || + ( a[1].refIndex == 0 && a[1].mv.hor == 0 && a[1].mv.ver == 0 ))) + { + mv.hor = mv.ver = 0; + } + else + { + mv = mbPred->mvdL0[0]; + GetInterNeighbour(pMb->sliceId, pMb->mbC, a+2, 10); + if (!a[2].available) + { + GetInterNeighbour(pMb->sliceId, pMb->mbD, a+2, 15); + } + + GetPredictionMv(&mvPred, a, refIndex); + + mv.hor += mvPred.hor; + mv.ver += mvPred.ver; + + /* horizontal motion vector range [-2048, 2047.75] */ + if ((u32)(i32)(mv.hor+8192) >= (16384)) + return(HANTRO_NOK); + + /* vertical motion vector range [-512, 511.75] + * (smaller for low levels) */ + if ((u32)(i32)(mv.ver+2048) >= (4096)) + return(HANTRO_NOK); + } + + tmp = h264bsdGetRefPicData(dpb, refIndex); + if (tmp == NULL) + return(HANTRO_NOK); + + pMb->mv[0] = pMb->mv[1] = pMb->mv[2] = pMb->mv[3] = + pMb->mv[4] = pMb->mv[5] = pMb->mv[6] = pMb->mv[7] = + pMb->mv[8] = pMb->mv[9] = pMb->mv[10] = pMb->mv[11] = + pMb->mv[12] = pMb->mv[13] = pMb->mv[14] = pMb->mv[15] = mv; + + pMb->refPic[0] = refIndex; + pMb->refPic[1] = refIndex; + pMb->refPic[2] = refIndex; + pMb->refPic[3] = refIndex; + pMb->refAddr[0] = tmp; + pMb->refAddr[1] = tmp; + pMb->refAddr[2] = tmp; + pMb->refAddr[3] = tmp; + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: MvPrediction16x8 + + Functional description: + Motion vector prediction for 16x8 partition mode + +------------------------------------------------------------------------------*/ + +u32 MvPrediction16x8(mbStorage_t *pMb, mbPred_t *mbPred, dpbStorage_t *dpb) +{ + +/* Variables */ + + mv_t mv; + mv_t mvPred; + interNeighbour_t a[3]; /* A, B, C */ + u32 refIndex; + u8 *tmp; + +/* Code */ + + mv = mbPred->mvdL0[0]; + refIndex = mbPred->refIdxL0[0]; + + GetInterNeighbour(pMb->sliceId, pMb->mbB, a+1, 10); + + if (a[1].refIndex == refIndex) + mvPred = a[1].mv; + else + { + GetInterNeighbour(pMb->sliceId, pMb->mbA, a, 5); + GetInterNeighbour(pMb->sliceId, pMb->mbC, a+2, 10); + if (!a[2].available) + { + GetInterNeighbour(pMb->sliceId, pMb->mbD, a+2, 15); + } + + GetPredictionMv(&mvPred, a, refIndex); + + } + mv.hor += mvPred.hor; + mv.ver += mvPred.ver; + + /* horizontal motion vector range [-2048, 2047.75] */ + if ((u32)(i32)(mv.hor+8192) >= (16384)) + return(HANTRO_NOK); + + /* vertical motion vector range [-512, 511.75] (smaller for low levels) */ + if ((u32)(i32)(mv.ver+2048) >= (4096)) + return(HANTRO_NOK); + + tmp = h264bsdGetRefPicData(dpb, refIndex); + if (tmp == NULL) + return(HANTRO_NOK); + + pMb->mv[0] = pMb->mv[1] = pMb->mv[2] = pMb->mv[3] = + pMb->mv[4] = pMb->mv[5] = pMb->mv[6] = pMb->mv[7] = mv; + pMb->refPic[0] = refIndex; + pMb->refPic[1] = refIndex; + pMb->refAddr[0] = tmp; + pMb->refAddr[1] = tmp; + + mv = mbPred->mvdL0[1]; + refIndex = mbPred->refIdxL0[1]; + + GetInterNeighbour(pMb->sliceId, pMb->mbA, a, 13); + if (a[0].refIndex == refIndex) + mvPred = a[0].mv; + else + { + a[1].available = HANTRO_TRUE; + a[1].refIndex = pMb->refPic[0]; + a[1].mv = pMb->mv[0]; + + /* c is not available */ + GetInterNeighbour(pMb->sliceId, pMb->mbA, a+2, 7); + + GetPredictionMv(&mvPred, a, refIndex); + + } + mv.hor += mvPred.hor; + mv.ver += mvPred.ver; + + /* horizontal motion vector range [-2048, 2047.75] */ + if ((u32)(i32)(mv.hor+8192) >= (16384)) + return(HANTRO_NOK); + + /* vertical motion vector range [-512, 511.75] (smaller for low levels) */ + if ((u32)(i32)(mv.ver+2048) >= (4096)) + return(HANTRO_NOK); + + tmp = h264bsdGetRefPicData(dpb, refIndex); + if (tmp == NULL) + return(HANTRO_NOK); + + pMb->mv[8] = pMb->mv[9] = pMb->mv[10] = pMb->mv[11] = + pMb->mv[12] = pMb->mv[13] = pMb->mv[14] = pMb->mv[15] = mv; + pMb->refPic[2] = refIndex; + pMb->refPic[3] = refIndex; + pMb->refAddr[2] = tmp; + pMb->refAddr[3] = tmp; + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: MvPrediction8x16 + + Functional description: + Motion vector prediction for 8x16 partition mode + +------------------------------------------------------------------------------*/ + +u32 MvPrediction8x16(mbStorage_t *pMb, mbPred_t *mbPred, dpbStorage_t *dpb) +{ + +/* Variables */ + + mv_t mv; + mv_t mvPred; + interNeighbour_t a[3]; /* A, B, C */ + u32 refIndex; + u8 *tmp; + +/* Code */ + + mv = mbPred->mvdL0[0]; + refIndex = mbPred->refIdxL0[0]; + + GetInterNeighbour(pMb->sliceId, pMb->mbA, a, 5); + + if (a[0].refIndex == refIndex) + mvPred = a[0].mv; + else + { + GetInterNeighbour(pMb->sliceId, pMb->mbB, a+1, 10); + GetInterNeighbour(pMb->sliceId, pMb->mbB, a+2, 14); + if (!a[2].available) + { + GetInterNeighbour(pMb->sliceId, pMb->mbD, a+2, 15); + } + + GetPredictionMv(&mvPred, a, refIndex); + + } + mv.hor += mvPred.hor; + mv.ver += mvPred.ver; + + /* horizontal motion vector range [-2048, 2047.75] */ + if ((u32)(i32)(mv.hor+8192) >= (16384)) + return(HANTRO_NOK); + + /* vertical motion vector range [-512, 511.75] (smaller for low levels) */ + if ((u32)(i32)(mv.ver+2048) >= (4096)) + return(HANTRO_NOK); + + tmp = h264bsdGetRefPicData(dpb, refIndex); + if (tmp == NULL) + return(HANTRO_NOK); + + pMb->mv[0] = pMb->mv[1] = pMb->mv[2] = pMb->mv[3] = + pMb->mv[8] = pMb->mv[9] = pMb->mv[10] = pMb->mv[11] = mv; + pMb->refPic[0] = refIndex; + pMb->refPic[2] = refIndex; + pMb->refAddr[0] = tmp; + pMb->refAddr[2] = tmp; + + mv = mbPred->mvdL0[1]; + refIndex = mbPred->refIdxL0[1]; + + GetInterNeighbour(pMb->sliceId, pMb->mbC, a+2, 10); + if (!a[2].available) + { + GetInterNeighbour(pMb->sliceId, pMb->mbB, a+2, 11); + } + if (a[2].refIndex == refIndex) + mvPred = a[2].mv; + else + { + a[0].available = HANTRO_TRUE; + a[0].refIndex = pMb->refPic[0]; + a[0].mv = pMb->mv[0]; + + GetInterNeighbour(pMb->sliceId, pMb->mbB, a+1, 14); + + GetPredictionMv(&mvPred, a, refIndex); + + } + mv.hor += mvPred.hor; + mv.ver += mvPred.ver; + + /* horizontal motion vector range [-2048, 2047.75] */ + if ((u32)(i32)(mv.hor+8192) >= (16384)) + return(HANTRO_NOK); + + /* vertical motion vector range [-512, 511.75] (smaller for low levels) */ + if ((u32)(i32)(mv.ver+2048) >= (4096)) + return(HANTRO_NOK); + + tmp = h264bsdGetRefPicData(dpb, refIndex); + if (tmp == NULL) + return(HANTRO_NOK); + + pMb->mv[4] = pMb->mv[5] = pMb->mv[6] = pMb->mv[7] = + pMb->mv[12] = pMb->mv[13] = pMb->mv[14] = pMb->mv[15] = mv; + pMb->refPic[1] = refIndex; + pMb->refPic[3] = refIndex; + pMb->refAddr[1] = tmp; + pMb->refAddr[3] = tmp; + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: MvPrediction8x8 + + Functional description: + Motion vector prediction for 8x8 partition mode + +------------------------------------------------------------------------------*/ + +u32 MvPrediction8x8(mbStorage_t *pMb, subMbPred_t *subMbPred, dpbStorage_t *dpb) +{ + +/* Variables */ + + u32 i, j; + u32 numSubMbPart; + +/* Code */ + + for (i = 0; i < 4; i++) + { + numSubMbPart = h264bsdNumSubMbPart(subMbPred->subMbType[i]); + pMb->refPic[i] = subMbPred->refIdxL0[i]; + pMb->refAddr[i] = h264bsdGetRefPicData(dpb, subMbPred->refIdxL0[i]); + if (pMb->refAddr[i] == NULL) + return(HANTRO_NOK); + for (j = 0; j < numSubMbPart; j++) + { + if (MvPrediction(pMb, subMbPred, i, j) != HANTRO_OK) + return(HANTRO_NOK); + } + } + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: MvPrediction + + Functional description: + Perform motion vector prediction for sub-partition + +------------------------------------------------------------------------------*/ + +u32 MvPrediction(mbStorage_t *pMb, subMbPred_t *subMbPred, u32 mbPartIdx, + u32 subMbPartIdx) +{ + +/* Variables */ + + mv_t mv, mvPred; + u32 refIndex; + subMbPartMode_e subMbPartMode; + const neighbour_t *n; + mbStorage_t *nMb; + interNeighbour_t a[3]; /* A, B, C */ + +/* Code */ + + mv = subMbPred->mvdL0[mbPartIdx][subMbPartIdx]; + subMbPartMode = h264bsdSubMbPartMode(subMbPred->subMbType[mbPartIdx]); + refIndex = subMbPred->refIdxL0[mbPartIdx]; + + n = N_A_SUB_PART[mbPartIdx][subMbPartMode]+subMbPartIdx; + nMb = h264bsdGetNeighbourMb(pMb, n->mb); + GetInterNeighbour(pMb->sliceId, nMb, a, n->index); + + n = N_B_SUB_PART[mbPartIdx][subMbPartMode]+subMbPartIdx; + nMb = h264bsdGetNeighbourMb(pMb, n->mb); + GetInterNeighbour(pMb->sliceId, nMb, a+1, n->index); + + n = N_C_SUB_PART[mbPartIdx][subMbPartMode]+subMbPartIdx; + nMb = h264bsdGetNeighbourMb(pMb, n->mb); + GetInterNeighbour(pMb->sliceId, nMb, a+2, n->index); + + if (!a[2].available) + { + n = N_D_SUB_PART[mbPartIdx][subMbPartMode]+subMbPartIdx; + nMb = h264bsdGetNeighbourMb(pMb, n->mb); + GetInterNeighbour(pMb->sliceId, nMb, a+2, n->index); + } + + GetPredictionMv(&mvPred, a, refIndex); + + mv.hor += mvPred.hor; + mv.ver += mvPred.ver; + + /* horizontal motion vector range [-2048, 2047.75] */ + if (((u32)(i32)(mv.hor+8192) >= (16384))) + return(HANTRO_NOK); + + /* vertical motion vector range [-512, 511.75] (smaller for low levels) */ + if (((u32)(i32)(mv.ver+2048) >= (4096))) + return(HANTRO_NOK); + + switch (subMbPartMode) + { + case MB_SP_8x8: + pMb->mv[4*mbPartIdx] = mv; + pMb->mv[4*mbPartIdx + 1] = mv; + pMb->mv[4*mbPartIdx + 2] = mv; + pMb->mv[4*mbPartIdx + 3] = mv; + break; + + case MB_SP_8x4: + pMb->mv[4*mbPartIdx + 2*subMbPartIdx] = mv; + pMb->mv[4*mbPartIdx + 2*subMbPartIdx + 1] = mv; + break; + + case MB_SP_4x8: + pMb->mv[4*mbPartIdx + subMbPartIdx] = mv; + pMb->mv[4*mbPartIdx + subMbPartIdx + 2] = mv; + break; + + case MB_SP_4x4: + pMb->mv[4*mbPartIdx + subMbPartIdx] = mv; + break; + } + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: MedianFilter + + Functional description: + Median filtering for motion vector prediction + +------------------------------------------------------------------------------*/ + +i32 MedianFilter(i32 a, i32 b, i32 c) +{ + +/* Variables */ + + i32 max,min,med; + +/* Code */ + + max = min = med = a; + if (b > max) + { + max = b; + } + else if (b < min) + { + min = b; + } + if (c > max) + { + med = max; + } + else if (c < min) + { + med = min; + } + else + { + med = c; + } + + return(med); +} + +/*------------------------------------------------------------------------------ + + Function: GetInterNeighbour + + Functional description: + Get availability, reference index and motion vector of a neighbour + +------------------------------------------------------------------------------*/ + +void GetInterNeighbour(u32 sliceId, mbStorage_t *nMb, + interNeighbour_t *n, u32 index) +{ + + n->available = HANTRO_FALSE; + n->refIndex = 0xFFFFFFFF; + n->mv.hor = n->mv.ver = 0; + + if (nMb && (sliceId == nMb->sliceId)) + { + u32 tmp; + mv_t tmpMv; + + tmp = nMb->mbType; + n->available = HANTRO_TRUE; + /* MbPartPredMode "inlined" */ + if (tmp <= P_8x8ref0) + { + tmpMv = nMb->mv[index]; + tmp = nMb->refPic[index>>2]; + n->refIndex = tmp; + n->mv = tmpMv; + } + } + +} + +/*------------------------------------------------------------------------------ + + Function: GetPredictionMv + + Functional description: + Compute motion vector predictor based on neighbours A, B and C + +------------------------------------------------------------------------------*/ + +void GetPredictionMv(mv_t *mv, interNeighbour_t *a, u32 refIndex) +{ + + if ( a[1].available || a[2].available || !a[0].available) + { + u32 isA, isB, isC; + isA = (a[0].refIndex == refIndex) ? HANTRO_TRUE : HANTRO_FALSE; + isB = (a[1].refIndex == refIndex) ? HANTRO_TRUE : HANTRO_FALSE; + isC = (a[2].refIndex == refIndex) ? HANTRO_TRUE : HANTRO_FALSE; + + if (((u32)isA+(u32)isB+(u32)isC) != 1) + { + mv->hor = (i16)MedianFilter(a[0].mv.hor, a[1].mv.hor, a[2].mv.hor); + mv->ver = (i16)MedianFilter(a[0].mv.ver, a[1].mv.ver, a[2].mv.ver); + } + else if (isA) + *mv = a[0].mv; + else if (isB) + *mv = a[1].mv; + else + *mv = a[2].mv; + } + else + { + *mv = a[0].mv; + } + +} + + diff --git a/src/video/h264_decoder/h264bsd_inter_prediction.h b/src/video/h264_decoder/h264bsd_inter_prediction.h new file mode 100644 index 000000000..064683af3 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_inter_prediction.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_INTER_PREDICTION_H +#define H264SWDEC_INTER_PREDICTION_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_image.h" +#include "h264bsd_macroblock_layer.h" +#include "h264bsd_dpb.h" +#include "h264bsd_slice_header.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +u32 h264bsdInterPrediction(mbStorage_t *pMb, macroblockLayer_t *pMbLayer, + dpbStorage_t *dpb, u32 mbNum, image_t *image, + const sliceHeader_t *pSliceHeader); + +#ifdef H264BSD_N64 +/* Must be called once per slice, before decoding its macroblocks. */ +void h264bsdPrepareWeights(sliceHeader_t *pSliceHeader); +#endif + +#endif /* #ifdef H264SWDEC_INTER_PREDICTION_H */ + diff --git a/src/video/h264_decoder/h264bsd_intra_prediction.c b/src/video/h264_decoder/h264bsd_intra_prediction.c new file mode 100644 index 000000000..d5efda071 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_intra_prediction.c @@ -0,0 +1,614 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdIntraPrediction + h264bsdGetNeighbourPels + h264bsdIntra16x16Prediction + h264bsdIntra4x4Prediction + h264bsdIntraChromaPrediction + h264bsdAddResidual + Intra16x16VerticalPrediction + Intra16x16HorizontalPrediction + Intra16x16DcPrediction + Intra16x16PlanePrediction + IntraChromaDcPrediction + IntraChromaHorizontalPrediction + IntraChromaVerticalPrediction + IntraChromaPlanePrediction + Get4x4NeighbourPels + Write4x4To16x16 + Intra4x4VerticalPrediction + Intra4x4HorizontalPrediction + Intra4x4DcPrediction + Intra4x4DiagonalDownLeftPrediction + Intra4x4DiagonalDownRightPrediction + Intra4x4VerticalRightPrediction + Intra4x4HorizontalDownPrediction + Intra4x4VerticalLeftPrediction + Intra4x4HorizontalUpPrediction + DetermineIntra4x4PredMode + +------------------------------------------------------------------------------*/ + +#include "opttarget.h" + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_intra_prediction.h" +#include "h264bsd_util.h" +#include "h264bsd_macroblock_layer.h" +#include "h264bsd_neighbour.h" +#include "h264bsd_image.h" + +#include "omxdl/omxtypes.h" +#include "omxdl/omxVC.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/* Switch off the following Lint messages for this file: + * Info 702: Shift right of signed quantity (int) + */ +/*lint -e702 */ + + +/* x- and y-coordinates for each block */ +const u32 h264bsdBlockX[16] = + { 0, 4, 0, 4, 8, 12, 8, 12, 0, 4, 0, 4, 8, 12, 8, 12 }; +const u32 h264bsdBlockY[16] = + { 0, 0, 4, 4, 0, 0, 4, 4, 8, 8, 12, 12, 8, 8, 12, 12 }; + +const u8 h264bsdClip[1280] = +{ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15, + 16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47, + 48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63, + 64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79, + 80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95, + 96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111, + 112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127, + 128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143, + 144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159, + 160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175, + 176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191, + 192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207, + 208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223, + 224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239, + 240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255 +}; + +static u32 DetermineIntra4x4PredMode(macroblockLayer_t *pMbLayer, + u32 available, neighbour_t *nA, neighbour_t *nB, u32 index, + mbStorage_t *nMbA, mbStorage_t *nMbB); + + +/*------------------------------------------------------------------------------ + + Function: h264bsdIntra16x16Prediction + + Functional description: + Perform intra 16x16 prediction mode for luma pixels and add + residual into prediction. The resulting luma pixels are + stored in macroblock array 'data'. + +------------------------------------------------------------------------------*/ +u32 h264bsdIntra16x16Prediction(mbStorage_t *pMb, u8 *data, u8 *ptr, + u32 width, u32 constrainedIntraPred) +{ + +/* Variables */ + + u32 availableA, availableB, availableD; + OMXResult omxRes; + +/* Code */ + ASSERT(pMb); + ASSERT(data); + ASSERT(ptr); + ASSERT(h264bsdPredModeIntra16x16(pMb->mbType) < 4); + + availableA = h264bsdIsNeighbourAvailable(pMb, pMb->mbA); + if (availableA && constrainedIntraPred && + (h264bsdMbPartPredMode(pMb->mbA->mbType) == PRED_MODE_INTER)) + availableA = HANTRO_FALSE; + availableB = h264bsdIsNeighbourAvailable(pMb, pMb->mbB); + if (availableB && constrainedIntraPred && + (h264bsdMbPartPredMode(pMb->mbB->mbType) == PRED_MODE_INTER)) + availableB = HANTRO_FALSE; + availableD = h264bsdIsNeighbourAvailable(pMb, pMb->mbD); + if (availableD && constrainedIntraPred && + (h264bsdMbPartPredMode(pMb->mbD->mbType) == PRED_MODE_INTER)) + availableD = HANTRO_FALSE; + + omxRes = omxVCM4P10_PredictIntra_16x16( (ptr-1), + (ptr - width), + (ptr - width-1), + data, + (i32)width, + 16, + (OMXVCM4P10Intra16x16PredMode) + h264bsdPredModeIntra16x16(pMb->mbType), + (i32)(availableB + (availableA<<1) + + (availableD<<5)) ); + if (omxRes != OMX_Sts_NoErr) + return HANTRO_NOK; + else + return(HANTRO_OK); +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdIntra4x4Prediction + + Functional description: + Perform intra 4x4 prediction for luma pixels and add residual + into prediction. The resulting luma pixels are stored in + macroblock array 'data'. The intra 4x4 prediction mode for each + block is stored in 'pMb' structure. + +------------------------------------------------------------------------------*/ +u32 h264bsdIntra4x4Prediction(mbStorage_t *pMb, u8 *data, + macroblockLayer_t *mbLayer, + u8 *ptr, u32 width, + u32 constrainedIntraPred, u32 block) +{ + +/* Variables */ + u32 mode; + neighbour_t neighbour, neighbourB; + mbStorage_t *nMb, *nMb2; + u32 availableA, availableB, availableC, availableD; + + OMXResult omxRes; + u32 x, y; + u8 *l, *a, *al; +/* Code */ + ASSERT(pMb); + ASSERT(data); + ASSERT(mbLayer); + ASSERT(ptr); + ASSERT(pMb->intra4x4PredMode[block] < 9); + + neighbour = *h264bsdNeighbour4x4BlockA(block); + nMb = h264bsdGetNeighbourMb(pMb, neighbour.mb); + availableA = h264bsdIsNeighbourAvailable(pMb, nMb); + if (availableA && constrainedIntraPred && + ( h264bsdMbPartPredMode(nMb->mbType) == PRED_MODE_INTER) ) + { + availableA = HANTRO_FALSE; + } + + neighbourB = *h264bsdNeighbour4x4BlockB(block); + nMb2 = h264bsdGetNeighbourMb(pMb, neighbourB.mb); + availableB = h264bsdIsNeighbourAvailable(pMb, nMb2); + if (availableB && constrainedIntraPred && + ( h264bsdMbPartPredMode(nMb2->mbType) == PRED_MODE_INTER) ) + { + availableB = HANTRO_FALSE; + } + + mode = DetermineIntra4x4PredMode(mbLayer, + (u32)(availableA && availableB), + &neighbour, &neighbourB, block, nMb, nMb2); + pMb->intra4x4PredMode[block] = (u8)mode; + + neighbour = *h264bsdNeighbour4x4BlockC(block); + nMb = h264bsdGetNeighbourMb(pMb, neighbour.mb); + availableC = h264bsdIsNeighbourAvailable(pMb, nMb); + if (availableC && constrainedIntraPred && + ( h264bsdMbPartPredMode(nMb->mbType) == PRED_MODE_INTER) ) + { + availableC = HANTRO_FALSE; + } + + neighbour = *h264bsdNeighbour4x4BlockD(block); + nMb = h264bsdGetNeighbourMb(pMb, neighbour.mb); + availableD = h264bsdIsNeighbourAvailable(pMb, nMb); + if (availableD && constrainedIntraPred && + ( h264bsdMbPartPredMode(nMb->mbType) == PRED_MODE_INTER) ) + { + availableD = HANTRO_FALSE; + } + + x = h264bsdBlockX[block]; + y = h264bsdBlockY[block]; + + if (y == 0) + a = ptr - width + x; + else + a = data-16; + + if (x == 0) + l = ptr + y * width -1; + else + { + l = data-1; + width = 16; + } + + if (x == 0) + al = l-width; + else + al = a-1; + + omxRes = omxVCM4P10_PredictIntra_4x4( l, + a, + al, + data, + (i32)width, + 16, + (OMXVCM4P10Intra4x4PredMode)mode, + (i32)(availableB + + (availableA<<1) + + (availableD<<5) + + (availableC<<6)) ); + if (omxRes != OMX_Sts_NoErr) + return HANTRO_NOK; + + return(HANTRO_OK); + +} + + +u32 h264bsdIntra4x4PredictionAndTransformAll(mbStorage_t *pMb, + macroblockLayer_t *mbLayer, + u32 constrainedIntraPred, + image_t *image, + const u8 **pCoeff +) +{ + u8 modeAvail[16]; + int i; + + ASSERT(pMb); + ASSERT(mbLayer); + + for (i = 0; i < 16; i++) + { + u32 mode; + neighbour_t neighbour, neighbourB; + mbStorage_t *nMb, *nMb2; + u32 availableA, availableB, availableC, availableD; + int block = i; + + ASSERT(pMb->intra4x4PredMode[block] < 9); + + /* Inline h264bsdIntra4x4Prediction */ + + neighbour = *h264bsdNeighbour4x4BlockA(block); + nMb = h264bsdGetNeighbourMb(pMb, neighbour.mb); + availableA = h264bsdIsNeighbourAvailable(pMb, nMb); + if (availableA && constrainedIntraPred && + ( h264bsdMbPartPredMode(nMb->mbType) == PRED_MODE_INTER) ) + { + availableA = HANTRO_FALSE; + } + + neighbourB = *h264bsdNeighbour4x4BlockB(block); + nMb2 = h264bsdGetNeighbourMb(pMb, neighbourB.mb); + availableB = h264bsdIsNeighbourAvailable(pMb, nMb2); + if (availableB && constrainedIntraPred && + ( h264bsdMbPartPredMode(nMb2->mbType) == PRED_MODE_INTER) ) + { + availableB = HANTRO_FALSE; + } + + mode = DetermineIntra4x4PredMode(mbLayer, + (u32)(availableA && availableB), + &neighbour, &neighbourB, block, nMb, nMb2); + pMb->intra4x4PredMode[block] = (u8)mode; + + neighbour = *h264bsdNeighbour4x4BlockC(block); + nMb = h264bsdGetNeighbourMb(pMb, neighbour.mb); + availableC = h264bsdIsNeighbourAvailable(pMb, nMb); + if (availableC && constrainedIntraPred && + ( h264bsdMbPartPredMode(nMb->mbType) == PRED_MODE_INTER) ) + { + availableC = HANTRO_FALSE; + } + + neighbour = *h264bsdNeighbour4x4BlockD(block); + nMb = h264bsdGetNeighbourMb(pMb, neighbour.mb); + availableD = h264bsdIsNeighbourAvailable(pMb, nMb); + if (availableD && constrainedIntraPred && + ( h264bsdMbPartPredMode(nMb->mbType) == PRED_MODE_INTER) ) + { + availableD = HANTRO_FALSE; + } + + + // For each 4x4 partition, we store: + // * 4 bits of availability (upper, left, upper-left and upper-right). + // These are the only availability bits used by Intra4x4. + // * 4 bits of intra mode (value 0-9) + modeAvail[block] = + (availableB << 0) | // upper + (availableA << 1) | // left + (availableD << 2) | // upper-left + (availableC << 3) | // upper-right + (mode << 4); + } + + + OMXResult omxRes = HIGHFUNC_PredictIntraTransform_4x4( + image->luma, + image->luma, + image->width*16, + image->width*16, + pCoeff, + pMb->totalCoeff, + modeAvail, + pMb->qpY); + if (omxRes != OMX_Sts_NoErr) + return HANTRO_NOK; + + return(HANTRO_OK); +} + + + +/*------------------------------------------------------------------------------ + + Function: h264bsdIntraChromaPrediction + + Functional description: + Perform intra prediction for chroma pixels and add residual + into prediction. The resulting chroma pixels are stored in 'data'. + +------------------------------------------------------------------------------*/ +u32 h264bsdIntraChromaPrediction(mbStorage_t *pMb, image_t *image, + u32 predMode, u32 constrainedIntraPred) +{ + +/* Variables */ + + u32 availableA, availableB, availableD; + OMXResult omxRes; + u8 *ptr; + u32 width; + +/* Code */ + ASSERT(pMb); + ASSERT(image); + ASSERT(predMode < 4); + + availableA = h264bsdIsNeighbourAvailable(pMb, pMb->mbA); + if (availableA && constrainedIntraPred && + (h264bsdMbPartPredMode(pMb->mbA->mbType) == PRED_MODE_INTER)) + availableA = HANTRO_FALSE; + availableB = h264bsdIsNeighbourAvailable(pMb, pMb->mbB); + if (availableB && constrainedIntraPred && + (h264bsdMbPartPredMode(pMb->mbB->mbType) == PRED_MODE_INTER)) + availableB = HANTRO_FALSE; + availableD = h264bsdIsNeighbourAvailable(pMb, pMb->mbD); + if (availableD && constrainedIntraPred && + (h264bsdMbPartPredMode(pMb->mbD->mbType) == PRED_MODE_INTER)) + availableD = HANTRO_FALSE; + + ptr = image->cb; + width = image->width*8; + + omxRes = omxVCM4P10_PredictIntraChroma_8x8( (ptr-1), + (ptr - width), + (ptr - width -1), + image->cb, + (i32)width, + image->width*8, + (OMXVCM4P10IntraChromaPredMode) + predMode, + (i32)(availableB + + (availableA<<1) + + (availableD<<5)) ); + if (omxRes != OMX_Sts_NoErr) + return HANTRO_NOK; + + /* advance pointers */ + ptr = image->cr; + + omxRes = omxVCM4P10_PredictIntraChroma_8x8( (ptr-1), + (ptr - width), + (ptr - width -1), + image->cr, + (i32)width, + image->width*8, + (OMXVCM4P10IntraChromaPredMode) + predMode, + (i32)(availableB + + (availableA<<1) + + (availableD<<5)) ); + if (omxRes != OMX_Sts_NoErr) + return HANTRO_NOK; + + return(HANTRO_OK); + +} + + + +/*------------------------------------------------------------------------------ + + Function: Write4x4To16x16 + + Functional description: + Write a 4x4 block (data4x4) into correct position + in 16x16 macroblock (data). + +------------------------------------------------------------------------------*/ + +void Write4x4To16x16(u8 *data, u8 *data4x4, u32 blockNum) +{ + +/* Variables */ + + u32 x, y; + u32 *in32, *out32; + +/* Code */ + + ASSERT(data); + ASSERT(data4x4); + ASSERT(blockNum < 16); + + x = h264bsdBlockX[blockNum]; + y = h264bsdBlockY[blockNum]; + + data += y*16+x; + + ASSERT(((u32)data&0x3) == 0); + + /*lint --e(826) */ + out32 = (u32 *)data; + /*lint --e(826) */ + in32 = (u32 *)data4x4; + + out32[0] = *in32++; + out32[4] = *in32++; + out32[8] = *in32++; + out32[12] = *in32++; +} + +/*------------------------------------------------------------------------------ + + Function: DetermineIntra4x4PredMode + + Functional description: + Returns the intra 4x4 prediction mode of a block based on the + neighbouring macroblocks and information parsed from stream. + +------------------------------------------------------------------------------*/ + +u32 DetermineIntra4x4PredMode(macroblockLayer_t *pMbLayer, + u32 available, neighbour_t *nA, neighbour_t *nB, u32 index, + mbStorage_t *nMbA, mbStorage_t *nMbB) +{ + +/* Variables */ + + u32 mode1, mode2; + mbStorage_t *pMb; + +/* Code */ + + ASSERT(pMbLayer); + + /* dc only prediction? */ + if (!available) + mode1 = 2; + else + { + pMb = nMbA; + if (h264bsdMbPartPredMode(pMb->mbType) == PRED_MODE_INTRA4x4) + { + mode1 = pMb->intra4x4PredMode[nA->index]; + } + else + mode1 = 2; + + pMb = nMbB; + if (h264bsdMbPartPredMode(pMb->mbType) == PRED_MODE_INTRA4x4) + { + mode2 = pMb->intra4x4PredMode[nB->index]; + } + else + mode2 = 2; + + mode1 = MIN(mode1, mode2); + } + + if (!pMbLayer->mbPred.prevIntra4x4PredModeFlag[index]) + { + if (pMbLayer->mbPred.remIntra4x4PredMode[index] < mode1) + { + mode1 = pMbLayer->mbPred.remIntra4x4PredMode[index]; + } + else + { + mode1 = pMbLayer->mbPred.remIntra4x4PredMode[index] + 1; + } + } + + return(mode1); +} + + +/*lint +e702 */ + + diff --git a/src/video/h264_decoder/h264bsd_intra_prediction.h b/src/video/h264_decoder/h264bsd_intra_prediction.h new file mode 100644 index 000000000..d73ba1d94 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_intra_prediction.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_INTRA_PREDICTION_H +#define H264SWDEC_INTRA_PREDICTION_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_image.h" +#include "h264bsd_macroblock_layer.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ +u32 h264bsdIntra4x4Prediction(mbStorage_t *pMb, u8 *data, + macroblockLayer_t *mbLayer, + u8 *pImage, u32 width, + u32 constrainedIntraPred, u32 block); + +u32 h264bsdIntra4x4PredictionAndTransformAll(mbStorage_t *pMb, + macroblockLayer_t *mbLayer, + u32 constrainedIntraPred, + image_t *image, + const u8 **pCoeff); + +u32 h264bsdIntra16x16Prediction(mbStorage_t *pMb, u8 *data, u8 *pImage, + u32 width, u32 constrainedIntraPred); + +u32 h264bsdIntraChromaPrediction(mbStorage_t *pMb, image_t *image, + u32 predMode, u32 constrainedIntraPred); + +#endif /* #ifdef H264SWDEC_INTRA_PREDICTION_H */ + diff --git a/src/video/h264_decoder/h264bsd_macroblock_layer.c b/src/video/h264_decoder/h264bsd_macroblock_layer.c new file mode 100644 index 000000000..95c75d89d --- /dev/null +++ b/src/video/h264_decoder/h264bsd_macroblock_layer.c @@ -0,0 +1,1159 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdDecodeMacroblockLayer + h264bsdMbPartPredMode + h264bsdNumMbPart + h264bsdNumSubMbPart + DecodeMbPred + DecodeSubMbPred + DecodeResidual + DetermineNc + CbpIntra16x16 + h264bsdPredModeIntra16x16 + h264bsdDecodeMacroblock + ProcessResidual + h264bsdSubMbPartMode + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_macroblock_layer.h" +#include "h264bsd_slice_header.h" +#include "h264bsd_util.h" +#include "h264bsd_vlc.h" +#include "h264bsd_cavlc.h" +#include "h264bsd_nal_unit.h" +#include "h264bsd_neighbour.h" +#include "h264bsd_transform.h" +#include "h264bsd_intra_prediction.h" +#include "h264bsd_inter_prediction.h" + +#include "omxdl/omxtypes.h" +#include "omxdl/omxVC.h" +#include "omxdl/armVC.h" + +#ifdef H264BSD_N64 +#include "../rsph264_internal.h" +#endif +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ +const u32 lumaIndex[16] = { 0, 4, 64, 68, + 8, 12, 72, 76, + 128, 132, 192, 196, + 136, 140, 200, 204 }; + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +static u32 DecodeMbPred(strmData_t *pStrmData, mbPred_t *pMbPred, + mbType_e mbType, u32 numRefIdxActive); +static u32 DecodeSubMbPred(strmData_t *pStrmData, subMbPred_t *pSubMbPred, + mbType_e mbType, u32 numRefIdxActive); +static u32 DecodeResidual(strmData_t *pStrmData, residual_t *pResidual, + mbStorage_t *pMb, mbType_e mbType, u32 codedBlockPattern); + +u32 DetermineNc(mbStorage_t *pMb, u32 blockIndex, u8 *pTotalCoeff); + +static u32 CbpIntra16x16(mbType_e mbType); +static u32 ProcessIntra4x4Residual(mbStorage_t *pMb, u32 constrainedIntraPred, + macroblockLayer_t *mbLayer, const u8 **pSrc, image_t *image); +static u32 ProcessChromaResidual(mbStorage_t *pMb, const u8 **pSrc, image_t *image); +static u32 ProcessIntra16x16Residual(mbStorage_t *pMb, u32 constrainedIntraPred, + u32 intraChromaPredMode, const u8 **pSrc, image_t *image); + + +/*------------------------------------------------------------------------------ + + Function name: h264bsdDecodeMacroblockLayer + + Functional description: + Parse macroblock specific information from bit stream. + + Inputs: + pStrmData pointer to stream data structure + pMb pointer to macroblock storage structure + sliceType type of the current slice + numRefIdxActive maximum reference index + + Outputs: + pMbLayer stores the macroblock data parsed from stream + + Returns: + HANTRO_OK success + HANTRO_NOK end of stream or error in stream + +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeMacroblockLayer(strmData_t *pStrmData, + macroblockLayer_t *pMbLayer, mbStorage_t *pMb, u32 sliceType, + u32 numRefIdxActive) +{ + +/* Variables */ + + u32 tmp, i, value; + i32 itmp; + mbPartPredMode_e partMode; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(pMbLayer); + + PROFILE_START(PS_H264_LAYER_CLEAR); +#ifdef H264DEC_NEON + h264bsdClearMbLayer(pMbLayer, ((sizeof(macroblockLayer_t) + 63) & ~0x3F)); +#elif defined(H264BSD_N64) + // On N64, we totally skip clearing pMBLayer. It's a waste of time and shows + // up in the profile, probably because of data cache trashing. Instead, + // we have adapted a little bit the code around here to not rely on this + // initial clearing (eg: explicit clearing the specific fields in case + // the codepath does not set them but later code relies on that). +#else + H264SwDecMemset(pMbLayer, 0, sizeof(macroblockLayer_t)); +#endif + + PROFILE_STOP(PS_H264_LAYER_CLEAR); + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + + if (IS_I_SLICE(sliceType)) + { + if ((value + 6) > 31 || tmp != HANTRO_OK) + return(HANTRO_NOK); + pMbLayer->mbType = (mbType_e)(value + 6); + } + else + { + if ((value + 1) > 31 || tmp != HANTRO_OK) + return(HANTRO_NOK); + pMbLayer->mbType = (mbType_e)(value + 1); + } + + if (pMbLayer->mbType == I_PCM) + { + while( !h264bsdIsByteAligned(pStrmData) ) + { + /* pcm_alignment_zero_bit */ + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp) + return(HANTRO_NOK); + } + + u8 *level = pMbLayer->residual.posCoefBuf; + + for (i = 0; i < 384; i++) + { + value = h264bsdGetBits(pStrmData, 8); + if (value == END_OF_STREAM) + return(HANTRO_NOK); + *level++ = value; + } + } + else + { + PROFILE_START(PS_H264_LAYER_PRED); + partMode = h264bsdMbPartPredMode(pMbLayer->mbType); + if ( (partMode == PRED_MODE_INTER) && + (h264bsdNumMbPart(pMbLayer->mbType) == 4) ) + { + tmp = DecodeSubMbPred(pStrmData, &pMbLayer->subMbPred, + pMbLayer->mbType, numRefIdxActive); + } + else + { + tmp = DecodeMbPred(pStrmData, &pMbLayer->mbPred, + pMbLayer->mbType, numRefIdxActive); + } + if (tmp != HANTRO_OK) { + PROFILE_STOP(PS_H264_LAYER_PRED); + return(tmp); + } + + if (partMode != PRED_MODE_INTRA16x16) + { + tmp = h264bsdDecodeExpGolombMapped(pStrmData, &value, + (u32)(partMode == PRED_MODE_INTRA4x4)); + if (tmp != HANTRO_OK) { + PROFILE_STOP(PS_H264_LAYER_PRED); + return(tmp); + } + pMbLayer->codedBlockPattern = value; + } + else + { + pMbLayer->codedBlockPattern = CbpIntra16x16(pMbLayer->mbType); + } + PROFILE_STOP(PS_H264_LAYER_PRED); + + if ( pMbLayer->codedBlockPattern || + (partMode == PRED_MODE_INTRA16x16) ) + { + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &itmp); + if (tmp != HANTRO_OK || (itmp < -26) || (itmp > 25) ) + return(HANTRO_NOK); + pMbLayer->mbQpDelta = itmp; + + PROFILE_START(PS_H264_LAYER_RES); + tmp = DecodeResidual(pStrmData, &pMbLayer->residual, pMb, + pMbLayer->mbType, pMbLayer->codedBlockPattern); + PROFILE_STOP(PS_H264_LAYER_RES); + + if (tmp != HANTRO_OK) + return(tmp); + } else { + PROFILE_START(PS_H264_LAYER_CLEAR, 1); + pMbLayer->mbQpDelta = 0; + for (i = 0; i < 27; i++) + pMbLayer->residual.totalCoeff[i] = 0; + PROFILE_STOP(PS_H264_LAYER_CLEAR, 1); + } + } + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdMbPartPredMode + + Functional description: + Returns the prediction mode of a macroblock type + +------------------------------------------------------------------------------*/ + +mbPartPredMode_e h264bsdMbPartPredMode(mbType_e mbType) +{ + +/* Variables */ + + +/* Code */ + + ASSERT(mbType <= 31); + + if ((mbType <= P_8x8ref0)) + return(PRED_MODE_INTER); + else if (mbType == I_4x4) + return(PRED_MODE_INTRA4x4); + else + return(PRED_MODE_INTRA16x16); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdNumMbPart + + Functional description: + Returns the amount of macroblock partitions in a macroblock type + +------------------------------------------------------------------------------*/ + +u32 h264bsdNumMbPart(mbType_e mbType) +{ + +/* Variables */ + + +/* Code */ + + ASSERT(h264bsdMbPartPredMode(mbType) == PRED_MODE_INTER); + + switch (mbType) + { + case P_L0_16x16: + case P_Skip: + return(1); + + case P_L0_L0_16x8: + case P_L0_L0_8x16: + return(2); + + /* P_8x8 or P_8x8ref0 */ + default: + return(4); + } + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdNumSubMbPart + + Functional description: + Returns the amount of sub-partitions in a sub-macroblock type + +------------------------------------------------------------------------------*/ + +u32 h264bsdNumSubMbPart(subMbType_e subMbType) +{ + +/* Variables */ + + +/* Code */ + + ASSERT(subMbType <= P_L0_4x4); + + switch (subMbType) + { + case P_L0_8x8: + return(1); + + case P_L0_8x4: + case P_L0_4x8: + return(2); + + /* P_L0_4x4 */ + default: + return(4); + } + +} + +/*------------------------------------------------------------------------------ + + Function: DecodeMbPred + + Functional description: + Parse macroblock prediction information from bit stream and store + in 'pMbPred'. + +------------------------------------------------------------------------------*/ + +u32 DecodeMbPred(strmData_t *pStrmData, mbPred_t *pMbPred, mbType_e mbType, + u32 numRefIdxActive) +{ + +/* Variables */ + + u32 tmp, i, j, value; + i32 itmp; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(pMbPred); + + switch (h264bsdMbPartPredMode(mbType)) + { + case PRED_MODE_INTER: /* PRED_MODE_INTER */ + if (numRefIdxActive > 1) + { + for (i = h264bsdNumMbPart(mbType), j = 0; i--; j++) + { + tmp = h264bsdDecodeExpGolombTruncated(pStrmData, &value, + (u32)(numRefIdxActive > 2)); + if (tmp != HANTRO_OK || value >= numRefIdxActive) + return(HANTRO_NOK); + + pMbPred->refIdxL0[j] = value; + } + } else { + PROFILE_START(PS_H264_LAYER_CLEAR); + for (i=0; i<4; i++) + pMbPred->refIdxL0[i] = 0; + PROFILE_STOP(PS_H264_LAYER_CLEAR); + } + + for (i = h264bsdNumMbPart(mbType), j = 0; i--; j++) + { + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &itmp); + if (tmp != HANTRO_OK) + return(tmp); + pMbPred->mvdL0[j].hor = (i16)itmp; + + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &itmp); + if (tmp != HANTRO_OK) + return(tmp); + pMbPred->mvdL0[j].ver = (i16)itmp; + } + break; + + case PRED_MODE_INTRA4x4: + for (itmp = 0, i = 0; itmp < 2; itmp++) + { + value = h264bsdShowBits32(pStrmData); + tmp = 0; + for (j = 8; j--; i++) + { + pMbPred->prevIntra4x4PredModeFlag[i] = + value & 0x80000000 ? HANTRO_TRUE : HANTRO_FALSE; + value <<= 1; + if (!pMbPred->prevIntra4x4PredModeFlag[i]) + { + pMbPred->remIntra4x4PredMode[i] = value>>29; + value <<= 3; + tmp++; + } + } + if (h264bsdFlushBits(pStrmData, 8 + 3*tmp) == END_OF_STREAM) + return(HANTRO_NOK); + } + /* fall-through */ + + case PRED_MODE_INTRA16x16: + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK || value > 3) + return(HANTRO_NOK); + pMbPred->intraChromaPredMode = value; + break; + } + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: DecodeSubMbPred + + Functional description: + Parse sub-macroblock prediction information from bit stream and + store in 'pMbPred'. + +------------------------------------------------------------------------------*/ + +u32 DecodeSubMbPred(strmData_t *pStrmData, subMbPred_t *pSubMbPred, + mbType_e mbType, u32 numRefIdxActive) +{ + +/* Variables */ + + u32 tmp, i, j, value; + i32 itmp; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(pSubMbPred); + ASSERT(h264bsdMbPartPredMode(mbType) == PRED_MODE_INTER); + + for (i = 0; i < 4; i++) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK || value > 3) + return(HANTRO_NOK); + pSubMbPred->subMbType[i] = (subMbType_e)value; + } + + if ( (numRefIdxActive > 1) && (mbType != P_8x8ref0) ) + { + for (i = 0; i < 4; i++) + { + tmp = h264bsdDecodeExpGolombTruncated(pStrmData, &value, + (u32)(numRefIdxActive > 2)); + if (tmp != HANTRO_OK || value >= numRefIdxActive) + return(HANTRO_NOK); + pSubMbPred->refIdxL0[i] = value; + } + } else { + PROFILE_START(PS_H264_LAYER_CLEAR); + for (i=0; i<4; i++) + pSubMbPred->refIdxL0[i] = 0; + PROFILE_STOP(PS_H264_LAYER_CLEAR); + } + + for (i = 0; i < 4; i++) + { + j = 0; + for (value = h264bsdNumSubMbPart(pSubMbPred->subMbType[i]); + value--; j++) + { + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &itmp); + if (tmp != HANTRO_OK) + return(tmp); + pSubMbPred->mvdL0[i][j].hor = (i16)itmp; + + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &itmp); + if (tmp != HANTRO_OK) + return(tmp); + pSubMbPred->mvdL0[i][j].ver = (i16)itmp; + } + } + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: DecodeResidual + + Functional description: + Parse residual information from bit stream and store in 'pResidual'. + +------------------------------------------------------------------------------*/ + + +/* + * Y Cb Cr + * + * 24 25 26 27 32 33 36 37 + * ----------- ----- -- -- + * 28| 0 1 4 5 34|16 17 38|20 21 + * 29| 2 3 6 7 35|18 19 39|22 23 + * 30| 8 9 12 13 + * 31|10 11 14 15 + * + */ + +// To determine NC, we need to average the totalCoeffs of +// the left and upper block. This table stores the direct indices +// for the left/upper block within the linear totalCoeffTable. +static const u8 ncLookupTable[24][2] = { + { 28, 24 }, // 0 + { 0, 25 }, // 1 + { 29, 0 }, // 2 + { 2, 1 }, // 3 + { 1, 26 }, // 4 + { 4, 27 }, // 5 + { 3, 4 }, // 6 + { 6, 5 }, // 7 + { 30, 2 }, // 8 + { 8, 3 }, // 9 + { 31, 8 }, // 10 + { 10, 9 }, // 11 + { 9, 6 }, // 12 + { 12, 7 }, // 13 + { 11, 12 }, // 14 + { 14, 13 }, // 15 + { 34, 32 }, // 16 + { 16, 33 }, // 17 + { 35, 16 }, // 18 + { 18, 17 }, // 19 + { 38, 36 }, // 20 + { 20, 37 }, // 21 + { 39, 20 }, // 22 + { 22, 21 }, // 23 +}; + +static u8 CalcNC(int block, const u8 *totalCoeffTable) { + const u8 *k = ncLookupTable[block]; + + u8 ncLeft = totalCoeffTable[k[0]]; + u8 ncUp = totalCoeffTable[k[1]]; + + int hasLeft = 0; + u8 n = 0; + if ((i8)ncLeft >= 0) { + n += ncLeft; + hasLeft = 1; + } + if ((i8)ncUp >= 0) { + n += ncUp; + if (hasLeft) { + n = (n+1)>>1; + } + } + return n; +} + +u32 DecodeResidual(strmData_t *pStrmData, residual_t *pResidual, + mbStorage_t *pMb, mbType_e mbType, u32 codedBlockPattern) +{ + u8 totalCoeffTable[40]; + u8 *pPosCoefBuf = pResidual->posCoefBuf; + u8 *totalCoeffLeft = NULL, *totalCoeffUp = NULL; + u32 blockCoded; + u8 lumaDC = 0, chromaDCR = 0, chromaDCB = 0; + int is16x16 = (h264bsdMbPartPredMode(mbType) == PRED_MODE_INTRA16x16); + int tmp; + + if (h264bsdIsNeighbourAvailable(pMb, pMb->mbA)) + totalCoeffLeft = pMb->mbA->totalCoeff; + if (h264bsdIsNeighbourAvailable(pMb, pMb->mbB)) + totalCoeffUp = pMb->mbB->totalCoeff; + + for (int i=0;i<24;i++) + totalCoeffTable[i] = 0; + for (int i=24;i<40;i++) + totalCoeffTable[i] = 0xFF; + if (totalCoeffUp) { + totalCoeffTable[24] = totalCoeffUp[10]; + totalCoeffTable[25] = totalCoeffUp[11]; + totalCoeffTable[26] = totalCoeffUp[14]; + totalCoeffTable[27] = totalCoeffUp[15]; + totalCoeffTable[32] = totalCoeffUp[18]; + totalCoeffTable[33] = totalCoeffUp[19]; + totalCoeffTable[36] = totalCoeffUp[22]; + totalCoeffTable[37] = totalCoeffUp[23]; + } + if (totalCoeffLeft) { + totalCoeffTable[28] = totalCoeffLeft[5]; + totalCoeffTable[29] = totalCoeffLeft[7]; + totalCoeffTable[30] = totalCoeffLeft[13]; + totalCoeffTable[31] = totalCoeffLeft[15]; + totalCoeffTable[34] = totalCoeffLeft[17]; + totalCoeffTable[35] = totalCoeffLeft[19]; + totalCoeffTable[38] = totalCoeffLeft[21]; + totalCoeffTable[39] = totalCoeffLeft[23]; + } + + // Decode residual info for luma DC (only present in 16x16 blocks) + if (is16x16) { + tmp = h264bsdDecodeResidualBlockCavlc( + pStrmData, &pPosCoefBuf, + CalcNC(0, totalCoeffTable), 16); + if ((tmp & 0xF) != HANTRO_OK) + return(tmp); + lumaDC = (tmp >> 4) & 0xFF; + } + + // Decode residual info for luma blocks. Notice that each block + // contains up to 15 or 16 coefficients (depending if lumaDC is + // present or not). + int block = 0; + for (int i=0;i<4;i++) { + blockCoded = codedBlockPattern & 0x1; + codedBlockPattern >>= 1; + if (blockCoded) { + for (int j=0;j<4;j++) { + tmp = h264bsdDecodeResidualBlockCavlc( + pStrmData, &pPosCoefBuf, + CalcNC(block, totalCoeffTable), is16x16 ? 15 : 16); + if ((tmp & 0xF) != HANTRO_OK) + return(tmp); + totalCoeffTable[block] = (tmp >> 4) & 0xFF; + block++; + } + } else { + block += 4; + } + } + + // Decode residual info for chroma DC + blockCoded = codedBlockPattern & 0x3; + if (blockCoded) + { + tmp = h264bsdDecodeResidualBlockCavlc( + pStrmData, &pPosCoefBuf, + -1, 4); + if ((tmp & 0xF) != HANTRO_OK) + return(tmp); + chromaDCR = (tmp >> 4) & 0xFF; + tmp = h264bsdDecodeResidualBlockCavlc( + pStrmData, + &pPosCoefBuf, + -1, 4); + if ((tmp & 0xF) != HANTRO_OK) + return(tmp); + chromaDCB = (tmp >> 4) & 0xFF; + } + + // Decode residual info for chroma + blockCoded = codedBlockPattern & 0x2; + if (blockCoded) + { + for (int i=0;i<8;i++) { + tmp = h264bsdDecodeResidualBlockCavlc( + pStrmData, &pPosCoefBuf, + CalcNC(block, totalCoeffTable), 15); + if ((tmp & 0xF) != HANTRO_OK) + return(tmp); + totalCoeffTable[block] = (tmp >> 4) & 0xFF; + block++; + } + } + + for (int i=0;i<24;i++) + pResidual->totalCoeff[i] = totalCoeffTable[i]; + pResidual->totalCoeff[24] = lumaDC; + pResidual->totalCoeff[25] = chromaDCR; + pResidual->totalCoeff[26] = chromaDCB; + return (HANTRO_OK); +} + +/*------------------------------------------------------------------------------ + + Function: DetermineNc + + Functional description: + Returns the nC of a block. + +------------------------------------------------------------------------------*/ +u32 DetermineNc(mbStorage_t *pMb, u32 blockIndex, u8 *pTotalCoeff) +{ +/*lint -e702 */ +/* Variables */ + + u32 tmp; + i32 n; + const neighbour_t *neighbourA, *neighbourB; + u8 neighbourAindex, neighbourBindex; + +/* Code */ + + ASSERT(blockIndex < 24); + + /* if neighbour block belongs to current macroblock totalCoeff array + * mbStorage has not been set/updated yet -> use pTotalCoeff */ + neighbourA = h264bsdNeighbour4x4BlockA(blockIndex); + neighbourB = h264bsdNeighbour4x4BlockB(blockIndex); + neighbourAindex = neighbourA->index; + neighbourBindex = neighbourB->index; + if (neighbourA->mb == MB_CURR && neighbourB->mb == MB_CURR) + { + n = (pTotalCoeff[neighbourAindex] + + pTotalCoeff[neighbourBindex] + 1)>>1; + } + else if (neighbourA->mb == MB_CURR) + { + n = pTotalCoeff[neighbourAindex]; + if (h264bsdIsNeighbourAvailable(pMb, pMb->mbB)) + { + n = (n + pMb->mbB->totalCoeff[neighbourBindex] + 1) >> 1; + } + } + else if (neighbourB->mb == MB_CURR) + { + n = pTotalCoeff[neighbourBindex]; + if (h264bsdIsNeighbourAvailable(pMb, pMb->mbA)) + { + n = (n + pMb->mbA->totalCoeff[neighbourAindex] + 1) >> 1; + } + } + else + { + n = tmp = 0; + if (h264bsdIsNeighbourAvailable(pMb, pMb->mbA)) + { + n = pMb->mbA->totalCoeff[neighbourAindex]; + tmp = 1; + } + if (h264bsdIsNeighbourAvailable(pMb, pMb->mbB)) + { + if (tmp) + n = (n + pMb->mbB->totalCoeff[neighbourBindex] + 1) >> 1; + else + n = pMb->mbB->totalCoeff[neighbourBindex]; + } + } + return((u32)n); +/*lint +e702 */ +} + +/*------------------------------------------------------------------------------ + + Function: CbpIntra16x16 + + Functional description: + Returns the coded block pattern for intra 16x16 macroblock. + +------------------------------------------------------------------------------*/ + +u32 CbpIntra16x16(mbType_e mbType) +{ + +/* Variables */ + + u32 cbp; + u32 tmp; + +/* Code */ + + ASSERT(mbType >= I_16x16_0_0_0 && mbType <= I_16x16_3_2_1); + + if (mbType >= I_16x16_0_0_1) + cbp = 15; + else + cbp = 0; + + /* tmp is 0 for I_16x16_0_0_0 mb type */ + /* ignore lint warning on arithmetic on enum's */ + tmp = /*lint -e(656)*/(mbType - I_16x16_0_0_0) >> 2; + if (tmp > 2) + tmp -= 3; + + cbp += tmp << 4; + + return(cbp); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdPredModeIntra16x16 + + Functional description: + Returns the prediction mode for intra 16x16 macroblock. + +------------------------------------------------------------------------------*/ + +u32 h264bsdPredModeIntra16x16(mbType_e mbType) +{ + +/* Variables */ + + u32 tmp; + +/* Code */ + + ASSERT(mbType >= I_16x16_0_0_0 && mbType <= I_16x16_3_2_1); + + /* tmp is 0 for I_16x16_0_0_0 mb type */ + /* ignore lint warning on arithmetic on enum's */ + tmp = /*lint -e(656)*/(mbType - I_16x16_0_0_0); + + return(tmp & 0x3); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdDecodeMacroblock + + Functional description: + Decode one macroblock and write into output image. + + Inputs: + pMb pointer to macroblock specific information + mbLayer pointer to current macroblock data from stream + currImage pointer to output image + dpb pointer to decoded picture buffer + qpY pointer to slice QP + mbNum current macroblock number + constrainedIntraPred flag specifying if neighbouring inter + macroblocks are used in intra prediction + + Outputs: + pMb structure is updated with current macroblock + currImage decoded macroblock is written into output image + + Returns: + HANTRO_OK success + HANTRO_NOK error in macroblock decoding + +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeMacroblock(mbStorage_t *pMb, macroblockLayer_t *pMbLayer, + image_t *currImage, dpbStorage_t *dpb, i32 *qpY, u32 mbNum, + u32 constrainedIntraPredFlag, const sliceHeader_t *pSliceHeader) +{ + +/* Variables */ + + u32 tmp; + mbType_e mbType; + const u8 *pSrc; +/* Code */ + + ASSERT(pMb); + ASSERT(pMbLayer); + ASSERT(currImage); + ASSERT(qpY && *qpY < 52); + ASSERT(mbNum < currImage->width*currImage->height); + + mbType = pMbLayer->mbType; + pMb->mbType = mbType; + +#ifndef OPTIMIZE_NO_DECODED_FLAG + pMb->decoded++; +#endif + + h264bsdSetCurrImageMbPointers(currImage, mbNum); + + if (mbType == I_PCM) + { + ASSERT(0); // Check if PCM is really used +#if 0 + // TODO: this is rarely(never?) used so for now it's not implemented. + // The below code should be reworked to write directly into + // currImage buffers. + u8 *pData = (u8*)data; + u8 *tot = pMb->totalCoeff; + u8 *lev = pMbLayer->residual.posCoefBuf; + + pMb->qpY = 0; + +#ifndef OPTIMIZE_NO_DECODED_FLAG + /* if decoded flag > 1 -> mb has already been successfully decoded and + * written to output -> do not write again */ + if (pMb->decoded > 1) + { + for (i = 24; i--;) + *tot++ = 16; + return HANTRO_OK; + } +#endif + + for (i = 24; i--;) + { + *tot++ = 16; + for (tmp = 16; tmp--;) + *pData++ = (u8)(*lev++); + } + h264bsdWriteMacroblock(currImage, (u8*)data, 1); +#endif + return(HANTRO_OK); + } + else + { + if (h264bsdMbPartPredMode(mbType) == PRED_MODE_INTER) + { + tmp = h264bsdInterPrediction(pMb, pMbLayer, dpb, mbNum, currImage, + pSliceHeader); + ASSERT(tmp == HANTRO_OK); + if (tmp != HANTRO_OK) return (tmp); + } + + if (mbType != P_Skip) + { + H264SwDecMemcpy(pMb->totalCoeff, + pMbLayer->residual.totalCoeff, + 27*sizeof(*pMb->totalCoeff)); + pMb->totalCoeffMask = h264bsdTotalCoeffMask(pMb->totalCoeff); + + /* update qpY */ + if (pMbLayer->mbQpDelta) + { + *qpY = *qpY + pMbLayer->mbQpDelta; + if (*qpY < 0) *qpY += 52; + else if (*qpY >= 52) *qpY -= 52; + } + pMb->qpY = (u32)*qpY; + + pSrc = pMbLayer->residual.posCoefBuf; + + if (h264bsdMbPartPredMode(mbType) == PRED_MODE_INTER) + { + PROFILE_START(PS_H264_RESIDUAL_LUMA); + tmp = HIGHFUNC_ProcessLumaInterResidual( + &pSrc, currImage->luma, currImage->luma, + currImage->width*16, currImage->width*16, *qpY, pMb->totalCoeff); + if (tmp != HANTRO_OK) { + #ifdef H264BSD_N64 + PROFILE_STOP(PS_H264_RESIDUAL_LUMA); + #endif + return (tmp); + } + PROFILE_STOP(PS_H264_RESIDUAL_LUMA); + } + else if (h264bsdMbPartPredMode(mbType) == PRED_MODE_INTRA4x4) + { + tmp = ProcessIntra4x4Residual(pMb, + constrainedIntraPredFlag, + pMbLayer, + &pSrc, + currImage); + if (tmp != HANTRO_OK) { + return (tmp); + } + } + else if (h264bsdMbPartPredMode(mbType) == PRED_MODE_INTRA16x16) + { + tmp = ProcessIntra16x16Residual(pMb, + constrainedIntraPredFlag, + pMbLayer->mbPred.intraChromaPredMode, + &pSrc, + currImage); + if (tmp != HANTRO_OK) { + return (tmp); + } + } + + PROFILE_START(PS_H264_RESIDUAL_CHROMA); + tmp = ProcessChromaResidual(pMb, &pSrc, currImage); + PROFILE_STOP(PS_H264_RESIDUAL_CHROMA); + + if (tmp != HANTRO_OK) + return (tmp); + } + else + { + H264SwDecMemset(pMb->totalCoeff, 0, 27*sizeof(*pMb->totalCoeff)); + pMb->totalCoeffMask = 0; + pMb->qpY = (u32)*qpY; + } + + /* if decoded flag > 1 -> mb has already been successfully decoded and + * written to output -> do not write again */ + // TODO(rasky): this used to guard a call to WriteMacroblock, but + // we now directly decode to the output buffer so there's no more + // decoded to skip. I'm not sure what this flag means, but it's useless now. +#ifndef OPTIMIZE_NO_DECODED_FLAG + if (pMb->decoded > 1) + return HANTRO_OK; +#endif + + #ifdef H264BSD_N64 + rsph264_queue_set_packed_delta_buffer_if_changed(0, NULL); + #endif + } + + return HANTRO_OK; +} + + +/*------------------------------------------------------------------------------ + + Function: ProcessChromaResidual + + Functional description: + Process the residual data of chroma with + inverse quantization and inverse transform. + +------------------------------------------------------------------------------*/ +u32 ProcessChromaResidual(mbStorage_t *pMb, const u8 **pSrc, image_t *image) +{ + u32 chromaQp; + // OMXResult result; + + /* chroma DC processing. First chroma dc block is block with index 25 */ + chromaQp = + h264bsdQpC[CLIP3(0, 51, (i32)pMb->qpY + pMb->chromaQpIndexOffset)]; + + OMXResult result; + result = HIGHFUNC_ProcessChromaResidual(pSrc, + image->cb, image->cr, image->width*8, image->width*8, + chromaQp, pMb->totalCoeff); + if (result != OMX_Sts_NoErr) + return (HANTRO_NOK); + return(HANTRO_OK); +} + +/*------------------------------------------------------------------------------ + + Function: ProcessIntra16x16Residual + + Functional description: + Process the residual data of luma with + inverse quantization and inverse transform. + +------------------------------------------------------------------------------*/ +u32 ProcessIntra16x16Residual(mbStorage_t *pMb, + u32 constrainedIntraPred, + u32 intraChromaPredMode, + const u8** pCoeff, + image_t *image) +{ + u32 availableA, availableB, availableD; + OMXResult result; + + ASSERT(pMb); + ASSERT(pCoeff); + ASSERT(image); + ASSERT(h264bsdPredModeIntra16x16(pMb->mbType) < 4); + + availableA = h264bsdIsNeighbourAvailable(pMb, pMb->mbA); + if (availableA && constrainedIntraPred && + (h264bsdMbPartPredMode(pMb->mbA->mbType) == PRED_MODE_INTER)) + availableA = HANTRO_FALSE; + availableB = h264bsdIsNeighbourAvailable(pMb, pMb->mbB); + if (availableB && constrainedIntraPred && + (h264bsdMbPartPredMode(pMb->mbB->mbType) == PRED_MODE_INTER)) + availableB = HANTRO_FALSE; + availableD = h264bsdIsNeighbourAvailable(pMb, pMb->mbD); + if (availableD && constrainedIntraPred && + (h264bsdMbPartPredMode(pMb->mbD->mbType) == PRED_MODE_INTER)) + availableD = HANTRO_FALSE; + + PROFILE_START(PS_H264_INTRAPRED_16X16); + result = HIGHFUNC_ProcessLumaIntra16x16Residual( + image->luma, image->luma, image->width*16, image->width*16, + pCoeff, pMb->totalCoeff, + (OMXVCM4P10Intra16x16PredMode)h264bsdPredModeIntra16x16(pMb->mbType), + (i32)(availableB + (availableA<<1) + (availableD<<5)), + pMb->qpY); + if (result != OMX_Sts_NoErr) + return (HANTRO_NOK); + + if (h264bsdIntraChromaPrediction(pMb, + image, + intraChromaPredMode, + constrainedIntraPred) != HANTRO_OK) + return(HANTRO_NOK); + PROFILE_STOP(PS_H264_INTRAPRED_16X16); + + return HANTRO_OK; +} + +/*------------------------------------------------------------------------------ + + Function: ProcessIntra4x4Residual + + Functional description: + Process the residual data of luma with + inverse quantization and inverse transform. + +------------------------------------------------------------------------------*/ +u32 ProcessIntra4x4Residual(mbStorage_t *pMb, + u32 constrainedIntraPred, + macroblockLayer_t *mbLayer, + const u8 **pSrc, + image_t *image) +{ + PROFILE_START(PS_H264_INTRAPRED_4X4); + if (h264bsdIntra4x4PredictionAndTransformAll(pMb, mbLayer, + constrainedIntraPred, image, pSrc) != HANTRO_OK) + return (HANTRO_NOK); + + if (h264bsdIntraChromaPrediction(pMb, + image, + mbLayer->mbPred.intraChromaPredMode, + constrainedIntraPred) != HANTRO_OK) + return(HANTRO_NOK); + PROFILE_STOP(PS_H264_INTRAPRED_4X4); + + return HANTRO_OK; +} + + +/*------------------------------------------------------------------------------ + + Function: h264bsdSubMbPartMode + + Functional description: + Returns the macroblock's sub-partition mode. + +------------------------------------------------------------------------------*/ + +subMbPartMode_e h264bsdSubMbPartMode(subMbType_e subMbType) +{ + +/* Variables */ + + +/* Code */ + + ASSERT(subMbType < 4); + + return((subMbPartMode_e)subMbType); + +} + + diff --git a/src/video/h264_decoder/h264bsd_macroblock_layer.h b/src/video/h264_decoder/h264bsd_macroblock_layer.h new file mode 100644 index 000000000..f3684f40f --- /dev/null +++ b/src/video/h264_decoder/h264bsd_macroblock_layer.h @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_MACROBLOCK_LAYER_H +#define H264SWDEC_MACROBLOCK_LAYER_H + +#include "opttarget.h" + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_stream.h" +#include "h264bsd_image.h" +#include "h264bsd_dpb.h" +#include "h264bsd_slice_header.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/* Macro to determine if a mb is an intra mb */ +#define IS_INTRA_MB(a) ((a).mbType > 5) + +/* Macro to determine if a mb is an I_PCM mb */ +#define IS_I_PCM_MB(a) ((a).mbType == 31) + +typedef enum { + P_Skip = 0, + P_L0_16x16 = 1, + P_L0_L0_16x8 = 2, + P_L0_L0_8x16 = 3, + P_8x8 = 4, + P_8x8ref0 = 5, + I_4x4 = 6, + I_16x16_0_0_0 = 7, + I_16x16_1_0_0 = 8, + I_16x16_2_0_0 = 9, + I_16x16_3_0_0 = 10, + I_16x16_0_1_0 = 11, + I_16x16_1_1_0 = 12, + I_16x16_2_1_0 = 13, + I_16x16_3_1_0 = 14, + I_16x16_0_2_0 = 15, + I_16x16_1_2_0 = 16, + I_16x16_2_2_0 = 17, + I_16x16_3_2_0 = 18, + I_16x16_0_0_1 = 19, + I_16x16_1_0_1 = 20, + I_16x16_2_0_1 = 21, + I_16x16_3_0_1 = 22, + I_16x16_0_1_1 = 23, + I_16x16_1_1_1 = 24, + I_16x16_2_1_1 = 25, + I_16x16_3_1_1 = 26, + I_16x16_0_2_1 = 27, + I_16x16_1_2_1 = 28, + I_16x16_2_2_1 = 29, + I_16x16_3_2_1 = 30, + I_PCM = 31 +} mbType_e; + +typedef enum { + P_L0_8x8 = 0, + P_L0_8x4 = 1, + P_L0_4x8 = 2, + P_L0_4x4 = 3 +} subMbType_e; + +typedef enum { + MB_P_16x16 = 0, + MB_P_16x8, + MB_P_8x16, + MB_P_8x8 +} mbPartMode_e; + +typedef enum { + MB_SP_8x8 = 0, + MB_SP_8x4, + MB_SP_4x8, + MB_SP_4x4 +} subMbPartMode_e; + +typedef enum { + PRED_MODE_INTRA4x4 = 0, + PRED_MODE_INTRA16x16 , + PRED_MODE_INTER +} mbPartPredMode_e; + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +typedef struct +{ + /* MvPrediction16x16 assumes that MVs are 16bits */ + i16 hor; + i16 ver; +} mv_t; + +typedef struct +{ + u32 prevIntra4x4PredModeFlag[16]; + u32 remIntra4x4PredMode[16]; + u32 intraChromaPredMode; + u32 refIdxL0[4]; + mv_t mvdL0[4]; +} mbPred_t; + +typedef struct +{ + subMbType_e subMbType[4]; + u32 refIdxL0[4]; + mv_t mvdL0[4][4]; +} subMbPred_t; + +typedef struct +{ + u8 posCoefBuf[27*16*3]; + u8 totalCoeff[27]; +} residual_t; + +typedef struct __attribute__((aligned(16))) +{ + mbType_e mbType; + u32 codedBlockPattern; + i32 mbQpDelta; + mbPred_t mbPred; + subMbPred_t subMbPred; + residual_t residual; +} macroblockLayer_t; + +typedef struct mbStorage +{ + mbType_e mbType; + u32 sliceId; + u32 disableDeblockingFilterIdc; + i32 filterOffsetA; + i32 filterOffsetB; + u32 qpY; + i32 chromaQpIndexOffset; + u8 totalCoeff[27]; + u32 totalCoeffMask; + u8 intra4x4PredMode[16]; + u32 refPic[4]; + u8* refAddr[4]; + mv_t mv[16]; +#ifndef OPTIMIZE_NO_DECODED_FLAG + u32 decoded; +#endif + struct mbStorage *mbA; + struct mbStorage *mbB; + struct mbStorage *mbC; + struct mbStorage *mbD; +} mbStorage_t; + +static inline u32 h264bsdTotalCoeffMask(const u8 *totalCoeff) +{ + u32 mask = 0; + for (u32 i = 0; i < 27; i++) { + if (totalCoeff[i]) { + mask |= 1u << i; + } + } + return mask; +} + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeMacroblockLayer(strmData_t *pStrmData, + macroblockLayer_t *pMbLayer, mbStorage_t *pMb, u32 sliceType, + u32 numRefIdxActive); + +u32 h264bsdNumMbPart(mbType_e mbType); +u32 h264bsdNumSubMbPart(subMbType_e subMbType); + +subMbPartMode_e h264bsdSubMbPartMode(subMbType_e subMbType); + +u32 h264bsdDecodeMacroblock(mbStorage_t *pMb, macroblockLayer_t *pMbLayer, + image_t *currImage, dpbStorage_t *dpb, i32 *qpY, u32 mbNum, + u32 constrainedIntraPredFlag, const sliceHeader_t *pSliceHeader); + +u32 h264bsdPredModeIntra16x16(mbType_e mbType); + +mbPartPredMode_e h264bsdMbPartPredMode(mbType_e mbType); +#ifdef H264DEC_NEON +u32 h264bsdClearMbLayer(macroblockLayer_t *pMbLayer, u32 size); +#endif + +#endif /* #ifdef H264SWDEC_MACROBLOCK_LAYER_H */ + + diff --git a/src/video/h264_decoder/h264bsd_nal_unit.c b/src/video/h264_decoder/h264bsd_nal_unit.c new file mode 100644 index 000000000..e89cc5f2c --- /dev/null +++ b/src/video/h264_decoder/h264bsd_nal_unit.c @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdDecodeNalUnit + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_nal_unit.h" +#include "h264bsd_util.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + + Function name: h264bsdDecodeNalUnit + + Functional description: + Decode NAL unit header information + + Inputs: + pStrmData pointer to stream data structure + + Outputs: + pNalUnit NAL unit header information is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK invalid NAL unit header information + +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeNalUnit(strmData_t *pStrmData, nalUnit_t *pNalUnit) +{ + +/* Variables */ + + u32 tmp; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(pNalUnit); + #ifndef H264BSD_N64 + ASSERT(pStrmData->bitPosInWord == 0); + #else + ASSERT(STRM_CURR_BITOFF(pStrmData) == 0); + #endif + + /* forbidden_zero_bit (not checked to be zero, errors ignored) */ + tmp = h264bsdGetBits(pStrmData, 1); + /* Assuming that NAL unit starts from byte boundary ­> don't have to check + * following 7 bits for END_OF_STREAM */ + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + + tmp = h264bsdGetBits(pStrmData, 2); + pNalUnit->nalRefIdc = tmp; + + tmp = h264bsdGetBits(pStrmData, 5); + pNalUnit->nalUnitType = (nalUnitType_e)tmp; + + /* data partitioning NAL units not supported */ + if ( (tmp == 2) || (tmp == 3) || (tmp == 4) ) + { + return(HANTRO_NOK); + } + + /* nal_ref_idc shall not be zero for these nal_unit_types */ + if ( ( (tmp == NAL_SEQ_PARAM_SET) || (tmp == NAL_PIC_PARAM_SET) || + (tmp == NAL_CODED_SLICE_IDR) ) && (pNalUnit->nalRefIdc == 0) ) + { + return(HANTRO_NOK); + } + /* nal_ref_idc shall be zero for these nal_unit_types */ + else if ( ( (tmp == NAL_SEI) || (tmp == NAL_ACCESS_UNIT_DELIMITER) || + (tmp == NAL_END_OF_SEQUENCE) || (tmp == NAL_END_OF_STREAM) || + (tmp == NAL_FILLER_DATA) ) && (pNalUnit->nalRefIdc != 0) ) + { + return(HANTRO_NOK); + } + + return(HANTRO_OK); + +} + diff --git a/src/video/h264_decoder/h264bsd_nal_unit.h b/src/video/h264_decoder/h264bsd_nal_unit.h new file mode 100644 index 000000000..38957bf73 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_nal_unit.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_NAL_UNIT_H +#define H264SWDEC_NAL_UNIT_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_stream.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/* macro to determine if NAL unit pointed by pNalUnit contains an IDR slice */ +#define IS_IDR_NAL_UNIT(pNalUnit) \ + ((pNalUnit)->nalUnitType == NAL_CODED_SLICE_IDR) + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +typedef enum { + NAL_CODED_SLICE = 1, + NAL_CODED_SLICE_IDR = 5, + NAL_SEI = 6, + NAL_SEQ_PARAM_SET = 7, + NAL_PIC_PARAM_SET = 8, + NAL_ACCESS_UNIT_DELIMITER = 9, + NAL_END_OF_SEQUENCE = 10, + NAL_END_OF_STREAM = 11, + NAL_FILLER_DATA = 12, + NAL_MAX_TYPE_VALUE = 31 +} nalUnitType_e; + +typedef struct +{ + nalUnitType_e nalUnitType; + u32 nalRefIdc; +} nalUnit_t; + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeNalUnit(strmData_t *pStrmData, nalUnit_t *pNalUnit); + +#endif /* #ifdef H264SWDEC_NAL_UNIT_H */ + diff --git a/src/video/h264_decoder/h264bsd_neighbour.c b/src/video/h264_decoder/h264bsd_neighbour.c new file mode 100644 index 000000000..ce5eeff76 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_neighbour.c @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdInitMbNeighbours + h264bsdGetNeighbourMb + h264bsdNeighbour4x4BlockA + h264bsdNeighbour4x4BlockB + h264bsdNeighbour4x4BlockC + h264bsdNeighbour4x4BlockD + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_neighbour.h" +#include "h264bsd_util.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/* Following four tables indicate neighbours of each block of a macroblock. + * First 16 values are for luma blocks, next 4 values for Cb and last 4 + * values for Cr. Elements of the table indicate to which macroblock the + * neighbour block belongs and the index of the neighbour block in question. + * Indexing of the blocks goes as follows + * + * Y Cb Cr + * 0 1 4 5 16 17 20 21 + * 2 3 6 7 18 19 22 23 + * 8 9 12 13 + * 10 11 14 15 + */ + +/* left neighbour for each block */ +static const neighbour_t N_A_4x4B[24] = { + {MB_A,5}, {MB_CURR,0}, {MB_A,7}, {MB_CURR,2}, + {MB_CURR,1}, {MB_CURR,4}, {MB_CURR,3}, {MB_CURR,6}, + {MB_A,13}, {MB_CURR,8}, {MB_A,15}, {MB_CURR,10}, + {MB_CURR,9}, {MB_CURR,12},{MB_CURR,11},{MB_CURR,14}, + {MB_A,17}, {MB_CURR,16},{MB_A,19}, {MB_CURR,18}, + {MB_A,21}, {MB_CURR,20},{MB_A,23}, {MB_CURR,22} }; + +/* above neighbour for each block */ +static const neighbour_t N_B_4x4B[24] = { + {MB_B,10}, {MB_B,11}, {MB_CURR,0}, {MB_CURR,1}, + {MB_B,14}, {MB_B,15}, {MB_CURR,4}, {MB_CURR,5}, + {MB_CURR,2}, {MB_CURR,3}, {MB_CURR,8}, {MB_CURR,9}, + {MB_CURR,6}, {MB_CURR,7}, {MB_CURR,12},{MB_CURR,13}, + {MB_B,18}, {MB_B,19}, {MB_CURR,16},{MB_CURR,17}, + {MB_B,22}, {MB_B,23}, {MB_CURR,20},{MB_CURR,21} }; + +/* above-right neighbour for each block */ +static const neighbour_t N_C_4x4B[24] = { + {MB_B,11}, {MB_B,14}, {MB_CURR,1}, {MB_NA,4}, + {MB_B,15}, {MB_C,10}, {MB_CURR,5}, {MB_NA,0}, + {MB_CURR,3}, {MB_CURR,6}, {MB_CURR,9}, {MB_NA,12}, + {MB_CURR,7}, {MB_NA,2}, {MB_CURR,13},{MB_NA,8}, + {MB_B,19}, {MB_C,18}, {MB_CURR,17},{MB_NA,16}, + {MB_B,23}, {MB_C,22}, {MB_CURR,21},{MB_NA,20} }; + +/* above-left neighbour for each block */ +static const neighbour_t N_D_4x4B[24] = { + {MB_D,15}, {MB_B,10}, {MB_A,5}, {MB_CURR,0}, + {MB_B,11}, {MB_B,14}, {MB_CURR,1}, {MB_CURR,4}, + {MB_A,7}, {MB_CURR,2}, {MB_A,13}, {MB_CURR,8}, + {MB_CURR,3}, {MB_CURR,6}, {MB_CURR,9}, {MB_CURR,12}, + {MB_D,19}, {MB_B,18}, {MB_A,17}, {MB_CURR,16}, + {MB_D,23}, {MB_B,22}, {MB_A,21}, {MB_CURR,20} }; + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + + Function: h264bsdInitMbNeighbours + + Functional description: + Initialize macroblock neighbours. Function sets neighbour + macroblock pointers in macroblock structures to point to + macroblocks on the left, above, above-right and above-left. + Pointers are set NULL if the neighbour does not fit into the + picture. + + Inputs: + picWidth width of the picture in macroblocks + picSizeInMbs no need to clarify + + Outputs: + pMbStorage neighbour pointers of each mbStorage structure + stored here + + Returns: + none + +------------------------------------------------------------------------------*/ + +void h264bsdInitMbNeighbours(mbStorage_t *pMbStorage, u32 picWidth, + u32 picSizeInMbs) +{ + +/* Variables */ + + u32 i, row, col; + +/* Code */ + + ASSERT(pMbStorage); + ASSERT(picWidth); + ASSERT(picWidth <= picSizeInMbs); + ASSERT(((picSizeInMbs / picWidth) * picWidth) == picSizeInMbs); + + row = col = 0; + + for (i = 0; i < picSizeInMbs; i++) + { + + if (col) + pMbStorage[i].mbA = pMbStorage + i - 1; + else + pMbStorage[i].mbA = NULL; + + if (row) + pMbStorage[i].mbB = pMbStorage + i - picWidth; + else + pMbStorage[i].mbB = NULL; + + if (row && (col < picWidth - 1)) + pMbStorage[i].mbC = pMbStorage + i - (picWidth - 1); + else + pMbStorage[i].mbC = NULL; + + if (row && col) + pMbStorage[i].mbD = pMbStorage + i - (picWidth + 1); + else + pMbStorage[i].mbD = NULL; + + col++; + if (col == picWidth) + { + col = 0; + row++; + } + } + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdGetNeighbourMb + + Functional description: + Get pointer to neighbour macroblock. + + Inputs: + pMb pointer to macroblock structure of the macroblock + whose neighbour is wanted + neighbour indicates which neighbour is wanted + + Outputs: + none + + Returns: + pointer to neighbour macroblock + NULL if not available + +------------------------------------------------------------------------------*/ + +mbStorage_t* h264bsdGetNeighbourMb(mbStorage_t *pMb, neighbourMb_e neighbour) +{ + +/* Variables */ + + +/* Code */ + + ASSERT((neighbour <= MB_CURR) || (neighbour == MB_NA)); + + if (neighbour == MB_A) + return(pMb->mbA); + else if (neighbour == MB_B) + return(pMb->mbB); + else if (neighbour == MB_C) + return(pMb->mbC); + else if (neighbour == MB_D) + return(pMb->mbD); + else if (neighbour == MB_CURR) + return(pMb); + else + return(NULL); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdNeighbour4x4BlockA + + Functional description: + Get left neighbour of the block. Function returns pointer to + the table defined in the beginning of the file. + + Inputs: + blockIndex indicates the block whose neighbours are wanted + + Outputs: + + Returns: + pointer to neighbour structure + +------------------------------------------------------------------------------*/ + +const neighbour_t* h264bsdNeighbour4x4BlockA(u32 blockIndex) +{ + +/* Variables */ + +/* Code */ + + ASSERT(blockIndex < 24); + + return(N_A_4x4B+blockIndex); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdNeighbour4x4BlockB + + Functional description: + Get above neighbour of the block. Function returns pointer to + the table defined in the beginning of the file. + + Inputs: + blockIndex indicates the block whose neighbours are wanted + + Outputs: + + Returns: + pointer to neighbour structure + +------------------------------------------------------------------------------*/ + +const neighbour_t* h264bsdNeighbour4x4BlockB(u32 blockIndex) +{ + +/* Variables */ + +/* Code */ + + ASSERT(blockIndex < 24); + + return(N_B_4x4B+blockIndex); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdNeighbour4x4BlockC + + Functional description: + Get above-right neighbour of the block. Function returns pointer + to the table defined in the beginning of the file. + + Inputs: + blockIndex indicates the block whose neighbours are wanted + + Outputs: + + Returns: + pointer to neighbour structure + +------------------------------------------------------------------------------*/ + +const neighbour_t* h264bsdNeighbour4x4BlockC(u32 blockIndex) +{ + +/* Variables */ + +/* Code */ + + ASSERT(blockIndex < 24); + + return(N_C_4x4B+blockIndex); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdNeighbour4x4BlockD + + Functional description: + Get above-left neighbour of the block. Function returns pointer to + the table defined in the beginning of the file. + + Inputs: + blockIndex indicates the block whose neighbours are wanted + + Outputs: + + Returns: + pointer to neighbour structure + +------------------------------------------------------------------------------*/ + +const neighbour_t* h264bsdNeighbour4x4BlockD(u32 blockIndex) +{ + +/* Variables */ + +/* Code */ + + ASSERT(blockIndex < 24); + + return(N_D_4x4B+blockIndex); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdIsNeighbourAvailable + + Functional description: + Check if neighbour macroblock is available. Neighbour macroblock + is considered available if it is within the picture and belongs + to the same slice as the current macroblock. + + Inputs: + pMb pointer to the current macroblock + pNeighbour pointer to the neighbour macroblock + + Outputs: + none + + Returns: + TRUE neighbour is available + FALSE neighbour is not available + +------------------------------------------------------------------------------*/ + +u32 h264bsdIsNeighbourAvailable(mbStorage_t *pMb, mbStorage_t *pNeighbour) +{ + +/* Variables */ + +/* Code */ + + if ( (pNeighbour == NULL) || (pMb->sliceId != pNeighbour->sliceId) ) + return(HANTRO_FALSE); + else + return(HANTRO_TRUE); + +} + diff --git a/src/video/h264_decoder/h264bsd_neighbour.h b/src/video/h264_decoder/h264bsd_neighbour.h new file mode 100644 index 000000000..fce0ad19e --- /dev/null +++ b/src/video/h264_decoder/h264bsd_neighbour.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_NEIGHBOUR_H +#define H264SWDEC_NEIGHBOUR_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_macroblock_layer.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +typedef enum { + MB_A = 0, + MB_B, + MB_C, + MB_D, + MB_CURR, + MB_NA = 0xFF +} neighbourMb_e; + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +typedef struct +{ + neighbourMb_e mb; + u8 index; +} neighbour_t; + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +void h264bsdInitMbNeighbours(mbStorage_t *pMbStorage, u32 picWidth, + u32 picSizeInMbs); + +mbStorage_t* h264bsdGetNeighbourMb(mbStorage_t *pMb, neighbourMb_e neighbour); + +u32 h264bsdIsNeighbourAvailable(mbStorage_t *pMb, mbStorage_t *pNeighbour); + +const neighbour_t* h264bsdNeighbour4x4BlockA(u32 blockIndex); +const neighbour_t* h264bsdNeighbour4x4BlockB(u32 blockIndex); +const neighbour_t* h264bsdNeighbour4x4BlockC(u32 blockIndex); +const neighbour_t* h264bsdNeighbour4x4BlockD(u32 blockIndex); + +#endif /* #ifdef H264SWDEC_NEIGHBOUR_H */ + diff --git a/src/video/h264_decoder/h264bsd_pic_order_cnt.c b/src/video/h264_decoder/h264bsd_pic_order_cnt.c new file mode 100644 index 000000000..a132df506 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_pic_order_cnt.c @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdDecodePicOrderCnt + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_util.h" +#include "h264bsd_pic_order_cnt.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + + Function: h264bsdDecodePicOrderCnt + + Functional description: + Compute picture order count for a picture. Function implements + computation of all POC types (0, 1 and 2), type is obtained from + sps. See standard for description of the POC types and how POC is + computed for each type. + + Function returns the minimum of top field and bottom field pic + order counts. + + Inputs: + poc pointer to previous results + sps pointer to sequence parameter set + slicHeader pointer to current slice header, frame number and + other params needed for POC computation + pNalUnit pointer to current NAL unit structrue, function needs + to know if this is an IDR picture and also if this is + a reference picture + + Outputs: + poc results stored here for computation of next POC + + Returns: + picture order count + +------------------------------------------------------------------------------*/ + +i32 h264bsdDecodePicOrderCnt(pocStorage_t *poc, seqParamSet_t *sps, + sliceHeader_t *pSliceHeader, nalUnit_t *pNalUnit) +{ + +/* Variables */ + + u32 i; + i32 picOrderCnt; + u32 frameNumOffset, absFrameNum, picOrderCntCycleCnt=0; + u32 frameNumInPicOrderCntCycle=0; + i32 expectedDeltaPicOrderCntCycle; + u32 containsMmco5; + +/* Code */ + + ASSERT(poc); + ASSERT(sps); + ASSERT(pSliceHeader); + ASSERT(pNalUnit); + ASSERT(sps->picOrderCntType <= 2); + +#if 0 + /* JanSa: I don't think this is necessary, don't see any reason to + * increment prevFrameNum one by one instead of one big increment. + * However, standard specifies that this should be done -> if someone + * figures out any case when the outcome would be different for step by + * step increment, this part of the code should be enabled */ + + /* if there was a gap in frame numbering and picOrderCntType is 1 or 2 -> + * "compute" pic order counts for non-existing frames. These are not + * actually computed, but process needs to be done to update the + * prevFrameNum and prevFrameNumOffset */ + if ( sps->picOrderCntType > 0 && + pSliceHeader->frameNum != poc->prevFrameNum && + pSliceHeader->frameNum != ((poc->prevFrameNum + 1) % sps->maxFrameNum)) + { + + /* use variable i for unUsedShortTermFrameNum */ + i = (poc->prevFrameNum + 1) % sps->maxFrameNum; + + do + { + if (poc->prevFrameNum > i) + frameNumOffset = poc->prevFrameNumOffset + sps->maxFrameNum; + else + frameNumOffset = poc->prevFrameNumOffset; + + poc->prevFrameNumOffset = frameNumOffset; + poc->prevFrameNum = i; + + i = (i + 1) % sps->maxFrameNum; + + } while (i != pSliceHeader->frameNum); + } +#endif + + /* check if current slice includes mmco equal to 5 */ + containsMmco5 = HANTRO_FALSE; + if (pSliceHeader->decRefPicMarking.adaptiveRefPicMarkingModeFlag) + { + i = 0; + while (pSliceHeader->decRefPicMarking.operation[i]. + memoryManagementControlOperation) + { + if (pSliceHeader->decRefPicMarking.operation[i]. + memoryManagementControlOperation == 5) + { + containsMmco5 = HANTRO_TRUE; + break; + } + i++; + } + } + switch (sps->picOrderCntType) + { + + case 0: + /* set prevPicOrderCnt values for IDR frame */ + if (IS_IDR_NAL_UNIT(pNalUnit)) + { + poc->prevPicOrderCntMsb = 0; + poc->prevPicOrderCntLsb = 0; + } + + /* compute picOrderCntMsb (stored in picOrderCnt variable) */ + if ( (pSliceHeader->picOrderCntLsb < poc->prevPicOrderCntLsb) && + ((poc->prevPicOrderCntLsb - pSliceHeader->picOrderCntLsb) >= + sps->maxPicOrderCntLsb/2) ) + { + picOrderCnt = poc->prevPicOrderCntMsb + + (i32)sps->maxPicOrderCntLsb; + } + else if ((pSliceHeader->picOrderCntLsb > poc->prevPicOrderCntLsb) && + ((pSliceHeader->picOrderCntLsb - poc->prevPicOrderCntLsb) > + sps->maxPicOrderCntLsb/2) ) + { + picOrderCnt = poc->prevPicOrderCntMsb - + (i32)sps->maxPicOrderCntLsb; + } + else + picOrderCnt = poc->prevPicOrderCntMsb; + + /* standard specifies that prevPicOrderCntMsb is from previous + * rererence frame -> replace old value only if current frame is + * rererence frame */ + if (pNalUnit->nalRefIdc) + poc->prevPicOrderCntMsb = picOrderCnt; + + /* compute top field order cnt (stored in picOrderCnt) */ + picOrderCnt += (i32)pSliceHeader->picOrderCntLsb; + + /* if delta for bottom field is negative -> bottom will be the + * minimum pic order count */ + if (pSliceHeader->deltaPicOrderCntBottom < 0) + picOrderCnt += pSliceHeader->deltaPicOrderCntBottom; + + /* standard specifies that prevPicOrderCntLsb is from previous + * rererence frame -> replace old value only if current frame is + * rererence frame */ + if (pNalUnit->nalRefIdc) + { + /* if current frame contains mmco5 -> modify values to be + * stored */ + if (containsMmco5) + { + poc->prevPicOrderCntMsb = 0; + /* prevPicOrderCntLsb should be the top field picOrderCnt + * if previous frame included mmco5. Top field picOrderCnt + * for frames containing mmco5 is obtained by subtracting + * the picOrderCnt from original top field order count -> + * value is zero if top field was the minimum, i.e. delta + * for bottom was positive, otherwise value is + * -deltaPicOrderCntBottom */ + if (pSliceHeader->deltaPicOrderCntBottom < 0) + poc->prevPicOrderCntLsb = + (u32)(-pSliceHeader->deltaPicOrderCntBottom); + else + poc->prevPicOrderCntLsb = 0; + picOrderCnt = 0; + } + else + { + poc->prevPicOrderCntLsb = pSliceHeader->picOrderCntLsb; + } + } + + break; + + case 1: + + /* step 1 (in the description in the standard) */ + if (IS_IDR_NAL_UNIT(pNalUnit)) + frameNumOffset = 0; + else if (poc->prevFrameNum > pSliceHeader->frameNum) + frameNumOffset = poc->prevFrameNumOffset + sps->maxFrameNum; + else + frameNumOffset = poc->prevFrameNumOffset; + + /* step 2 */ + if (sps->numRefFramesInPicOrderCntCycle) + absFrameNum = frameNumOffset + pSliceHeader->frameNum; + else + absFrameNum = 0; + + if (pNalUnit->nalRefIdc == 0 && absFrameNum > 0) + absFrameNum -= 1; + + /* step 3 */ + if (absFrameNum > 0) + { + picOrderCntCycleCnt = + (absFrameNum - 1)/sps->numRefFramesInPicOrderCntCycle; + frameNumInPicOrderCntCycle = + (absFrameNum - 1)%sps->numRefFramesInPicOrderCntCycle; + } + + /* step 4 */ + expectedDeltaPicOrderCntCycle = 0; + for (i = 0; i < sps->numRefFramesInPicOrderCntCycle; i++) + expectedDeltaPicOrderCntCycle += sps->offsetForRefFrame[i]; + + /* step 5 (picOrderCnt used to store expectedPicOrderCnt) */ + /*lint -esym(644,picOrderCntCycleCnt) always initialized */ + /*lint -esym(644,frameNumInPicOrderCntCycle) always initialized */ + if (absFrameNum > 0) + { + picOrderCnt = + (i32)picOrderCntCycleCnt * expectedDeltaPicOrderCntCycle; + for (i = 0; i <= frameNumInPicOrderCntCycle; i++) + picOrderCnt += sps->offsetForRefFrame[i]; + } + else + picOrderCnt = 0; + + if (pNalUnit->nalRefIdc == 0) + picOrderCnt += sps->offsetForNonRefPic; + + /* step 6 (picOrderCnt is top field order cnt if delta for bottom + * is positive, otherwise it is bottom field order cnt) */ + picOrderCnt += pSliceHeader->deltaPicOrderCnt[0]; + + if ( (sps->offsetForTopToBottomField + + pSliceHeader->deltaPicOrderCnt[1]) < 0 ) + { + picOrderCnt += sps->offsetForTopToBottomField + + pSliceHeader->deltaPicOrderCnt[1]; + } + + /* if current picture contains mmco5 -> set prevFrameNumOffset and + * prevFrameNum to 0 for computation of picOrderCnt of next + * frame, otherwise store frameNum and frameNumOffset to poc + * structure */ + if (!containsMmco5) + { + poc->prevFrameNumOffset = frameNumOffset; + poc->prevFrameNum = pSliceHeader->frameNum; + } + else + { + poc->prevFrameNumOffset = 0; + poc->prevFrameNum = 0; + picOrderCnt = 0; + } + break; + + default: /* case 2 */ + /* derive frameNumOffset */ + if (IS_IDR_NAL_UNIT(pNalUnit)) + frameNumOffset = 0; + else if (poc->prevFrameNum > pSliceHeader->frameNum) + frameNumOffset = poc->prevFrameNumOffset + sps->maxFrameNum; + else + frameNumOffset = poc->prevFrameNumOffset; + + /* derive picOrderCnt (type 2 has same value for top and bottom + * field order cnts) */ + if (IS_IDR_NAL_UNIT(pNalUnit)) + picOrderCnt = 0; + else if (pNalUnit->nalRefIdc == 0) + picOrderCnt = + 2 * (i32)(frameNumOffset + pSliceHeader->frameNum) - 1; + else + picOrderCnt = + 2 * (i32)(frameNumOffset + pSliceHeader->frameNum); + + /* if current picture contains mmco5 -> set prevFrameNumOffset and + * prevFrameNum to 0 for computation of picOrderCnt of next + * frame, otherwise store frameNum and frameNumOffset to poc + * structure */ + if (!containsMmco5) + { + poc->prevFrameNumOffset = frameNumOffset; + poc->prevFrameNum = pSliceHeader->frameNum; + } + else + { + poc->prevFrameNumOffset = 0; + poc->prevFrameNum = 0; + picOrderCnt = 0; + } + break; + + } + + /*lint -esym(644,picOrderCnt) always initialized */ + return(picOrderCnt); + +} + diff --git a/src/video/h264_decoder/h264bsd_pic_order_cnt.h b/src/video/h264_decoder/h264bsd_pic_order_cnt.h new file mode 100644 index 000000000..19741eb31 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_pic_order_cnt.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_PIC_ORDER_CNT_H +#define H264SWDEC_PIC_ORDER_CNT_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_seq_param_set.h" +#include "h264bsd_slice_header.h" +#include "h264bsd_nal_unit.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/* structure to store information computed for previous picture, needed for + * POC computation of a picture. Two first fields for POC type 0, last two + * for types 1 and 2 */ +typedef struct +{ + u32 prevPicOrderCntLsb; + i32 prevPicOrderCntMsb; + u32 prevFrameNum; + u32 prevFrameNumOffset; +} pocStorage_t; + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +i32 h264bsdDecodePicOrderCnt(pocStorage_t *poc, seqParamSet_t *sps, + sliceHeader_t *sliceHeader, nalUnit_t *pNalUnit); + +#endif /* #ifdef H264SWDEC_PIC_ORDER_CNT_H */ + diff --git a/src/video/h264_decoder/h264bsd_pic_param_set.c b/src/video/h264_decoder/h264bsd_pic_param_set.c new file mode 100644 index 000000000..63bc9a3af --- /dev/null +++ b/src/video/h264_decoder/h264bsd_pic_param_set.c @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdDecodePicParamSet + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_pic_param_set.h" +#include "h264bsd_util.h" +#include "h264bsd_vlc.h" +#include "h264bsd_cfg.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/* lookup table for ceil(log2(numSliceGroups)), i.e. number of bits needed to + * represent range [0, numSliceGroups) + * + * NOTE: if MAX_NUM_SLICE_GROUPS is higher than 8 this table has to be resized + * accordingly */ +static const u32 CeilLog2NumSliceGroups[8] = {1, 1, 2, 2, 3, 3, 3, 3}; + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + + Function name: h264bsdDecodePicParamSet + + Functional description: + Decode picture parameter set information from the stream. + + Function allocates memory for + - run lengths if slice group map type is 0 + - top-left and bottom-right arrays if map type is 2 + - for slice group ids if map type is 6 + + Validity of some of the slice group mapping information depends + on the image dimensions which are not known here. Therefore the + validity has to be checked afterwards, currently in the parameter + set activation phase. + + Inputs: + pStrmData pointer to stream data structure + + Outputs: + pPicParamSet decoded information is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK failure, invalid information or end of stream + MEMORY_ALLOCATION_ERROR for memory allocation failure + + +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodePicParamSet(strmData_t *pStrmData, picParamSet_t *pPicParamSet) +{ + +/* Variables */ + + u32 tmp, i, value; + i32 itmp; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(pPicParamSet); + + + H264SwDecMemset(pPicParamSet, 0, sizeof(picParamSet_t)); + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pPicParamSet->picParameterSetId); + if (tmp != HANTRO_OK) + return(tmp); + if (pPicParamSet->picParameterSetId >= MAX_NUM_PIC_PARAM_SETS) + { + EPRINT("pic_parameter_set_id"); + return(HANTRO_NOK); + } + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pPicParamSet->seqParameterSetId); + if (tmp != HANTRO_OK) + return(tmp); + if (pPicParamSet->seqParameterSetId >= MAX_NUM_SEQ_PARAM_SETS) + { + EPRINT("seq_param_set_id"); + return(HANTRO_NOK); + } + + /* entropy_coding_mode_flag, shall be 0 for baseline profile */ + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp) + { + EPRINT("entropy_coding_mode_flag"); + return(HANTRO_NOK); + } + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pPicParamSet->picOrderPresentFlag = (tmp == 1) ? HANTRO_TRUE : HANTRO_FALSE; + + /* num_slice_groups_minus1 */ + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pPicParamSet->numSliceGroups = value + 1; + if (pPicParamSet->numSliceGroups > MAX_NUM_SLICE_GROUPS) + { + EPRINT("num_slice_groups_minus1"); + return(HANTRO_NOK); + } + + /* decode slice group mapping information if more than one slice groups */ + if (pPicParamSet->numSliceGroups > 1) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pPicParamSet->sliceGroupMapType); + if (tmp != HANTRO_OK) + return(tmp); + if (pPicParamSet->sliceGroupMapType > 6) + { + EPRINT("slice_group_map_type"); + return(HANTRO_NOK); + } + + if (pPicParamSet->sliceGroupMapType == 0) + { + ALLOCATE(pPicParamSet->runLength, + pPicParamSet->numSliceGroups, u32); + if (pPicParamSet->runLength == NULL) + return(MEMORY_ALLOCATION_ERROR); + for (i = 0; i < pPicParamSet->numSliceGroups; i++) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pPicParamSet->runLength[i] = value+1; + /* param values checked in CheckPps() */ + } + } + else if (pPicParamSet->sliceGroupMapType == 2) + { + ALLOCATE(pPicParamSet->topLeft, + pPicParamSet->numSliceGroups - 1, u32); + ALLOCATE(pPicParamSet->bottomRight, + pPicParamSet->numSliceGroups - 1, u32); + if (pPicParamSet->topLeft == NULL || + pPicParamSet->bottomRight == NULL) + return(MEMORY_ALLOCATION_ERROR); + for (i = 0; i < pPicParamSet->numSliceGroups - 1; i++) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pPicParamSet->topLeft[i] = value; + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pPicParamSet->bottomRight[i] = value; + /* param values checked in CheckPps() */ + } + } + else if ( (pPicParamSet->sliceGroupMapType == 3) || + (pPicParamSet->sliceGroupMapType == 4) || + (pPicParamSet->sliceGroupMapType == 5) ) + { + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pPicParamSet->sliceGroupChangeDirectionFlag = + (tmp == 1) ? HANTRO_TRUE : HANTRO_FALSE; + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pPicParamSet->sliceGroupChangeRate = value + 1; + /* param value checked in CheckPps() */ + } + else if (pPicParamSet->sliceGroupMapType == 6) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pPicParamSet->picSizeInMapUnits = value + 1; + + ALLOCATE(pPicParamSet->sliceGroupId, + pPicParamSet->picSizeInMapUnits, u32); + if (pPicParamSet->sliceGroupId == NULL) + return(MEMORY_ALLOCATION_ERROR); + + /* determine number of bits needed to represent range + * [0, numSliceGroups) */ + tmp = CeilLog2NumSliceGroups[pPicParamSet->numSliceGroups-1]; + + for (i = 0; i < pPicParamSet->picSizeInMapUnits; i++) + { + pPicParamSet->sliceGroupId[i] = h264bsdGetBits(pStrmData, tmp); + if ( pPicParamSet->sliceGroupId[i] >= + pPicParamSet->numSliceGroups ) + { + EPRINT("slice_group_id"); + return(HANTRO_NOK); + } + } + } + } + + /* num_ref_idx_l0_active_minus1 */ + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + if (value > 31) + { + EPRINT("num_ref_idx_l0_active_minus1"); + return(HANTRO_NOK); + } + pPicParamSet->numRefIdxL0Active = value + 1; + + /* num_ref_idx_l1_active_minus1 */ + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + if (value > 31) + { + EPRINT("num_ref_idx_l1_active_minus1"); + return(HANTRO_NOK); + } + + /* weighted_pred_flag */ + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pPicParamSet->weightedPredFlag = tmp; + + /* weighted_bipred_idc */ + tmp = h264bsdGetBits(pStrmData, 2); + if (tmp > 2) + { + EPRINT("weighted_bipred_idc"); + return(HANTRO_NOK); + } + + /* pic_init_qp_minus26 */ + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &itmp); + if (tmp != HANTRO_OK) + return(tmp); + if ((itmp < -26) || (itmp > 25)) + { + EPRINT("pic_init_qp_minus26"); + return(HANTRO_NOK); + } + pPicParamSet->picInitQp = (u32)(itmp + 26); + + /* pic_init_qs_minus26 */ + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &itmp); + if (tmp != HANTRO_OK) + return(tmp); + if ((itmp < -26) || (itmp > 25)) + { + EPRINT("pic_init_qs_minus26"); + return(HANTRO_NOK); + } + + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &itmp); + if (tmp != HANTRO_OK) + return(tmp); + if ((itmp < -12) || (itmp > 12)) + { + EPRINT("chroma_qp_index_offset"); + return(HANTRO_NOK); + } + pPicParamSet->chromaQpIndexOffset = itmp; + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pPicParamSet->deblockingFilterControlPresentFlag = + (tmp == 1) ? HANTRO_TRUE : HANTRO_FALSE; + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pPicParamSet->constrainedIntraPredFlag = (tmp == 1) ? + HANTRO_TRUE : HANTRO_FALSE; + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pPicParamSet->redundantPicCntPresentFlag = (tmp == 1) ? + HANTRO_TRUE : HANTRO_FALSE; + + tmp = h264bsdRbspTrailingBits(pStrmData); + + /* ignore possible errors in trailing bits of parameters sets */ + return(HANTRO_OK); + +} + diff --git a/src/video/h264_decoder/h264bsd_pic_param_set.h b/src/video/h264_decoder/h264bsd_pic_param_set.h new file mode 100644 index 000000000..d1d7f0b93 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_pic_param_set.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_PIC_PARAM_SET_H +#define H264SWDEC_PIC_PARAM_SET_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_stream.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/* data structure to store PPS information decoded from the stream */ +typedef struct +{ + u32 picParameterSetId; + u32 seqParameterSetId; + u32 picOrderPresentFlag; + u32 numSliceGroups; + u32 sliceGroupMapType; + u32 *runLength; + u32 *topLeft; + u32 *bottomRight; + u32 sliceGroupChangeDirectionFlag; + u32 sliceGroupChangeRate; + u32 picSizeInMapUnits; + u32 *sliceGroupId; + u32 numRefIdxL0Active; + u32 weightedPredFlag; + u32 picInitQp; + i32 chromaQpIndexOffset; + u32 deblockingFilterControlPresentFlag; + u32 constrainedIntraPredFlag; + u32 redundantPicCntPresentFlag; +} picParamSet_t; + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodePicParamSet(strmData_t *pStrmData, + picParamSet_t *pPicParamSet); + +#endif /* #ifdef H264SWDEC_PIC_PARAM_SET_H */ + diff --git a/src/video/h264_decoder/h264bsd_reconstruct.c b/src/video/h264_decoder/h264bsd_reconstruct.c new file mode 100644 index 000000000..d0837e39c --- /dev/null +++ b/src/video/h264_decoder/h264bsd_reconstruct.c @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_reconstruct.h" +#include "h264bsd_macroblock_layer.h" +#include "h264bsd_image.h" +#include "h264bsd_util.h" + +#include "omxdl/omxtypes.h" +#include "omxdl/omxVC.h" +#include "omxdl/armVC.h" + +#define UNUSED(x) (void)(x) + +#ifdef H264BSD_N64 +#include "../rsph264_internal.h" +#include "../fastcache.h" +#endif + + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/* Switch off the following Lint messages for this file: + * Info 701: Shift left of signed quantity (int) + * Info 702: Shift right of signed quantity (int) + */ +/*lint -e701 -e702 */ + +/* clipping table, defined in h264bsd_intra_prediction.c */ +extern const u8 h264bsdClip[]; + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ +/*------------------------------------------------------------------------------ + + Function: h264bsdPredictSamples + + Functional description: + This function reconstructs a prediction for a macroblock partition. + The prediction is either copied or interpolated using the reference + frame and the motion vector. Both luminance and chrominance parts are + predicted. The prediction is stored in given macroblock array (data). + Inputs: + data pointer to macroblock array (384 bytes) for output + mv pointer to motion vector used for prediction + refPic pointer to reference picture structure + xA x-coordinate for current macroblock + yA y-coordinate for current macroblock + partX x-offset for partition in macroblock + partY y-offset for partition in macroblock + partWidth width of partition + partHeight height of partition + Outputs: + data macroblock array (16x16+8x8+8x8) where predicted + partition is stored at correct position + +------------------------------------------------------------------------------*/ +#if !H264BSD_N64 +/*lint -e{550} Symbol 'res' not accessed */ +void h264bsdPredictSamples( + image_t *pic, + mv_t *mv, + image_t *refPic, + u32 colAndRow, + u32 part, + u8 *pFill) + +{ + +/* Variables */ + + u32 xFrac, yFrac; + u32 width, height; + i32 xInt, yInt, x0, y0; + u8 *partData, *ref; + u32 partPitch; + OMXSize roi; + u32 fillWidth; + u32 fillHeight; + OMXResult res; + u32 xA, yA; + u32 partX, partY; + u32 partWidth, partHeight; + +/* Code */ + + ASSERT(pic->luma); + ASSERT(pic->cb); + ASSERT(pic->cr); + ASSERT(mv); + ASSERT(refPic); + ASSERT(refPic->data); + ASSERT(refPic->width); + ASSERT(refPic->height); + + xA = (colAndRow & 0xFFFF0000) >> 16; + yA = (colAndRow & 0x0000FFFF); + + partX = (part & 0xFF000000) >> 24; + partY = (part & 0x00FF0000) >> 16; + partWidth = (part & 0x0000FF00) >> 8; + partHeight = (part & 0x000000FF); + + ASSERT(partWidth); + ASSERT(partHeight); + + /* luma */ + PROFILE_START(PS_H264_INTERPRED_LUMA); + partPitch = pic->width*16; + partData = pic->luma + partPitch*partY + partX; + + xFrac = mv->hor & 0x3; + yFrac = mv->ver & 0x3; + + width = 16 * refPic->width; + height = 16 * refPic->height; + + xInt = (i32)xA + (i32)partX + (mv->hor >> 2); + yInt = (i32)yA + (i32)partY + (mv->ver >> 2); + + x0 = (xFrac) ? xInt-2 : xInt; + y0 = (yFrac) ? yInt-2 : yInt; + + if (xFrac) + { + if (partWidth == 16) + fillWidth = 32; + else + fillWidth = 16; + } + else + fillWidth = (partWidth*2); + if (yFrac) + fillHeight = partHeight+5; + else + fillHeight = partHeight; + + + if ((x0 < 0) || ((u32)x0+partWidth+(xFrac?5:0)> width) || + (y0 < 0) || ((u32)y0+partHeight+(yFrac?5:0) > height)) + { + h264bsdFillBlock(refPic->data, (u8*)pFill, x0, y0, width, height, + fillWidth, fillHeight, fillWidth); + + x0 = 0; + y0 = 0; + ref = pFill; + width = fillWidth; + if (yFrac) + ref += 2*width; + if (xFrac) + ref += 2; + + #ifdef H264BSD_N64 + fast_data_cache_hit_writeback(pFill, fillWidth*fillHeight); + pFill += fillWidth*fillHeight; + #endif + } + else + { + /*lint --e(737) Loss of sign */ + ref = refPic->data + yInt*width + xInt; + } + /* Luma interpolation */ + roi.width = (i32)partWidth; + roi.height = (i32)partHeight; + +#ifdef H264BSD_N64 + rsph264_queue_interpolate_luma(RSPH264_CACHE_SKIP_ALL, + ref, (i32)width, + partData, partPitch, + roi.width, roi.height, + (i32)xFrac, (i32)yFrac); +#else + res = omxVCM4P10_InterpolateLuma(ref, (i32)width, partData, partPitch, + (i32)xFrac, (i32)yFrac, roi); + ASSERT(res == 0); +#endif + PROFILE_STOP(PS_H264_INTERPRED_LUMA); + + /* Chroma */ + PROFILE_START(PS_H264_INTERPRED_CHROMA); + width = 8 * refPic->width; + height = 8 * refPic->height; + + x0 = ((xA + partX) >> 1) + (mv->hor >> 3); + y0 = ((yA + partY) >> 1) + (mv->ver >> 3); + xFrac = mv->hor & 0x7; + yFrac = mv->ver & 0x7; + + ref = refPic->data + 256 * refPic->width * refPic->height; + + roi.width = (i32)(partWidth >> 1); + fillWidth = ((partWidth >> 1) + 8) & ~0x7; + roi.height = (i32)(partHeight >> 1); + fillHeight = (partHeight >> 1) + 1; + + if ((x0 < 0) || ((u32)x0+roi.width+(xFrac?1:0) > width) || + (y0 < 0) || ((u32)y0+roi.height+(yFrac?1:0) > height)) + { + h264bsdFillBlock(ref, pFill, x0, y0, width, height, + fillWidth, fillHeight, fillWidth); + ref += width * height; + h264bsdFillBlock(ref, pFill + fillWidth*fillHeight, + x0, y0, width, height, fillWidth, + fillHeight, fillWidth); + + ref = pFill; + x0 = 0; + y0 = 0; + width = fillWidth; + height = fillHeight; + + #ifdef H264BSD_N64 + fast_data_cache_hit_writeback(pFill, fillWidth*fillHeight*2); + pFill += fillWidth*fillHeight*2; + #endif + } + + partPitch = pic->width*8; + partData = pic->cb + (partY>>1)*partPitch + (partX>>1); + + /* Chroma interpolation */ + /*lint --e(737) Loss of sign */ + ref += y0 * width + x0; +#ifdef H264BSD_N64 + rsph264_queue_interpolate_chroma(RSPH264_CACHE_SKIP_ALL, + ref, width, + partData, partPitch, + roi.width, roi.height, + xFrac, yFrac); +#else + res = armVCM4P10_Interpolate_Chroma(ref, width, partData, pic->width*8, + (u32)roi.width, (u32)roi.height, xFrac, yFrac); + ASSERT(res == 0); +#endif + partData = pic->cr + (partY>>1)*partPitch + (partX>>1); + ref += height * width; +#ifdef H264BSD_N64 + rsph264_queue_interpolate_chroma(RSPH264_CACHE_SKIP_ALL, + ref, width, + partData, partPitch, + roi.width, roi.height, + xFrac, yFrac); +#else + res = armVCM4P10_Interpolate_Chroma(ref, width, partData, pic->width*8, + (u32)roi.width, (u32)roi.height, xFrac, yFrac); + ASSERT(res == 0); +#endif + + PROFILE_STOP(PS_H264_INTERPRED_CHROMA); + (void)res; +} +#endif + +/*------------------------------------------------------------------------------ + + Function: FillRow1 + + Functional description: + This function gets a row of reference pels in a 'normal' case when no + overfilling is necessary. + +------------------------------------------------------------------------------*/ + +static void FillRow1( + u8 *ref, + u8 *fill, + i32 left, + i32 center, + i32 right) +{ + UNUSED(left); + UNUSED(right); + ASSERT(ref); + ASSERT(fill); + + H264SwDecMemcpy(fill, ref, (u32)center); + + /*lint -e(715) */ +} + + +/*------------------------------------------------------------------------------ + + Function: h264bsdFillRow7 + + Functional description: + This function gets a row of reference pels when horizontal coordinate + is partly negative or partly greater than reference picture width + (overfilling some pels on left and/or right edge). + Inputs: + ref pointer to reference samples + left amount of pixels to overfill on left-edge + center amount of pixels to copy + right amount of pixels to overfill on right-edge + Outputs: + fill pointer where samples are stored + +------------------------------------------------------------------------------*/ +#ifndef H264DEC_NEON +void h264bsdFillRow7( + u8 *ref, + u8 *fill, + i32 left, + i32 center, + i32 right) +{ + u8 tmp = 0; + + ASSERT(ref); + ASSERT(fill); + + if (left) + tmp = *ref; + + for ( ; left; left--) + /*lint -esym(644,tmp) tmp is initialized if used */ + *fill++ = tmp; + + for ( ; center; center--) + *fill++ = *ref++; + + if (right) + tmp = ref[-1]; + + for ( ; right; right--) + /*lint -esym(644,tmp) tmp is initialized if used */ + *fill++ = tmp; +} +#endif +/*------------------------------------------------------------------------------ + + Function: h264bsdFillBlock + + Functional description: + This function gets a block of reference pels. It determines whether + overfilling is needed or not and repeatedly calls an appropriate + function (by using a function pointer) that fills one row the block. + Inputs: + ref pointer to reference frame + x0 x-coordinate for block + y0 y-coordinate for block + width width of reference frame + height height of reference frame + blockWidth width of block + blockHeight height of block + fillScanLength length of a line in output array (pixels) + Outputs: + fill pointer to array where output block is written + +------------------------------------------------------------------------------*/ + +void h264bsdFillBlock( + u8 *ref, + u8 *fill, + i32 x0, + i32 y0, + u32 width, + u32 height, + u32 blockWidth, + u32 blockHeight, + u32 fillScanLength) + +{ + +/* Variables */ + + i32 xstop, ystop; + void (*fp)(u8*, u8*, i32, i32, i32); + i32 left, x, right; + i32 top, y, bottom; + +/* Code */ + + ASSERT(ref); + ASSERT(fill); + ASSERT(width); + ASSERT(height); + ASSERT(fill); + ASSERT(blockWidth); + ASSERT(blockHeight); + + xstop = x0 + (i32)blockWidth; + ystop = y0 + (i32)blockHeight; + + /* Choose correct function whether overfilling on left-edge or right-edge + * is needed or not */ + if (x0 >= 0 && xstop <= (i32)width) + fp = FillRow1; + else + fp = h264bsdFillRow7; + + if (ystop < 0) + y0 = -(i32)blockHeight; + + if (xstop < 0) + x0 = -(i32)blockWidth; + + if (y0 > (i32)height) + y0 = (i32)height; + + if (x0 > (i32)width) + x0 = (i32)width; + + xstop = x0 + (i32)blockWidth; + ystop = y0 + (i32)blockHeight; + + if (x0 > 0) + ref += x0; + + if (y0 > 0) + ref += y0 * (i32)width; + + left = x0 < 0 ? -x0 : 0; + right = xstop > (i32)width ? xstop - (i32)width : 0; + x = (i32)blockWidth - left - right; + + top = y0 < 0 ? -y0 : 0; + bottom = ystop > (i32)height ? ystop - (i32)height : 0; + y = (i32)blockHeight - top - bottom; + + /* Top-overfilling */ + for ( ; top; top-- ) + { + (*fp)(ref, fill, left, x, right); + fill += fillScanLength; + } + + /* Lines inside reference image */ + for ( ; y; y-- ) + { + (*fp)(ref, fill, left, x, right); + ref += width; + fill += fillScanLength; + } + + ref -= width; + + /* Bottom-overfilling */ + for ( ; bottom; bottom-- ) + { + (*fp)(ref, fill, left, x, right); + fill += fillScanLength; + } +} + +/*lint +e701 +e702 */ + + diff --git a/src/video/h264_decoder/h264bsd_reconstruct.h b/src/video/h264_decoder/h264bsd_reconstruct.h new file mode 100644 index 000000000..ec7c8b0a9 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_reconstruct.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_RECONSTRUCT_H +#define H264SWDEC_RECONSTRUCT_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_macroblock_layer.h" +#include "h264bsd_image.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ +void h264bsdPredictSamples( + image_t *pic, + mv_t *mv, + image_t *refPic, + u32 colAndRow,/* packaged data | column | row |*/ + u32 part, /* packaged data |partX|partY|partWidth|partHeight|*/ + u8 *pFill); + +void h264bsdFillBlock( + u8 * ref, + u8 * fill, + i32 x0, + i32 y0, + u32 width, + u32 height, + u32 blockWidth, + u32 blockHeight, + u32 fillScanLength); + +void h264bsdFillRow7( + u8 *ref, + u8 *fill, + i32 left, + i32 center, + i32 right); + +#endif /* #ifdef H264SWDEC_RECONSTRUCT_H */ + diff --git a/src/video/h264_decoder/h264bsd_seq_param_set.c b/src/video/h264_decoder/h264bsd_seq_param_set.c new file mode 100644 index 000000000..751051a14 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_seq_param_set.c @@ -0,0 +1,577 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdDecodeSeqParamSet + GetDpbSize + h264bsdCompareSeqParamSets + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_seq_param_set.h" +#include "h264bsd_util.h" +#include "h264bsd_vlc.h" +#include "h264bsd_vui.h" +#include "h264bsd_cfg.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/* enumeration to indicate invalid return value from the GetDpbSize function */ +enum {INVALID_DPB_SIZE = 0x7FFFFFFF}; + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +static u32 GetDpbSize(u32 picSizeInMbs, u32 levelIdc); + +/*------------------------------------------------------------------------------ + + Function name: h264bsdDecodeSeqParamSet + + Functional description: + Decode sequence parameter set information from the stream. + + Function allocates memory for offsetForRefFrame array if + picture order count type is 1 and numRefFramesInPicOrderCntCycle + is greater than zero. + + Inputs: + pStrmData pointer to stream data structure + + Outputs: + pSeqParamSet decoded information is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK failure, invalid information or end of stream + MEMORY_ALLOCATION_ERROR for memory allocation failure + +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeSeqParamSet(strmData_t *pStrmData, seqParamSet_t *pSeqParamSet) +{ + +/* Variables */ + + u32 tmp, i, value; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(pSeqParamSet); + + H264SwDecMemset(pSeqParamSet, 0, sizeof(seqParamSet_t)); + + /* profile_idc */ + tmp = h264bsdGetBits(pStrmData, 8); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + if (tmp != 66) + { + DEBUG(("NOT BASELINE PROFILE %d\n", tmp)); + } + pSeqParamSet->profileIdc = tmp; + + /* constrained_set0_flag */ + tmp = h264bsdGetBits(pStrmData, 1); + /* constrained_set1_flag */ + tmp = h264bsdGetBits(pStrmData, 1); + /* constrained_set2_flag */ + tmp = h264bsdGetBits(pStrmData, 1); + + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + + /* reserved_zero_5bits, values of these bits shall be ignored */ + tmp = h264bsdGetBits(pStrmData, 5); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + + tmp = h264bsdGetBits(pStrmData, 8); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pSeqParamSet->levelIdc = tmp; + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pSeqParamSet->seqParameterSetId); + if (tmp != HANTRO_OK) + return(tmp); + if (pSeqParamSet->seqParameterSetId >= MAX_NUM_SEQ_PARAM_SETS) + { + EPRINT("seq_param_set_id"); + return(HANTRO_NOK); + } + + /* log2_max_frame_num_minus4 */ + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + if (value > 12) + { + EPRINT("log2_max_frame_num_minus4"); + return(HANTRO_NOK); + } + /* maxFrameNum = 2^(log2_max_frame_num_minus4 + 4) */ + pSeqParamSet->maxFrameNum = 1 << (value+4); + + /* valid POC types are 0, 1 and 2 */ + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + if (value > 2) + { + EPRINT("pic_order_cnt_type"); + return(HANTRO_NOK); + } + pSeqParamSet->picOrderCntType = value; + + if (pSeqParamSet->picOrderCntType == 0) + { + /* log2_max_pic_order_cnt_lsb_minus4 */ + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + if (value > 12) + { + EPRINT("log2_max_pic_order_cnt_lsb_minus4"); + return(HANTRO_NOK); + } + /* maxPicOrderCntLsb = 2^(log2_max_pic_order_cnt_lsb_minus4 + 4) */ + pSeqParamSet->maxPicOrderCntLsb = 1 << (value+4); + } + else if (pSeqParamSet->picOrderCntType == 1) + { + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pSeqParamSet->deltaPicOrderAlwaysZeroFlag = (tmp == 1) ? + HANTRO_TRUE : HANTRO_FALSE; + + tmp = h264bsdDecodeExpGolombSigned(pStrmData, + &pSeqParamSet->offsetForNonRefPic); + if (tmp != HANTRO_OK) + return(tmp); + + tmp = h264bsdDecodeExpGolombSigned(pStrmData, + &pSeqParamSet->offsetForTopToBottomField); + if (tmp != HANTRO_OK) + return(tmp); + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pSeqParamSet->numRefFramesInPicOrderCntCycle); + if (tmp != HANTRO_OK) + return(tmp); + if (pSeqParamSet->numRefFramesInPicOrderCntCycle > 255) + { + EPRINT("num_ref_frames_in_pic_order_cnt_cycle"); + return(HANTRO_NOK); + } + + if (pSeqParamSet->numRefFramesInPicOrderCntCycle) + { + /* NOTE: This has to be freed somewhere! */ + ALLOCATE(pSeqParamSet->offsetForRefFrame, + pSeqParamSet->numRefFramesInPicOrderCntCycle, i32); + if (pSeqParamSet->offsetForRefFrame == NULL) + return(MEMORY_ALLOCATION_ERROR); + + for (i = 0; i < pSeqParamSet->numRefFramesInPicOrderCntCycle; i++) + { + tmp = h264bsdDecodeExpGolombSigned(pStrmData, + pSeqParamSet->offsetForRefFrame + i); + if (tmp != HANTRO_OK) + return(tmp); + } + } + else + { + pSeqParamSet->offsetForRefFrame = NULL; + } + } + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pSeqParamSet->numRefFrames); + if (tmp != HANTRO_OK) + return(tmp); + if (pSeqParamSet->numRefFrames > MAX_NUM_REF_PICS) + { + EPRINT("num_ref_frames"); + return(HANTRO_NOK); + } + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pSeqParamSet->gapsInFrameNumValueAllowedFlag = (tmp == 1) ? + HANTRO_TRUE : HANTRO_FALSE; + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pSeqParamSet->picWidthInMbs = value + 1; + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pSeqParamSet->picHeightInMbs = value + 1; + + /* frame_mbs_only_flag, shall be 1 for baseline profile */ + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + if (!tmp) + { + EPRINT("frame_mbs_only_flag"); + return(HANTRO_NOK); + } + + /* direct_8x8_inference_flag */ + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pSeqParamSet->frameCroppingFlag = (tmp == 1) ? HANTRO_TRUE : HANTRO_FALSE; + + if (pSeqParamSet->frameCroppingFlag) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pSeqParamSet->frameCropLeftOffset); + if (tmp != HANTRO_OK) + return(tmp); + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pSeqParamSet->frameCropRightOffset); + if (tmp != HANTRO_OK) + return(tmp); + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pSeqParamSet->frameCropTopOffset); + if (tmp != HANTRO_OK) + return(tmp); + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pSeqParamSet->frameCropBottomOffset); + if (tmp != HANTRO_OK) + return(tmp); + + /* check that frame cropping params are valid, parameters shall + * specify non-negative area within the original picture */ + if ( ( (i32)pSeqParamSet->frameCropLeftOffset > + ( 8 * (i32)pSeqParamSet->picWidthInMbs - + ((i32)pSeqParamSet->frameCropRightOffset + 1) ) ) || + ( (i32)pSeqParamSet->frameCropTopOffset > + ( 8 * (i32)pSeqParamSet->picHeightInMbs - + ((i32)pSeqParamSet->frameCropBottomOffset + 1) ) ) ) + { + EPRINT("frame_cropping"); + return(HANTRO_NOK); + } + } + + /* check that image dimensions and levelIdc match */ + tmp = pSeqParamSet->picWidthInMbs * pSeqParamSet->picHeightInMbs; + value = GetDpbSize(tmp, pSeqParamSet->levelIdc); + if (value == INVALID_DPB_SIZE || pSeqParamSet->numRefFrames > value) + { + DEBUG(("WARNING! Invalid DPB size based on SPS Level!\n")); + DEBUG(("WARNING! Using num_ref_frames =%d for DPB size!\n", + pSeqParamSet->numRefFrames)); + value = pSeqParamSet->numRefFrames; + } + pSeqParamSet->maxDpbSize = value; + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pSeqParamSet->vuiParametersPresentFlag = (tmp == 1) ? + HANTRO_TRUE : HANTRO_FALSE; + + /* VUI */ + if (pSeqParamSet->vuiParametersPresentFlag) + { + ALLOCATE(pSeqParamSet->vuiParameters, 1, vuiParameters_t); + if (pSeqParamSet->vuiParameters == NULL) + return(MEMORY_ALLOCATION_ERROR); + tmp = h264bsdDecodeVuiParameters(pStrmData, + pSeqParamSet->vuiParameters); + if (tmp != HANTRO_OK) + return(tmp); + /* check numReorderFrames and maxDecFrameBuffering */ + if (pSeqParamSet->vuiParameters->bitstreamRestrictionFlag) + { + if (pSeqParamSet->vuiParameters->numReorderFrames > + pSeqParamSet->vuiParameters->maxDecFrameBuffering || + pSeqParamSet->vuiParameters->maxDecFrameBuffering < + pSeqParamSet->numRefFrames || + pSeqParamSet->vuiParameters->maxDecFrameBuffering > + pSeqParamSet->maxDpbSize) + { + return(HANTRO_NOK); + } + + /* standard says that "the sequence shall not require a DPB with + * size of more than max(1, maxDecFrameBuffering) */ + pSeqParamSet->maxDpbSize = + MAX(1, pSeqParamSet->vuiParameters->maxDecFrameBuffering); + } + } + + tmp = h264bsdRbspTrailingBits(pStrmData); + + /* ignore possible errors in trailing bits of parameters sets */ + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: GetDpbSize + + Functional description: + Get size of the DPB in frames. Size is determined based on the + picture size and MaxDPB for the specified level. These determine + how many pictures may fit into to the buffer. However, the size + is also limited to a maximum of 16 frames and therefore function + returns the minimum of the determined size and 16. + + Inputs: + picSizeInMbs number of macroblocks in the picture + levelIdc indicates the level + + Outputs: + none + + Returns: + size of the DPB in frames + INVALID_DPB_SIZE when invalid levelIdc specified or picSizeInMbs + is higher than supported by the level in question + +------------------------------------------------------------------------------*/ + +u32 GetDpbSize(u32 picSizeInMbs, u32 levelIdc) +{ + +/* Variables */ + + u32 tmp; + u32 maxPicSizeInMbs; + +/* Code */ + + ASSERT(picSizeInMbs); + + /* use tmp as the size of the DPB in bytes, computes as 1024 * MaxDPB + * (from table A-1 in Annex A) */ + switch (levelIdc) + { + case 10: + tmp = 152064; + maxPicSizeInMbs = 99; + break; + + case 11: + tmp = 345600; + maxPicSizeInMbs = 396; + break; + + case 12: + tmp = 912384; + maxPicSizeInMbs = 396; + break; + + case 13: + tmp = 912384; + maxPicSizeInMbs = 396; + break; + + case 20: + tmp = 912384; + maxPicSizeInMbs = 396; + break; + + case 21: + tmp = 1824768; + maxPicSizeInMbs = 792; + break; + + case 22: + tmp = 3110400; + maxPicSizeInMbs = 1620; + break; + + case 30: + tmp = 3110400; + maxPicSizeInMbs = 1620; + break; + + case 31: + tmp = 6912000; + maxPicSizeInMbs = 3600; + break; + + case 32: + tmp = 7864320; + maxPicSizeInMbs = 5120; + break; + + case 40: + tmp = 12582912; + maxPicSizeInMbs = 8192; + break; + + case 41: + tmp = 12582912; + maxPicSizeInMbs = 8192; + break; + + case 42: + tmp = 34816*384; + maxPicSizeInMbs = 8704; + break; + + case 50: + /* standard says 42301440 here, but corrigendum "corrects" this to + * 42393600 */ + tmp = 42393600; + maxPicSizeInMbs = 22080; + break; + + case 51: + tmp = 70778880; + maxPicSizeInMbs = 36864; + break; + + default: + return(INVALID_DPB_SIZE); + } + + /* this is not "correct" return value! However, it results in error in + * decoding and this was easiest place to check picture size */ + if (picSizeInMbs > maxPicSizeInMbs) + return(INVALID_DPB_SIZE); + + tmp /= (picSizeInMbs*384); + + return(MIN(tmp, 16)); + +} + +/*------------------------------------------------------------------------------ + + Function name: h264bsdCompareSeqParamSets + + Functional description: + Compare two sequence parameter sets. + + Inputs: + pSps1 pointer to a sequence parameter set + pSps2 pointer to another sequence parameter set + + Outputs: + 0 sequence parameter sets are equal + 1 otherwise + +------------------------------------------------------------------------------*/ + +u32 h264bsdCompareSeqParamSets(seqParamSet_t *pSps1, seqParamSet_t *pSps2) +{ + +/* Variables */ + + u32 i; + +/* Code */ + + ASSERT(pSps1); + ASSERT(pSps2); + + /* first compare parameters whose existence does not depend on other + * parameters and only compare the rest of the params if these are equal */ + if (pSps1->profileIdc == pSps2->profileIdc && + pSps1->levelIdc == pSps2->levelIdc && + pSps1->maxFrameNum == pSps2->maxFrameNum && + pSps1->picOrderCntType == pSps2->picOrderCntType && + pSps1->numRefFrames == pSps2->numRefFrames && + pSps1->gapsInFrameNumValueAllowedFlag == + pSps2->gapsInFrameNumValueAllowedFlag && + pSps1->picWidthInMbs == pSps2->picWidthInMbs && + pSps1->picHeightInMbs == pSps2->picHeightInMbs && + pSps1->frameCroppingFlag == pSps2->frameCroppingFlag && + pSps1->vuiParametersPresentFlag == pSps2->vuiParametersPresentFlag) + { + if (pSps1->picOrderCntType == 0) + { + if (pSps1->maxPicOrderCntLsb != pSps2->maxPicOrderCntLsb) + return 1; + } + else if (pSps1->picOrderCntType == 1) + { + if (pSps1->deltaPicOrderAlwaysZeroFlag != + pSps2->deltaPicOrderAlwaysZeroFlag || + pSps1->offsetForNonRefPic != pSps2->offsetForNonRefPic || + pSps1->offsetForTopToBottomField != + pSps2->offsetForTopToBottomField || + pSps1->numRefFramesInPicOrderCntCycle != + pSps2->numRefFramesInPicOrderCntCycle) + { + return 1; + } + else + { + for (i = 0; i < pSps1->numRefFramesInPicOrderCntCycle; i++) + if (pSps1->offsetForRefFrame[i] != + pSps2->offsetForRefFrame[i]) + { + return 1; + } + } + } + if (pSps1->frameCroppingFlag) + { + if (pSps1->frameCropLeftOffset != pSps2->frameCropLeftOffset || + pSps1->frameCropRightOffset != pSps2->frameCropRightOffset || + pSps1->frameCropTopOffset != pSps2->frameCropTopOffset || + pSps1->frameCropBottomOffset != pSps2->frameCropBottomOffset) + { + return 1; + } + } + + return 0; + } + + return 1; +} + diff --git a/src/video/h264_decoder/h264bsd_seq_param_set.h b/src/video/h264_decoder/h264bsd_seq_param_set.h new file mode 100644 index 000000000..e18df9497 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_seq_param_set.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_SEQ_PARAM_SET_H +#define H264SWDEC_SEQ_PARAM_SET_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_stream.h" +#include "h264bsd_vui.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/* structure to store sequence parameter set information decoded from the + * stream */ +typedef struct +{ + u32 profileIdc; + u32 levelIdc; + u32 seqParameterSetId; + u32 maxFrameNum; + u32 picOrderCntType; + u32 maxPicOrderCntLsb; + u32 deltaPicOrderAlwaysZeroFlag; + i32 offsetForNonRefPic; + i32 offsetForTopToBottomField; + u32 numRefFramesInPicOrderCntCycle; + i32 *offsetForRefFrame; + u32 numRefFrames; + u32 gapsInFrameNumValueAllowedFlag; + u32 picWidthInMbs; + u32 picHeightInMbs; + u32 frameCroppingFlag; + u32 frameCropLeftOffset; + u32 frameCropRightOffset; + u32 frameCropTopOffset; + u32 frameCropBottomOffset; + u32 vuiParametersPresentFlag; + vuiParameters_t *vuiParameters; + u32 maxDpbSize; +} seqParamSet_t; + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeSeqParamSet(strmData_t *pStrmData, + seqParamSet_t *pSeqParamSet); + +u32 h264bsdCompareSeqParamSets(seqParamSet_t *pSps1, seqParamSet_t *pSps2); + +#endif /* #ifdef H264SWDEC_SEQ_PARAM_SET_H */ + diff --git a/src/video/h264_decoder/h264bsd_slice_data.c b/src/video/h264_decoder/h264bsd_slice_data.c new file mode 100644 index 000000000..84324e3a2 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_slice_data.c @@ -0,0 +1,404 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdDecodeSliceData + SetMbParams + h264bsdMarkSliceCorrupted + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_slice_data.h" +#include "h264bsd_inter_prediction.h" +#include "h264bsd_util.h" +#include "h264bsd_vlc.h" + +#include "../profile.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +static void SetMbParams(mbStorage_t *pMb, sliceHeader_t *pSlice, u32 sliceId, + i32 chromaQpIndexOffset); + +/*------------------------------------------------------------------------------ + + 5.1 Function name: h264bsdDecodeSliceData + + Functional description: + Decode one slice. Function decodes stream data, i.e. macroblocks + and possible skip_run fields. h264bsdDecodeMacroblock function is + called to handle all other macroblock related processing. + Macroblock to slice group mapping is considered when next + macroblock to process is determined (h264bsdNextMbAddress function) + map + + Inputs: + pStrmData pointer to stream data structure + pStorage pointer to storage structure + currImage pointer to current processed picture, needed for + intra prediction of the macroblocks + pSliceHeader pointer to slice header of the current slice + + Outputs: + currImage processed macroblocks are written to current image + pStorage mbStorage structure of each processed macroblock + is updated here + + Returns: + HANTRO_OK success + HANTRO_NOK invalid stream data + +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeSliceData(strmData_t *pStrmData, storage_t *pStorage, + image_t *currImage, sliceHeader_t *pSliceHeader) +{ + +/* Variables */ + + u32 tmp; + u32 skipRun; + u32 prevSkipped; + u32 currMbAddr; + u32 moreMbs; + u32 mbCount; + u32 budget; + u32 doneThisCall; + i32 qpY; + macroblockLayer_t *mbLayer; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(pSliceHeader); + ASSERT(pStorage); + ASSERT(pSliceHeader->firstMbInSlice < pStorage->picSizeInMbs); + + if (pStorage->sliceDataPending) + { + currMbAddr = pStorage->sliceMbAddr; + skipRun = pStorage->sliceSkipRun; + prevSkipped = pStorage->slicePrevSkipped; + mbCount = pStorage->sliceMbCount; + qpY = pStorage->sliceQpY; + pStorage->sliceDataPending = 0; + } + else + { + currMbAddr = pSliceHeader->firstMbInSlice; + skipRun = 0; + prevSkipped = HANTRO_FALSE; + mbCount = 0; + qpY = (i32)pStorage->activePps->picInitQp + pSliceHeader->sliceQpDelta; + +#ifdef H264BSD_N64 + h264bsdPrepareWeights(pSliceHeader); +#endif + + /* increment slice index, will be one for decoding of the first slice of + * the picture */ + pStorage->slice->sliceId++; + + /* lastMbAddr stores address of the macroblock that was last successfully + * decoded, needed for error handling */ + pStorage->slice->lastMbAddr = 0; + } + + budget = pStorage->sliceMbBudget; + doneThisCall = 0; + do + { + mbLayer = &pStorage->mbLayers[pStorage->mbLayerIdx]; + +#ifndef OPTIMIZE_NO_DECODED_FLAG + /* primary picture and already decoded macroblock -> error */ + if (!pSliceHeader->redundantPicCnt && pStorage->mb[currMbAddr].decoded) + { + EPRINT("Primary and already decoded"); + return(HANTRO_NOK); + } +#endif + + SetMbParams(pStorage->mb + currMbAddr, pSliceHeader, + pStorage->slice->sliceId, pStorage->activePps->chromaQpIndexOffset); + + if (!IS_I_SLICE(pSliceHeader->sliceType)) + { + if (!prevSkipped) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &skipRun); + if (tmp != HANTRO_OK) + return(tmp); + /* skip_run shall be less than or equal to number of + * macroblocks left */ + if (skipRun > (pStorage->picSizeInMbs - currMbAddr)) + { + EPRINT("skip_run"); + return(HANTRO_NOK); + } + if (skipRun) + { + prevSkipped = HANTRO_TRUE; + H264SwDecMemset(&mbLayer->mbPred, 0, sizeof(mbPred_t)); + /* mark current macroblock skipped */ + mbLayer->mbType = P_Skip; + } + } + } + + PROFILE_START(PS_H264_LAYER); + if (skipRun) + { + //DEBUG(("Skipping macroblock %d\n", currMbAddr)); + skipRun--; + } + else + { + prevSkipped = HANTRO_FALSE; + tmp = h264bsdDecodeMacroblockLayer(pStrmData, mbLayer, + pStorage->mb + currMbAddr, pSliceHeader->sliceType, + pSliceHeader->numRefIdxL0Active); + if (tmp != HANTRO_OK) + { + EPRINT("macroblock_layer"); + return(tmp); + } + } + PROFILE_STOP(PS_H264_LAYER); + + PROFILE_START(PS_H264_MACROB); + tmp = h264bsdDecodeMacroblock(pStorage->mb + currMbAddr, mbLayer, + currImage, pStorage->dpb, &qpY, currMbAddr, + pStorage->activePps->constrainedIntraPredFlag, pSliceHeader); + PROFILE_STOP(PS_H264_MACROB); + if (tmp != HANTRO_OK) + { + EPRINT("MACRO_BLOCK"); + return(tmp); + } + + if (!prevSkipped) { + // Update the stream buffer read bits counter. + #ifndef H264BSD_N64 + pStrmData->strmBuffReadBits = + (u32)(pStrmData->pStrmCurrPos - pStrmData->pStrmBuffStart) * 8 + + pStrmData->bitPosInWord; + #endif + } + + /* increment macroblock count only for macroblocks that were decoded + * for the first time (redundant slices) */ +#ifndef OPTIMIZE_NO_DECODED_FLAG + if (pStorage->mb[currMbAddr].decoded == 1) +#endif + mbCount++; + doneThisCall++; + + /* keep on processing as long as there is stream data left or + * processing of macroblocks to be skipped based on the last skipRun is + * not finished */ + moreMbs = (h264bsdMoreRbspData(pStrmData) || skipRun) ? + HANTRO_TRUE : HANTRO_FALSE; + + /* lastMbAddr is only updated for intra slices (all macroblocks of + * inter slices will be lost in case of an error) */ + if (IS_I_SLICE(pSliceHeader->sliceType)) + pStorage->slice->lastMbAddr = currMbAddr; + + currMbAddr = h264bsdNextMbAddress(pStorage->sliceGroupMap, + pStorage->picSizeInMbs, currMbAddr); + if (!prevSkipped) + pStorage->mbLayerIdx = (pStorage->mbLayerIdx+1)%NUM_PARALLEL_MACROBLOCKS; + /* data left in the buffer but no more macroblocks for current slice + * group -> error */ + if (moreMbs && !currMbAddr) + { + EPRINT("Next mb address"); + return(HANTRO_NOK); + } + + /* Timeslice: pause after the budget so poll() can return to the game. + * State is kept in storage; strm bit position stays in *pStrmData. */ + if (budget && moreMbs && doneThisCall >= budget) + { + pStorage->sliceDataPending = 1; + pStorage->sliceMbAddr = currMbAddr; + pStorage->sliceSkipRun = skipRun; + pStorage->slicePrevSkipped = prevSkipped; + pStorage->sliceMbCount = mbCount; + pStorage->sliceQpY = qpY; + return(HANTRO_OK); + } + + } while (moreMbs); + + if ((pStorage->slice->numDecodedMbs + mbCount) > pStorage->picSizeInMbs) + { + EPRINT("Num decoded mbs"); + return(HANTRO_NOK); + } + + pStorage->slice->numDecodedMbs += mbCount; + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + 5.2 Function: SetMbParams + + Functional description: + Set macroblock parameters that remain constant for this slice + + Inputs: + pSlice pointer to current slice header + sliceId id of the current slice + chromaQpIndexOffset + + Outputs: + pMb pointer to macroblock structure which is updated + + Returns: + none + +------------------------------------------------------------------------------*/ + +void SetMbParams(mbStorage_t *pMb, sliceHeader_t *pSlice, u32 sliceId, + i32 chromaQpIndexOffset) +{ + +/* Variables */ + u32 tmp1; + i32 tmp2, tmp3; + +/* Code */ + + tmp1 = pSlice->disableDeblockingFilterIdc; + tmp2 = pSlice->sliceAlphaC0Offset; + tmp3 = pSlice->sliceBetaOffset; + pMb->sliceId = sliceId; + pMb->disableDeblockingFilterIdc = tmp1; + pMb->filterOffsetA = tmp2; + pMb->filterOffsetB = tmp3; + pMb->chromaQpIndexOffset = chromaQpIndexOffset; + +} + +/*------------------------------------------------------------------------------ + + 5.3 Function name: h264bsdMarkSliceCorrupted + + Functional description: + Mark macroblocks of the slice corrupted. If lastMbAddr in the slice + storage is set -> picWidhtInMbs (or at least 10) macroblocks back + from the lastMbAddr are marked corrupted. However, if lastMbAddr + is not set -> all macroblocks of the slice are marked. + + Inputs: + pStorage pointer to storage structure + firstMbInSlice address of the first macroblock in the slice, this + identifies the slice to be marked corrupted + + Outputs: + pStorage mbStorage for the corrupted macroblocks updated + + Returns: + none + +------------------------------------------------------------------------------*/ + +#ifndef OPTIMIZE_NO_DECODED_FLAG +void h264bsdMarkSliceCorrupted(storage_t *pStorage, u32 firstMbInSlice) +{ + +/* Variables */ + + u32 tmp, i; + u32 sliceId; + u32 currMbAddr; + +/* Code */ + + ASSERT(pStorage); + ASSERT(firstMbInSlice < pStorage->picSizeInMbs); + + currMbAddr = firstMbInSlice; + + sliceId = pStorage->slice->sliceId; + + /* DecodeSliceData sets lastMbAddr for I slices -> if it was set, go back + * MAX(picWidthInMbs, 10) macroblocks and start marking from there */ + if (pStorage->slice->lastMbAddr) + { + ASSERT(pStorage->mb[pStorage->slice->lastMbAddr].sliceId == sliceId); + i = pStorage->slice->lastMbAddr - 1; + tmp = 0; + while (i > currMbAddr) + { + if (pStorage->mb[i].sliceId == sliceId) + { + tmp++; + if (tmp >= MAX(pStorage->activeSps->picWidthInMbs, 10)) + break; + } + i--; + } + currMbAddr = i; + } + + do + { + + if ( (pStorage->mb[currMbAddr].sliceId == sliceId) && + (pStorage->mb[currMbAddr].decoded) ) + { + pStorage->mb[currMbAddr].decoded--; + } + else + { + break; + } + + currMbAddr = h264bsdNextMbAddress(pStorage->sliceGroupMap, + pStorage->picSizeInMbs, currMbAddr); + + } while (currMbAddr); + +} +#endif diff --git a/src/video/h264_decoder/h264bsd_slice_data.h b/src/video/h264_decoder/h264bsd_slice_data.h new file mode 100644 index 000000000..f23d49ec7 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_slice_data.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_SLICE_DATA_H +#define H264SWDEC_SLICE_DATA_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_stream.h" +#include "h264bsd_cfg.h" +#include "h264bsd_slice_header.h" +#include "h264bsd_storage.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeSliceData(strmData_t *pStrmData, storage_t *pStorage, + image_t *currImage, sliceHeader_t *pSliceHeader); + +void h264bsdMarkSliceCorrupted(storage_t *pStorage, u32 firstMbInSlice); + +#endif /* #ifdef H264SWDEC_SLICE_DATA_H */ + diff --git a/src/video/h264_decoder/h264bsd_slice_group_map.c b/src/video/h264_decoder/h264bsd_slice_group_map.c new file mode 100644 index 000000000..7cbb53415 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_slice_group_map.c @@ -0,0 +1,589 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + DecodeInterleavedMap + DecodeDispersedMap + DecodeForegroundLeftOverMap + DecodeBoxOutMap + DecodeRasterScanMap + DecodeWipeMap + h264bsdDecodeSliceGroupMap + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_slice_group_map.h" +#include "h264bsd_cfg.h" +#include "h264bsd_pic_param_set.h" +#include "h264bsd_util.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +static void DecodeInterleavedMap( + u32 *map, + u32 numSliceGroups, + u32 *runLength, + u32 picSize); + +static void DecodeDispersedMap( + u32 *map, + u32 numSliceGroups, + u32 picWidth, + u32 picHeight); + +static void DecodeForegroundLeftOverMap( + u32 *map, + u32 numSliceGroups, + u32 *topLeft, + u32 *bottomRight, + u32 picWidth, + u32 picHeight); + +static void DecodeBoxOutMap( + u32 *map, + u32 sliceGroupChangeDirectionFlag, + u32 unitsInSliceGroup0, + u32 picWidth, + u32 picHeight); + +static void DecodeRasterScanMap( + u32 *map, + u32 sliceGroupChangeDirectionFlag, + u32 sizeOfUpperLeftGroup, + u32 picSize); + +static void DecodeWipeMap( + u32 *map, + u32 sliceGroupChangeDirectionFlag, + u32 sizeOfUpperLeftGroup, + u32 picWidth, + u32 picHeight); + +/*------------------------------------------------------------------------------ + + Function: DecodeInterleavedMap + + Functional description: + Function to decode interleaved slice group map type, i.e. slice + group map type 0. + + Inputs: + map pointer to the map + numSliceGroups number of slice groups + runLength run_length[] values for each slice group + picSize picture size in macroblocks + + Outputs: + map slice group map is stored here + + Returns: + none + +------------------------------------------------------------------------------*/ + +void DecodeInterleavedMap( + u32 *map, + u32 numSliceGroups, + u32 *runLength, + u32 picSize) +{ + +/* Variables */ + + u32 i,j, group; + +/* Code */ + + ASSERT(map); + ASSERT(numSliceGroups >= 1 && numSliceGroups <= MAX_NUM_SLICE_GROUPS); + ASSERT(runLength); + + i = 0; + + do { + for (group = 0; group < numSliceGroups && i < picSize; + i += runLength[group++]) + { + ASSERT(runLength[group] <= picSize); + for (j = 0; j < runLength[group] && i + j < picSize; j++) + map[i+j] = group; + } + } while (i < picSize); + + +} + +/*------------------------------------------------------------------------------ + + Function: DecodeDispersedMap + + Functional description: + Function to decode dispersed slice group map type, i.e. slice + group map type 1. + + Inputs: + map pointer to the map + numSliceGroups number of slice groups + picWidth picture width in macroblocks + picHeight picture height in macroblocks + + Outputs: + map slice group map is stored here + + Returns: + none + +------------------------------------------------------------------------------*/ + +void DecodeDispersedMap( + u32 *map, + u32 numSliceGroups, + u32 picWidth, + u32 picHeight) +{ + +/* Variables */ + + u32 i, picSize; + +/* Code */ + + ASSERT(map); + ASSERT(numSliceGroups >= 1 && numSliceGroups <= MAX_NUM_SLICE_GROUPS); + ASSERT(picWidth); + ASSERT(picHeight); + + picSize = picWidth * picHeight; + + for (i = 0; i < picSize; i++) + map[i] = ((i % picWidth) + (((i / picWidth) * numSliceGroups) >> 1)) % + numSliceGroups; + + +} + +/*------------------------------------------------------------------------------ + + Function: DecodeForegroundLeftOverMap + + Functional description: + Function to decode foreground with left-over slice group map type, + i.e. slice group map type 2. + + Inputs: + map pointer to the map + numSliceGroups number of slice groups + topLeft top_left[] values + bottomRight bottom_right[] values + picWidth picture width in macroblocks + picHeight picture height in macroblocks + + Outputs: + map slice group map is stored here + + Returns: + none + +------------------------------------------------------------------------------*/ + +void DecodeForegroundLeftOverMap( + u32 *map, + u32 numSliceGroups, + u32 *topLeft, + u32 *bottomRight, + u32 picWidth, + u32 picHeight) +{ + +/* Variables */ + + u32 i,y,x,yTopLeft,yBottomRight,xTopLeft,xBottomRight, picSize; + u32 group; + +/* Code */ + + ASSERT(map); + ASSERT(numSliceGroups >= 1 && numSliceGroups <= MAX_NUM_SLICE_GROUPS); + ASSERT(topLeft); + ASSERT(bottomRight); + ASSERT(picWidth); + ASSERT(picHeight); + + picSize = picWidth * picHeight; + + for (i = 0; i < picSize; i++) + map[i] = numSliceGroups - 1; + + for (group = numSliceGroups - 1; group--; ) + { + ASSERT( topLeft[group] <= bottomRight[group] && + bottomRight[group] < picSize ); + yTopLeft = topLeft[group] / picWidth; + xTopLeft = topLeft[group] % picWidth; + yBottomRight = bottomRight[group] / picWidth; + xBottomRight = bottomRight[group] % picWidth; + ASSERT(xTopLeft <= xBottomRight); + + for (y = yTopLeft; y <= yBottomRight; y++) + for (x = xTopLeft; x <= xBottomRight; x++) + map[ y * picWidth + x ] = group; + } + + +} + +/*------------------------------------------------------------------------------ + + Function: DecodeBoxOutMap + + Functional description: + Function to decode box-out slice group map type, i.e. slice group + map type 3. + + Inputs: + map pointer to the map + sliceGroupChangeDirectionFlag slice_group_change_direction_flag + unitsInSliceGroup0 mbs on slice group 0 + picWidth picture width in macroblocks + picHeight picture height in macroblocks + + Outputs: + map slice group map is stored here + + Returns: + none + +------------------------------------------------------------------------------*/ + +void DecodeBoxOutMap( + u32 *map, + u32 sliceGroupChangeDirectionFlag, + u32 unitsInSliceGroup0, + u32 picWidth, + u32 picHeight) +{ + +/* Variables */ + + u32 i, k, picSize; + i32 x, y, xDir, yDir, leftBound, topBound, rightBound, bottomBound; + u32 mapUnitVacant; + +/* Code */ + + ASSERT(map); + ASSERT(picWidth); + ASSERT(picHeight); + + picSize = picWidth * picHeight; + ASSERT(unitsInSliceGroup0 <= picSize); + + for (i = 0; i < picSize; i++) + map[i] = 1; + + x = (picWidth - (u32)sliceGroupChangeDirectionFlag) >> 1; + y = (picHeight - (u32)sliceGroupChangeDirectionFlag) >> 1; + + leftBound = x; + topBound = y; + + rightBound = x; + bottomBound = y; + + xDir = (i32)sliceGroupChangeDirectionFlag - 1; + yDir = (i32)sliceGroupChangeDirectionFlag; + + for (k = 0; k < unitsInSliceGroup0; k += mapUnitVacant ? 1 : 0) + { + mapUnitVacant = (map[ (u32)y * picWidth + (u32)x ] == 1) ? + HANTRO_TRUE : HANTRO_FALSE; + + if (mapUnitVacant) + map[ (u32)y * picWidth + (u32)x ] = 0; + + if (xDir == -1 && x == leftBound) + { + leftBound = MAX(leftBound - 1, 0); + x = leftBound; + xDir = 0; + yDir = 2 * (i32)sliceGroupChangeDirectionFlag - 1; + } + else if (xDir == 1 && x == rightBound) + { + rightBound = MIN(rightBound + 1, (i32)picWidth - 1); + x = rightBound; + xDir = 0; + yDir = 1 - 2 * (i32)sliceGroupChangeDirectionFlag; + } + else if (yDir == -1 && y == topBound) + { + topBound = MAX(topBound - 1, 0); + y = topBound; + xDir = 1 - 2 * (i32)sliceGroupChangeDirectionFlag; + yDir = 0; + } + else if (yDir == 1 && y == bottomBound) + { + bottomBound = MIN(bottomBound + 1, (i32)picHeight - 1); + y = bottomBound; + xDir = 2 * (i32)sliceGroupChangeDirectionFlag - 1; + yDir = 0; + } + else + { + x += xDir; + y += yDir; + } + } + + +} + +/*------------------------------------------------------------------------------ + + Function: DecodeRasterScanMap + + Functional description: + Function to decode raster scan slice group map type, i.e. slice + group map type 4. + + Inputs: + map pointer to the map + sliceGroupChangeDirectionFlag slice_group_change_direction_flag + sizeOfUpperLeftGroup mbs in upperLeftGroup + picSize picture size in macroblocks + + Outputs: + map slice group map is stored here + + Returns: + none + +------------------------------------------------------------------------------*/ + +void DecodeRasterScanMap( + u32 *map, + u32 sliceGroupChangeDirectionFlag, + u32 sizeOfUpperLeftGroup, + u32 picSize) +{ + +/* Variables */ + + u32 i; + +/* Code */ + + ASSERT(map); + ASSERT(picSize); + ASSERT(sizeOfUpperLeftGroup <= picSize); + + for (i = 0; i < picSize; i++) + if (i < sizeOfUpperLeftGroup) + map[i] = (u32)sliceGroupChangeDirectionFlag; + else + map[i] = 1 - (u32)sliceGroupChangeDirectionFlag; + + +} + +/*------------------------------------------------------------------------------ + + Function: DecodeWipeMap + + Functional description: + Function to decode wipe slice group map type, i.e. slice group map + type 5. + + Inputs: + sliceGroupChangeDirectionFlag slice_group_change_direction_flag + sizeOfUpperLeftGroup mbs in upperLeftGroup + picWidth picture width in macroblocks + picHeight picture height in macroblocks + + Outputs: + map slice group map is stored here + + Returns: + none + +------------------------------------------------------------------------------*/ + +void DecodeWipeMap( + u32 *map, + u32 sliceGroupChangeDirectionFlag, + u32 sizeOfUpperLeftGroup, + u32 picWidth, + u32 picHeight) +{ + +/* Variables */ + + u32 i,j,k; + +/* Code */ + + ASSERT(map); + ASSERT(picWidth); + ASSERT(picHeight); + ASSERT(sizeOfUpperLeftGroup <= picWidth * picHeight); + + k = 0; + for (j = 0; j < picWidth; j++) + for (i = 0; i < picHeight; i++) + if (k++ < sizeOfUpperLeftGroup) + map[ i * picWidth + j ] = (u32)sliceGroupChangeDirectionFlag; + else + map[ i * picWidth + j ] = 1 - + (u32)sliceGroupChangeDirectionFlag; + + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdDecodeSliceGroupMap + + Functional description: + Function to decode macroblock to slice group map. Construction + of different slice group map types is handled by separate + functions defined above. See standard for details how slice group + maps are computed. + + Inputs: + pps active picture parameter set + sliceGroupChangeCycle slice_group_change_cycle + picWidth picture width in macroblocks + picHeight picture height in macroblocks + + Outputs: + map slice group map is stored here + + Returns: + none + +------------------------------------------------------------------------------*/ + +void h264bsdDecodeSliceGroupMap( + u32 *map, + picParamSet_t *pps, + u32 sliceGroupChangeCycle, + u32 picWidth, + u32 picHeight) +{ + +/* Variables */ + + u32 i, picSize, unitsInSliceGroup0 = 0, sizeOfUpperLeftGroup = 0; + +/* Code */ + + ASSERT(map); + ASSERT(pps); + ASSERT(picWidth); + ASSERT(picHeight); + ASSERT(pps->sliceGroupMapType < 7); + + picSize = picWidth * picHeight; + + /* just one slice group -> all macroblocks belong to group 0 */ + if (pps->numSliceGroups == 1) + { + H264SwDecMemset(map, 0, picSize * sizeof(u32)); + return; + } + + if (pps->sliceGroupMapType > 2 && pps->sliceGroupMapType < 6) + { + ASSERT(pps->sliceGroupChangeRate && + pps->sliceGroupChangeRate <= picSize); + + unitsInSliceGroup0 = + MIN(sliceGroupChangeCycle * pps->sliceGroupChangeRate, picSize); + + if (pps->sliceGroupMapType == 4 || pps->sliceGroupMapType == 5) + sizeOfUpperLeftGroup = pps->sliceGroupChangeDirectionFlag ? + (picSize - unitsInSliceGroup0) : unitsInSliceGroup0; + } + + switch (pps->sliceGroupMapType) + { + case 0: + DecodeInterleavedMap(map, pps->numSliceGroups, + pps->runLength, picSize); + break; + + case 1: + DecodeDispersedMap(map, pps->numSliceGroups, picWidth, + picHeight); + break; + + case 2: + DecodeForegroundLeftOverMap(map, pps->numSliceGroups, + pps->topLeft, pps->bottomRight, picWidth, picHeight); + break; + + case 3: + DecodeBoxOutMap(map, pps->sliceGroupChangeDirectionFlag, + unitsInSliceGroup0, picWidth, picHeight); + break; + + case 4: + DecodeRasterScanMap(map, + pps->sliceGroupChangeDirectionFlag, sizeOfUpperLeftGroup, + picSize); + break; + + case 5: + DecodeWipeMap(map, pps->sliceGroupChangeDirectionFlag, + sizeOfUpperLeftGroup, picWidth, picHeight); + break; + + default: + ASSERT(pps->sliceGroupId); + for (i = 0; i < picSize; i++) + { + ASSERT(pps->sliceGroupId[i] < pps->numSliceGroups); + map[i] = pps->sliceGroupId[i]; + } + break; + } + +} + diff --git a/src/video/h264_decoder/h264bsd_slice_group_map.h b/src/video/h264_decoder/h264bsd_slice_group_map.h new file mode 100644 index 000000000..4bcb6f2ce --- /dev/null +++ b/src/video/h264_decoder/h264bsd_slice_group_map.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_SLICE_GROUP_MAP_H +#define H264SWDEC_SLICE_GROUP_MAP_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_pic_param_set.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +void h264bsdDecodeSliceGroupMap( + u32 *map, + picParamSet_t *pps, + u32 sliceGroupChangeCycle, + u32 picWidth, + u32 picHeight); + +#endif /* #ifdef H264SWDEC_SLICE_GROUP_MAP_H */ + diff --git a/src/video/h264_decoder/h264bsd_slice_header.c b/src/video/h264_decoder/h264bsd_slice_header.c new file mode 100644 index 000000000..a12491416 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_slice_header.c @@ -0,0 +1,1652 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdDecodeSliceHeader + NumSliceGroupChangeCycleBits + RefPicListReordering + DecRefPicMarking + CheckPpsId + CheckFrameNum + CheckIdrPicId + CheckPicOrderCntLsb + CheckDeltaPicOrderCntBottom + CheckDeltaPicOrderCnt + CheckRedundantPicCnt + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_slice_header.h" +#include "h264bsd_util.h" +#include "h264bsd_vlc.h" +#include "h264bsd_nal_unit.h" +#include "h264bsd_dpb.h" + +#define UNUSED(x) (void)(x) + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +static u32 RefPicListReordering(strmData_t *, refPicListReordering_t *, + u32, u32); + +static u32 NumSliceGroupChangeCycleBits(u32 picSizeInMbs, + u32 sliceGroupChangeRate); + +static u32 DecRefPicMarking(strmData_t *pStrmData, + decRefPicMarking_t *pDecRefPicMarking, nalUnitType_e nalUnitType, + u32 numRefFrames); + +static u32 PredWeightTable(strmData_t *pStrmData, + predWeightTable_t *pPredWeightTable, u32 numRefIdxL0Active); + + +/*------------------------------------------------------------------------------ + + Function name: h264bsdDecodeSliceHeader + + Functional description: + Decode slice header data from the stream. + + Inputs: + pStrmData pointer to stream data structure + pSeqParamSet pointer to active sequence parameter set + pPicParamSet pointer to active picture parameter set + pNalUnit pointer to current NAL unit structure + + Outputs: + pSliceHeader decoded data is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK invalid stream data or end of stream + +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeSliceHeader(strmData_t *pStrmData, sliceHeader_t *pSliceHeader, + seqParamSet_t *pSeqParamSet, picParamSet_t *pPicParamSet, + nalUnit_t *pNalUnit) +{ + +/* Variables */ + + u32 tmp, i, value; + i32 itmp; + u32 picSizeInMbs; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(pSliceHeader); + ASSERT(pSeqParamSet); + ASSERT(pPicParamSet); + ASSERT( pNalUnit->nalUnitType == NAL_CODED_SLICE || + pNalUnit->nalUnitType == NAL_CODED_SLICE_IDR ); + + + H264SwDecMemset(pSliceHeader, 0, sizeof(sliceHeader_t)); + + picSizeInMbs = pSeqParamSet->picWidthInMbs * pSeqParamSet->picHeightInMbs; + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pSliceHeader->firstMbInSlice = value; + if (value >= picSizeInMbs) + { + EPRINT("first_mb_in_slice"); + return(HANTRO_NOK); + } + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pSliceHeader->sliceType = value; + /* slice type has to be either I or P slice. P slice is not allowed when + * current NAL unit is an IDR NAL unit or num_ref_frames is 0 */ + if ( !IS_I_SLICE(pSliceHeader->sliceType) && + ( !IS_P_SLICE(pSliceHeader->sliceType) || + IS_IDR_NAL_UNIT(pNalUnit) || + !pSeqParamSet->numRefFrames ) ) + { + EPRINT("slice_type"); + return(HANTRO_NOK); + } + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pSliceHeader->picParameterSetId = value; + if (pSliceHeader->picParameterSetId != pPicParamSet->picParameterSetId) + { + EPRINT("pic_parameter_set_id"); + return(HANTRO_NOK); + } + pSliceHeader->weightedPredFlag = pPicParamSet->weightedPredFlag; + + /* log2(maxFrameNum) -> num bits to represent frame_num */ + i = 0; + while (pSeqParamSet->maxFrameNum >> i) + i++; + i--; + + tmp = h264bsdGetBits(pStrmData, i); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + if (IS_IDR_NAL_UNIT(pNalUnit) && tmp != 0) + { + EPRINT("frame_num"); + return(HANTRO_NOK); + } + pSliceHeader->frameNum = tmp; + + if (IS_IDR_NAL_UNIT(pNalUnit)) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pSliceHeader->idrPicId = value; + if (value > 65535) + { + EPRINT("idr_pic_id"); + return(HANTRO_NOK); + } + } + + if (pSeqParamSet->picOrderCntType == 0) + { + /* log2(maxPicOrderCntLsb) -> num bits to represent pic_order_cnt_lsb */ + i = 0; + while (pSeqParamSet->maxPicOrderCntLsb >> i) + i++; + i--; + + tmp = h264bsdGetBits(pStrmData, i); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pSliceHeader->picOrderCntLsb = tmp; + + if (pPicParamSet->picOrderPresentFlag) + { + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &itmp); + if (tmp != HANTRO_OK) + return(tmp); + pSliceHeader->deltaPicOrderCntBottom = itmp; + } + + /* check that picOrderCnt for IDR picture will be zero. See + * DecodePicOrderCnt function to understand the logic here */ + if ( IS_IDR_NAL_UNIT(pNalUnit) && + ( (pSliceHeader->picOrderCntLsb > + pSeqParamSet->maxPicOrderCntLsb/2) || + MIN((i32)pSliceHeader->picOrderCntLsb, + (i32)pSliceHeader->picOrderCntLsb + + pSliceHeader->deltaPicOrderCntBottom) != 0 ) ) + { + return(HANTRO_NOK); + } + } + + if ( (pSeqParamSet->picOrderCntType == 1) && + !pSeqParamSet->deltaPicOrderAlwaysZeroFlag ) + { + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &itmp); + if (tmp != HANTRO_OK) + return(tmp); + pSliceHeader->deltaPicOrderCnt[0] = itmp; + + if (pPicParamSet->picOrderPresentFlag) + { + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &itmp); + if (tmp != HANTRO_OK) + return(tmp); + pSliceHeader->deltaPicOrderCnt[1] = itmp; + } + + /* check that picOrderCnt for IDR picture will be zero. See + * DecodePicOrderCnt function to understand the logic here */ + if ( IS_IDR_NAL_UNIT(pNalUnit) && + MIN(pSliceHeader->deltaPicOrderCnt[0], + pSliceHeader->deltaPicOrderCnt[0] + + pSeqParamSet->offsetForTopToBottomField + + pSliceHeader->deltaPicOrderCnt[1]) != 0) + { + return(HANTRO_NOK); + } + } + + if (pPicParamSet->redundantPicCntPresentFlag) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pSliceHeader->redundantPicCnt = value; + if (value > 127) + { + EPRINT("redundant_pic_cnt"); + return(HANTRO_NOK); + } + } + + if (IS_P_SLICE(pSliceHeader->sliceType)) + { + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pSliceHeader->numRefIdxActiveOverrideFlag = tmp; + + if (pSliceHeader->numRefIdxActiveOverrideFlag) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + if (value > 15) + { + EPRINT("num_ref_idx_l0_active_minus1"); + return(HANTRO_NOK); + } + pSliceHeader->numRefIdxL0Active = value + 1; + } + /* set numRefIdxL0Active from pic param set */ + else + { + /* if value (minus1) in picture parameter set exceeds 15 it should + * have been overridden here */ + if (pPicParamSet->numRefIdxL0Active > 16) + { + EPRINT("num_ref_idx_active_override_flag"); + return(HANTRO_NOK); + } + pSliceHeader->numRefIdxL0Active = pPicParamSet->numRefIdxL0Active; + } + } + + if (IS_P_SLICE(pSliceHeader->sliceType)) + { + tmp = RefPicListReordering(pStrmData, + &pSliceHeader->refPicListReordering, + pSliceHeader->numRefIdxL0Active, + pSeqParamSet->maxFrameNum); + if (tmp != HANTRO_OK) + return(tmp); + + if (pSliceHeader->weightedPredFlag) + { + tmp = PredWeightTable(pStrmData, &pSliceHeader->predWeightTable, + pSliceHeader->numRefIdxL0Active); + if (tmp != HANTRO_OK) + return(tmp); + } + } + + if (pNalUnit->nalRefIdc != 0) + { + tmp = DecRefPicMarking(pStrmData, &pSliceHeader->decRefPicMarking, + pNalUnit->nalUnitType, pSeqParamSet->numRefFrames); + if (tmp != HANTRO_OK) + return(tmp); + } + + /* decode sliceQpDelta and check that initial QP for the slice will be on + * the range [0, 51] */ + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &itmp); + if (tmp != HANTRO_OK) + return(tmp); + pSliceHeader->sliceQpDelta = itmp; + itmp += (i32)pPicParamSet->picInitQp; + if ( (itmp < 0) || (itmp > 51) ) + { + EPRINT("slice_qp_delta"); + return(HANTRO_NOK); + } + + if (pPicParamSet->deblockingFilterControlPresentFlag) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pSliceHeader->disableDeblockingFilterIdc = value; + if (pSliceHeader->disableDeblockingFilterIdc > 2) + { + EPRINT("disable_deblocking_filter_idc"); + return(HANTRO_NOK); + } + + if (pSliceHeader->disableDeblockingFilterIdc != 1) + { + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &itmp); + if (tmp != HANTRO_OK) + return(tmp); + if ( (itmp < -6) || (itmp > 6) ) + { + EPRINT("slice_alpha_c0_offset_div2"); + return(HANTRO_NOK); + } + pSliceHeader->sliceAlphaC0Offset = itmp * 2; + + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &itmp); + if (tmp != HANTRO_OK) + return(tmp); + if ( (itmp < -6) || (itmp > 6) ) + { + EPRINT("slice_beta_offset_div2"); + return(HANTRO_NOK); + } + pSliceHeader->sliceBetaOffset = itmp * 2; + } + } + + if ( (pPicParamSet->numSliceGroups > 1) && + (pPicParamSet->sliceGroupMapType >= 3) && + (pPicParamSet->sliceGroupMapType <= 5) ) + { + /* set tmp to number of bits used to represent slice_group_change_cycle + * in the stream */ + tmp = NumSliceGroupChangeCycleBits(picSizeInMbs, + pPicParamSet->sliceGroupChangeRate); + value = h264bsdGetBits(pStrmData, tmp); + if (value == END_OF_STREAM) + return(HANTRO_NOK); + pSliceHeader->sliceGroupChangeCycle = value; + + /* corresponds to tmp = Ceil(picSizeInMbs / sliceGroupChangeRate) */ + tmp = (picSizeInMbs + pPicParamSet->sliceGroupChangeRate - 1) / + pPicParamSet->sliceGroupChangeRate; + if (pSliceHeader->sliceGroupChangeCycle > tmp) + { + EPRINT("slice_group_change_cycle"); + return(HANTRO_NOK); + } + } + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: PredWeightTable + + Functional description: + Decode pred_weight_table syntax elements for P-slices. + Initializes default weights/offsets for all reference indices and + overrides entries present in the bitstream for list0 references. + + Inputs: + pStrmData pointer to stream data structure + numRefIdxL0Active number of active list0 references + + Outputs: + pPredWeightTable decoded weight/offset data + + Returns: + HANTRO_OK success + HANTRO_NOK invalid stream data + +------------------------------------------------------------------------------*/ +static u32 PredWeightTable(strmData_t *pStrmData, + predWeightTable_t *pPredWeightTable, u32 numRefIdxL0Active) +{ + u32 i, j, tmp, value; + i32 svalue; + i32 defaultLumaWeight, defaultChromaWeight; + + ASSERT(pStrmData); + ASSERT(pPredWeightTable); + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pPredWeightTable->lumaLog2WeightDenom); + if (tmp != HANTRO_OK) + return(tmp); + if (pPredWeightTable->lumaLog2WeightDenom > 7) + { + EPRINT("luma_log2_weight_denom"); + return(HANTRO_NOK); + } + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pPredWeightTable->chromaLog2WeightDenom); + if (tmp != HANTRO_OK) + return(tmp); + if (pPredWeightTable->chromaLog2WeightDenom > 7) + { + EPRINT("chroma_log2_weight_denom"); + return(HANTRO_NOK); + } + + defaultLumaWeight = 1 << pPredWeightTable->lumaLog2WeightDenom; + defaultChromaWeight = 1 << pPredWeightTable->chromaLog2WeightDenom; + + for (i = 0; i < MAX_NUM_REF_PICS; i++) + { + pPredWeightTable->lumaWeightL0[i] = defaultLumaWeight; + pPredWeightTable->lumaOffsetL0[i] = 0; + for (j = 0; j < 2; j++) + { + pPredWeightTable->chromaWeightL0[i][j] = defaultChromaWeight; + pPredWeightTable->chromaOffsetL0[i][j] = 0; + } + } + + for (i = 0; i < numRefIdxL0Active; i++) + { + value = h264bsdGetBits(pStrmData, 1); + if (value == END_OF_STREAM) + return(HANTRO_NOK); + if (value) + { + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &svalue); + if (tmp != HANTRO_OK) + return(tmp); + if (svalue < -128 || svalue > 127) + { + EPRINT("luma_weight_l0"); + return(HANTRO_NOK); + } + pPredWeightTable->lumaWeightL0[i] = svalue; + + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &svalue); + if (tmp != HANTRO_OK) + return(tmp); + if (svalue < -128 || svalue > 127) + { + EPRINT("luma_offset_l0"); + return(HANTRO_NOK); + } + pPredWeightTable->lumaOffsetL0[i] = svalue; + } + + value = h264bsdGetBits(pStrmData, 1); + if (value == END_OF_STREAM) + return(HANTRO_NOK); + if (value) + { + for (j = 0; j < 2; j++) + { + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &svalue); + if (tmp != HANTRO_OK) + return(tmp); + if (svalue < -128 || svalue > 127) + { + EPRINT("chroma_weight_l0"); + return(HANTRO_NOK); + } + pPredWeightTable->chromaWeightL0[i][j] = svalue; + + tmp = h264bsdDecodeExpGolombSigned(pStrmData, &svalue); + if (tmp != HANTRO_OK) + return(tmp); + if (svalue < -128 || svalue > 127) + { + EPRINT("chroma_offset_l0"); + return(HANTRO_NOK); + } + pPredWeightTable->chromaOffsetL0[i][j] = svalue; + } + } + } + + return(HANTRO_OK); +} + +/*------------------------------------------------------------------------------ + + Function: NumSliceGroupChangeCycleBits + + Functional description: + Determine number of bits needed to represent + slice_group_change_cycle in the stream. The standard states that + slice_group_change_cycle is represented by + Ceil( Log2( (picSizeInMbs / sliceGroupChangeRate) + 1) ) + + bits. Division "/" in the equation is non-truncating division. + + Inputs: + picSizeInMbs picture size in macroblocks + sliceGroupChangeRate + + Outputs: + none + + Returns: + number of bits needed + +------------------------------------------------------------------------------*/ + +u32 NumSliceGroupChangeCycleBits(u32 picSizeInMbs, u32 sliceGroupChangeRate) +{ + +/* Variables */ + + u32 tmp,numBits,mask; + +/* Code */ + + ASSERT(picSizeInMbs); + ASSERT(sliceGroupChangeRate); + ASSERT(sliceGroupChangeRate <= picSizeInMbs); + + /* compute (picSizeInMbs / sliceGroupChangeRate + 1), rounded up */ + if (picSizeInMbs % sliceGroupChangeRate) + tmp = 2 + picSizeInMbs/sliceGroupChangeRate; + else + tmp = 1 + picSizeInMbs/sliceGroupChangeRate; + + numBits = 0; + mask = ~0U; + + /* set numBits to position of right-most non-zero bit */ + while (tmp & (mask<<++numBits)) + ; + numBits--; + + /* add one more bit if value greater than 2^numBits */ + if (tmp & ((1<refPicListReorderingFlagL0 = tmp; + + if (pRefPicListReordering->refPicListReorderingFlagL0) + { + i = 0; + + do + { + if (i > numRefIdxActive) + { + EPRINT("Too many reordering commands"); + return(HANTRO_NOK); + } + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &command); + if (tmp != HANTRO_OK) + return(tmp); + if (command > 3) + { + EPRINT("reordering_of_pic_nums_idc"); + return(HANTRO_NOK); + } + + pRefPicListReordering->command[i].reorderingOfPicNumsIdc = command; + + if ((command == 0) || (command == 1)) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + if (value >= maxPicNum) + { + EPRINT("abs_diff_pic_num_minus1"); + return(HANTRO_NOK); + } + pRefPicListReordering->command[i].absDiffPicNum = value + 1; + } + else if (command == 2) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pRefPicListReordering->command[i].longTermPicNum = value; + } + i++; + } while (command != 3); + + /* there shall be at least one reordering command if + * refPicListReorderingFlagL0 was set */ + if (i == 1) + { + EPRINT("ref_pic_list_reordering"); + return(HANTRO_NOK); + } + } + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: DecRefPicMarking + + Functional description: + Decode decoded reference picture marking syntax elements from + the stream. + + Inputs: + pStrmData pointer to stream data structure + nalUnitType type of the current NAL unit + numRefFrames max number of reference frames from the active SPS + + Outputs: + pDecRefPicMarking decoded data is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK invalid stream data + +------------------------------------------------------------------------------*/ + +u32 DecRefPicMarking(strmData_t *pStrmData, + decRefPicMarking_t *pDecRefPicMarking, nalUnitType_e nalUnitType, + u32 numRefFrames) +{ + +/* Variables */ + + u32 tmp, value; + u32 i; + u32 operation; + /* variables for error checking purposes, store number of memory + * management operations of certain type */ + u32 num4 = 0, num5 = 0, num6 = 0, num1to3 = 0; + +/* Code */ + + ASSERT( nalUnitType == NAL_CODED_SLICE_IDR || + nalUnitType == NAL_CODED_SLICE || + nalUnitType == NAL_SEI ); + + + if (nalUnitType == NAL_CODED_SLICE_IDR) + { + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pDecRefPicMarking->noOutputOfPriorPicsFlag = tmp; + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pDecRefPicMarking->longTermReferenceFlag = tmp; + if (!numRefFrames && pDecRefPicMarking->longTermReferenceFlag) + { + EPRINT("long_term_reference_flag"); + return(HANTRO_NOK); + } + } + else + { + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pDecRefPicMarking->adaptiveRefPicMarkingModeFlag = tmp; + if (pDecRefPicMarking->adaptiveRefPicMarkingModeFlag) + { + i = 0; + do + { + /* see explanation of the MAX_NUM_MMC_OPERATIONS in + * slice_header.h */ + if (i > (2 * numRefFrames + 2)) + { + EPRINT("Too many management operations"); + return(HANTRO_NOK); + } + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &operation); + if (tmp != HANTRO_OK) + return(tmp); + if (operation > 6) + { + EPRINT("memory_management_control_operation"); + return(HANTRO_NOK); + } + + pDecRefPicMarking->operation[i]. + memoryManagementControlOperation = operation; + if ((operation == 1) || (operation == 3)) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pDecRefPicMarking->operation[i].differenceOfPicNums = + value + 1; + } + if (operation == 2) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pDecRefPicMarking->operation[i].longTermPicNum = value; + } + if ((operation == 3) || (operation == 6)) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + pDecRefPicMarking->operation[i].longTermFrameIdx = + value; + } + if (operation == 4) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + /* value shall be in range [0, numRefFrames] */ + if (value > numRefFrames) + { + EPRINT("max_long_term_frame_idx_plus1"); + return(HANTRO_NOK); + } + if (value == 0) + { + pDecRefPicMarking->operation[i]. + maxLongTermFrameIdx = + NO_LONG_TERM_FRAME_INDICES; + } + else + { + pDecRefPicMarking->operation[i]. + maxLongTermFrameIdx = value - 1; + } + num4++; + } + if (operation == 5) + { + num5++; + } + if (operation && operation <= 3) + num1to3++; + if (operation == 6) + num6++; + + i++; + } while (operation != 0); + + /* error checking */ + if (num4 > 1 || num5 > 1 || num6 > 1 || (num1to3 && num5)) + return(HANTRO_NOK); + + } + } + + return(HANTRO_OK); +} + +/*------------------------------------------------------------------------------ + + Function name: h264bsdCheckPpsId + + Functional description: + Peek value of pic_parameter_set_id from the slice header. Function + does not modify current stream positions but copies the stream + data structure to tmp structure which is used while accessing + stream data. + + Inputs: + pStrmData pointer to stream data structure + + Outputs: + picParamSetId value is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK invalid stream data + +------------------------------------------------------------------------------*/ + +u32 h264bsdCheckPpsId(strmData_t *pStrmData, u32 *picParamSetId) +{ + +/* Variables */ + + u32 tmp, value; + strmData_t tmpStrmData[1]; + +/* Code */ + + ASSERT(pStrmData); + + /* don't touch original stream position params */ + *tmpStrmData = *pStrmData; + + /* first_mb_in_slice */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* slice_type */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + if (value >= MAX_NUM_PIC_PARAM_SETS) + return(HANTRO_NOK); + + *picParamSetId = value; + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdCheckFrameNum + + Functional description: + Peek value of frame_num from the slice header. Function does not + modify current stream positions but copies the stream data + structure to tmp structure which is used while accessing stream + data. + + Inputs: + pStrmData pointer to stream data structure + maxFrameNum + + Outputs: + frameNum value is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK invalid stream data + +------------------------------------------------------------------------------*/ + +u32 h264bsdCheckFrameNum( + strmData_t *pStrmData, + u32 maxFrameNum, + u32 *frameNum) +{ + +/* Variables */ + + u32 tmp, value, i; + strmData_t tmpStrmData[1]; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(maxFrameNum); + ASSERT(frameNum); + + /* don't touch original stream position params */ + *tmpStrmData = *pStrmData; + + /* skip first_mb_in_slice */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* skip slice_type */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* skip pic_parameter_set_id */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* log2(maxFrameNum) -> num bits to represent frame_num */ + i = 0; + while (maxFrameNum >> i) + i++; + i--; + + /* frame_num */ + tmp = h264bsdGetBits(tmpStrmData, i); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + *frameNum = tmp; + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdCheckIdrPicId + + Functional description: + Peek value of idr_pic_id from the slice header. Function does not + modify current stream positions but copies the stream data + structure to tmp structure which is used while accessing stream + data. + + Inputs: + pStrmData pointer to stream data structure + maxFrameNum max frame number from active SPS + nalUnitType type of the current NAL unit + + Outputs: + idrPicId value is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK invalid stream data + +------------------------------------------------------------------------------*/ + +u32 h264bsdCheckIdrPicId( + strmData_t *pStrmData, + u32 maxFrameNum, + nalUnitType_e nalUnitType, + u32 *idrPicId) +{ + +/* Variables */ + + u32 tmp, value, i; + strmData_t tmpStrmData[1]; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(maxFrameNum); + ASSERT(idrPicId); + + /* nalUnitType must be equal to 5 because otherwise idrPicId is not + * present */ + if (nalUnitType != NAL_CODED_SLICE_IDR) + return(HANTRO_NOK); + + /* don't touch original stream position params */ + *tmpStrmData = *pStrmData; + + /* skip first_mb_in_slice */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* skip slice_type */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* skip pic_parameter_set_id */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* log2(maxFrameNum) -> num bits to represent frame_num */ + i = 0; + while (maxFrameNum >> i) + i++; + i--; + + /* skip frame_num */ + tmp = h264bsdGetBits(tmpStrmData, i); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + + /* idr_pic_id */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, idrPicId); + if (tmp != HANTRO_OK) + return(tmp); + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdCheckPicOrderCntLsb + + Functional description: + Peek value of pic_order_cnt_lsb from the slice header. Function + does not modify current stream positions but copies the stream + data structure to tmp structure which is used while accessing + stream data. + + Inputs: + pStrmData pointer to stream data structure + pSeqParamSet pointer to active SPS + nalUnitType type of the current NAL unit + + Outputs: + picOrderCntLsb value is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK invalid stream data + +------------------------------------------------------------------------------*/ + +u32 h264bsdCheckPicOrderCntLsb( + strmData_t *pStrmData, + seqParamSet_t *pSeqParamSet, + nalUnitType_e nalUnitType, + u32 *picOrderCntLsb) +{ + +/* Variables */ + + u32 tmp, value, i; + strmData_t tmpStrmData[1]; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(pSeqParamSet); + ASSERT(picOrderCntLsb); + + /* picOrderCntType must be equal to 0 */ + ASSERT(pSeqParamSet->picOrderCntType == 0); + ASSERT(pSeqParamSet->maxFrameNum); + ASSERT(pSeqParamSet->maxPicOrderCntLsb); + + /* don't touch original stream position params */ + *tmpStrmData = *pStrmData; + + /* skip first_mb_in_slice */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* skip slice_type */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* skip pic_parameter_set_id */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* log2(maxFrameNum) -> num bits to represent frame_num */ + i = 0; + while (pSeqParamSet->maxFrameNum >> i) + i++; + i--; + + /* skip frame_num */ + tmp = h264bsdGetBits(tmpStrmData, i); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + + /* skip idr_pic_id when necessary */ + if (nalUnitType == NAL_CODED_SLICE_IDR) + { + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + } + + /* log2(maxPicOrderCntLsb) -> num bits to represent pic_order_cnt_lsb */ + i = 0; + while (pSeqParamSet->maxPicOrderCntLsb >> i) + i++; + i--; + + /* pic_order_cnt_lsb */ + tmp = h264bsdGetBits(tmpStrmData, i); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + *picOrderCntLsb = tmp; + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdCheckDeltaPicOrderCntBottom + + Functional description: + Peek value of delta_pic_order_cnt_bottom from the slice header. + Function does not modify current stream positions but copies the + stream data structure to tmp structure which is used while + accessing stream data. + + Inputs: + pStrmData pointer to stream data structure + pSeqParamSet pointer to active SPS + nalUnitType type of the current NAL unit + + Outputs: + deltaPicOrderCntBottom value is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK invalid stream data + +------------------------------------------------------------------------------*/ + +u32 h264bsdCheckDeltaPicOrderCntBottom( + strmData_t *pStrmData, + seqParamSet_t *pSeqParamSet, + nalUnitType_e nalUnitType, + i32 *deltaPicOrderCntBottom) +{ + +/* Variables */ + + u32 tmp, value, i; + strmData_t tmpStrmData[1]; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(pSeqParamSet); + ASSERT(deltaPicOrderCntBottom); + + /* picOrderCntType must be equal to 0 and picOrderPresentFlag must be TRUE + * */ + ASSERT(pSeqParamSet->picOrderCntType == 0); + ASSERT(pSeqParamSet->maxFrameNum); + ASSERT(pSeqParamSet->maxPicOrderCntLsb); + + /* don't touch original stream position params */ + *tmpStrmData = *pStrmData; + + /* skip first_mb_in_slice */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* skip slice_type */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* skip pic_parameter_set_id */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* log2(maxFrameNum) -> num bits to represent frame_num */ + i = 0; + while (pSeqParamSet->maxFrameNum >> i) + i++; + i--; + + /* skip frame_num */ + tmp = h264bsdGetBits(tmpStrmData, i); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + + /* skip idr_pic_id when necessary */ + if (nalUnitType == NAL_CODED_SLICE_IDR) + { + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + } + + /* log2(maxPicOrderCntLsb) -> num bits to represent pic_order_cnt_lsb */ + i = 0; + while (pSeqParamSet->maxPicOrderCntLsb >> i) + i++; + i--; + + /* skip pic_order_cnt_lsb */ + tmp = h264bsdGetBits(tmpStrmData, i); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + + /* delta_pic_order_cnt_bottom */ + tmp = h264bsdDecodeExpGolombSigned(tmpStrmData, deltaPicOrderCntBottom); + if (tmp != HANTRO_OK) + return(tmp); + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdCheckDeltaPicOrderCnt + + Functional description: + Peek values delta_pic_order_cnt[0] and delta_pic_order_cnt[1] + from the slice header. Function does not modify current stream + positions but copies the stream data structure to tmp structure + which is used while accessing stream data. + + Inputs: + pStrmData pointer to stream data structure + pSeqParamSet pointer to active SPS + nalUnitType type of the current NAL unit + picOrderPresentFlag flag indicating if delta_pic_order_cnt[1] + is present in the stream + + Outputs: + deltaPicOrderCnt values are stored here + + Returns: + HANTRO_OK success + HANTRO_NOK invalid stream data + +------------------------------------------------------------------------------*/ + +u32 h264bsdCheckDeltaPicOrderCnt( + strmData_t *pStrmData, + seqParamSet_t *pSeqParamSet, + nalUnitType_e nalUnitType, + u32 picOrderPresentFlag, + i32 *deltaPicOrderCnt) +{ + +/* Variables */ + + u32 tmp, value, i; + strmData_t tmpStrmData[1]; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(pSeqParamSet); + ASSERT(deltaPicOrderCnt); + + /* picOrderCntType must be equal to 1 and deltaPicOrderAlwaysZeroFlag must + * be FALSE */ + ASSERT(pSeqParamSet->picOrderCntType == 1); + ASSERT(!pSeqParamSet->deltaPicOrderAlwaysZeroFlag); + ASSERT(pSeqParamSet->maxFrameNum); + + /* don't touch original stream position params */ + *tmpStrmData = *pStrmData; + + /* skip first_mb_in_slice */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* skip slice_type */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* skip pic_parameter_set_id */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* log2(maxFrameNum) -> num bits to represent frame_num */ + i = 0; + while (pSeqParamSet->maxFrameNum >> i) + i++; + i--; + + /* skip frame_num */ + tmp = h264bsdGetBits(tmpStrmData, i); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + + /* skip idr_pic_id when necessary */ + if (nalUnitType == NAL_CODED_SLICE_IDR) + { + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + } + + /* delta_pic_order_cnt[0] */ + tmp = h264bsdDecodeExpGolombSigned(tmpStrmData, &deltaPicOrderCnt[0]); + if (tmp != HANTRO_OK) + return(tmp); + + /* delta_pic_order_cnt[1] if present */ + if (picOrderPresentFlag) + { + tmp = h264bsdDecodeExpGolombSigned(tmpStrmData, &deltaPicOrderCnt[1]); + if (tmp != HANTRO_OK) + return(tmp); + } + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdCheckRedundantPicCnt + + Functional description: + Peek value of redundant_pic_cnt from the slice header. Function + does not modify current stream positions but copies the stream + data structure to tmp structure which is used while accessing + stream data. + + Inputs: + pStrmData pointer to stream data structure + pSeqParamSet pointer to active SPS + pPicParamSet pointer to active PPS + nalUnitType type of the current NAL unit + + Outputs: + redundantPicCnt value is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK invalid stream data + +------------------------------------------------------------------------------*/ + +u32 h264bsdCheckRedundantPicCnt( + strmData_t *pStrmData, + seqParamSet_t *pSeqParamSet, + picParamSet_t *pPicParamSet, + nalUnitType_e nalUnitType, + u32 *redundantPicCnt) +{ + +/* Variables */ + + u32 tmp, value, i; + i32 ivalue; + strmData_t tmpStrmData[1]; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(pSeqParamSet); + ASSERT(pPicParamSet); + ASSERT(redundantPicCnt); + + /* redundant_pic_cnt_flag must be TRUE */ + ASSERT(pPicParamSet->redundantPicCntPresentFlag); + ASSERT(pSeqParamSet->maxFrameNum); + ASSERT(pSeqParamSet->picOrderCntType > 0 || + pSeqParamSet->maxPicOrderCntLsb); + + /* don't touch original stream position params */ + *tmpStrmData = *pStrmData; + + /* skip first_mb_in_slice */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* skip slice_type */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* skip pic_parameter_set_id */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + + /* log2(maxFrameNum) -> num bits to represent frame_num */ + i = 0; + while (pSeqParamSet->maxFrameNum >> i) + i++; + i--; + + /* skip frame_num */ + tmp = h264bsdGetBits(tmpStrmData, i); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + + /* skip idr_pic_id when necessary */ + if (nalUnitType == NAL_CODED_SLICE_IDR) + { + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if (tmp != HANTRO_OK) + return(tmp); + } + + if (pSeqParamSet->picOrderCntType == 0) + { + /* log2(maxPicOrderCntLsb) -> num bits to represent pic_order_cnt_lsb */ + i = 0; + while (pSeqParamSet->maxPicOrderCntLsb >> i) + i++; + i--; + + /* pic_order_cnt_lsb */ + tmp = h264bsdGetBits(tmpStrmData, i); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + + if (pPicParamSet->picOrderPresentFlag) + { + /* skip delta_pic_order_cnt_bottom */ + tmp = h264bsdDecodeExpGolombSigned(tmpStrmData, &ivalue); + if (tmp != HANTRO_OK) + return(tmp); + } + } + + if (pSeqParamSet->picOrderCntType == 1 && + !pSeqParamSet->deltaPicOrderAlwaysZeroFlag) + { + /* delta_pic_order_cnt[0] */ + tmp = h264bsdDecodeExpGolombSigned(tmpStrmData, &ivalue); + if (tmp != HANTRO_OK) + return(tmp); + + /* delta_pic_order_cnt[1] if present */ + if (pPicParamSet->picOrderPresentFlag) + { + tmp = h264bsdDecodeExpGolombSigned(tmpStrmData, &ivalue); + if (tmp != HANTRO_OK) + return(tmp); + } + } + + /* redundant_pic_cnt */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, redundantPicCnt); + if (tmp != HANTRO_OK) + return(tmp); + + return(HANTRO_OK); + +} + + +/*------------------------------------------------------------------------------ + + Function: h264bsdCheckPriorPicsFlag + + Functional description: + Peek value of no_output_of_prior_pics_flag from the slice header. + Function does not modify current stream positions but copies + the stream data structure to tmp structure which is used while + accessing stream data. + + Inputs: + pStrmData pointer to stream data structure + pSeqParamSet pointer to active SPS + pPicParamSet pointer to active PPS + nalUnitType type of the current NAL unit + + Outputs: + noOutputOfPriorPicsFlag value is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK invalid stream data + +------------------------------------------------------------------------------*/ +/*lint -e715 disable lint info nalUnitType not referenced */ +u32 h264bsdCheckPriorPicsFlag(u32 * noOutputOfPriorPicsFlag, + const strmData_t * pStrmData, + const seqParamSet_t * pSeqParamSet, + const picParamSet_t * pPicParamSet, + nalUnitType_e nalUnitType) +{ +/* Variables */ + + u32 tmp, value, i; + i32 ivalue; + strmData_t tmpStrmData[1]; + UNUSED(nalUnitType); + +/* Code */ + + ASSERT(pStrmData); + ASSERT(pSeqParamSet); + ASSERT(pPicParamSet); + ASSERT(noOutputOfPriorPicsFlag); + + /* must be IDR lsice */ + ASSERT(nalUnitType == NAL_CODED_SLICE_IDR); + + /* don't touch original stream position params */ + *tmpStrmData = *pStrmData; + + /* skip first_mb_in_slice */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if(tmp != HANTRO_OK) + return (tmp); + + /* slice_type */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if(tmp != HANTRO_OK) + return (tmp); + + /* skip pic_parameter_set_id */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if(tmp != HANTRO_OK) + return (tmp); + + /* log2(maxFrameNum) -> num bits to represent frame_num */ + i = 0; + while(pSeqParamSet->maxFrameNum >> i) + i++; + i--; + + /* skip frame_num */ + tmp = h264bsdGetBits(tmpStrmData, i); + if(tmp == END_OF_STREAM) + return (HANTRO_NOK); + + /* skip idr_pic_id */ + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if(tmp != HANTRO_OK) + return (tmp); + + if(pSeqParamSet->picOrderCntType == 0) + { + /* log2(maxPicOrderCntLsb) -> num bits to represent pic_order_cnt_lsb */ + i = 0; + while(pSeqParamSet->maxPicOrderCntLsb >> i) + i++; + i--; + + /* skip pic_order_cnt_lsb */ + tmp = h264bsdGetBits(tmpStrmData, i); + if(tmp == END_OF_STREAM) + return (HANTRO_NOK); + + if(pPicParamSet->picOrderPresentFlag) + { + /* skip delta_pic_order_cnt_bottom */ + tmp = h264bsdDecodeExpGolombSigned(tmpStrmData, &ivalue); + if(tmp != HANTRO_OK) + return (tmp); + } + } + + if(pSeqParamSet->picOrderCntType == 1 && + !pSeqParamSet->deltaPicOrderAlwaysZeroFlag) + { + /* skip delta_pic_order_cnt[0] */ + tmp = h264bsdDecodeExpGolombSigned(tmpStrmData, &ivalue); + if(tmp != HANTRO_OK) + return (tmp); + + /* skip delta_pic_order_cnt[1] if present */ + if(pPicParamSet->picOrderPresentFlag) + { + tmp = h264bsdDecodeExpGolombSigned(tmpStrmData, &ivalue); + if(tmp != HANTRO_OK) + return (tmp); + } + } + + /* skip redundant_pic_cnt */ + if(pPicParamSet->redundantPicCntPresentFlag) + { + tmp = h264bsdDecodeExpGolombUnsigned(tmpStrmData, &value); + if(tmp != HANTRO_OK) + return (tmp); + } + + *noOutputOfPriorPicsFlag = h264bsdGetBits(tmpStrmData, 1); + if(*noOutputOfPriorPicsFlag == END_OF_STREAM) + return (HANTRO_NOK); + + return (HANTRO_OK); + +} +/*lint +e715 */ + + diff --git a/src/video/h264_decoder/h264bsd_slice_header.h b/src/video/h264_decoder/h264bsd_slice_header.h new file mode 100644 index 000000000..181128609 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_slice_header.h @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_SLICE_HEADER_H +#define H264SWDEC_SLICE_HEADER_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_stream.h" +#include "h264bsd_cfg.h" +#include "h264bsd_seq_param_set.h" +#include "h264bsd_pic_param_set.h" +#include "h264bsd_nal_unit.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +enum { + P_SLICE = 0, + I_SLICE = 2 +}; + +enum {NO_LONG_TERM_FRAME_INDICES = 0xFFFF}; + +/* macro to determine if slice is an inter slice, sliceTypes 0 and 5 */ +#define IS_P_SLICE(sliceType) (((sliceType) == P_SLICE) || \ + ((sliceType) == P_SLICE + 5)) + +/* macro to determine if slice is an intra slice, sliceTypes 2 and 7 */ +#define IS_I_SLICE(sliceType) (((sliceType) == I_SLICE) || \ + ((sliceType) == I_SLICE + 5)) + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/* structure to store data of one reference picture list reordering operation */ +typedef struct +{ + u32 reorderingOfPicNumsIdc; + u32 absDiffPicNum; + u32 longTermPicNum; +} refPicListReorderingOperation_t; + +/* structure to store reference picture list reordering operations */ +typedef struct +{ + u32 refPicListReorderingFlagL0; + refPicListReorderingOperation_t command[MAX_NUM_REF_PICS+1]; +} refPicListReordering_t; + +/* structure to store data of one DPB memory management control operation */ +typedef struct +{ + u32 memoryManagementControlOperation; + u32 differenceOfPicNums; + u32 longTermPicNum; + u32 longTermFrameIdx; + u32 maxLongTermFrameIdx; +} memoryManagementOperation_t; + +/* worst case scenario: all MAX_NUM_REF_PICS pictures in the buffer are + * short term pictures, each one of them is first marked as long term + * reference picture which is then marked as unused for reference. + * Additionally, max long-term frame index is set and current picture is + * marked as long term reference picture. Last position reserved for + * end memory_management_control_operation command */ +#define MAX_NUM_MMC_OPERATIONS (2*MAX_NUM_REF_PICS+2+1) + +/* structure to store decoded reference picture marking data */ +typedef struct +{ + u32 noOutputOfPriorPicsFlag; + u32 longTermReferenceFlag; + u32 adaptiveRefPicMarkingModeFlag; + memoryManagementOperation_t operation[MAX_NUM_MMC_OPERATIONS]; +} decRefPicMarking_t; + +/* stores weighted prediction parameters for list0 references in P-slices */ +typedef struct +{ + u32 lumaLog2WeightDenom; + u32 chromaLog2WeightDenom; + i32 lumaWeightL0[MAX_NUM_REF_PICS]; + i32 lumaOffsetL0[MAX_NUM_REF_PICS]; + i32 chromaWeightL0[MAX_NUM_REF_PICS][2]; + i32 chromaOffsetL0[MAX_NUM_REF_PICS][2]; +} predWeightTable_t; + +/* structure to store slice header data decoded from the stream */ +typedef struct +{ + u32 firstMbInSlice; + u32 sliceType; + u32 picParameterSetId; + u32 frameNum; + u32 idrPicId; + u32 picOrderCntLsb; + i32 deltaPicOrderCntBottom; + i32 deltaPicOrderCnt[2]; + u32 redundantPicCnt; + u32 numRefIdxActiveOverrideFlag; + u32 numRefIdxL0Active; + u32 weightedPredFlag; + i32 sliceQpDelta; + u32 disableDeblockingFilterIdc; + i32 sliceAlphaC0Offset; + i32 sliceBetaOffset; + u32 sliceGroupChangeCycle; + refPicListReordering_t refPicListReordering; + decRefPicMarking_t decRefPicMarking; + predWeightTable_t predWeightTable; +#ifdef H264BSD_N64 + /* predWeightTable repacked for the RSP by h264bsdPrepareWeights: see there + * for why. Kept in the slice header so that concurrent decoder instances + * (and a decode paused mid-slice) cannot clobber each other. */ + u32 packedWeights[MAX_NUM_REF_PICS][3]; + u32 numPackedWeights; +#endif +} sliceHeader_t; + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeSliceHeader(strmData_t *pStrmData, + sliceHeader_t *pSliceHeader, + seqParamSet_t *pSeqParamSet, + picParamSet_t *pPicParamSet, + nalUnit_t *pNalUnit); + +u32 h264bsdCheckPpsId(strmData_t *pStrmData, u32 *ppsId); + +u32 h264bsdCheckFrameNum( + strmData_t *pStrmData, + u32 maxFrameNum, + u32 *frameNum); + +u32 h264bsdCheckIdrPicId( + strmData_t *pStrmData, + u32 maxFrameNum, + nalUnitType_e nalUnitType, + u32 *idrPicId); + +u32 h264bsdCheckPicOrderCntLsb( + strmData_t *pStrmData, + seqParamSet_t *pSeqParamSet, + nalUnitType_e nalUnitType, + u32 *picOrderCntLsb); + +u32 h264bsdCheckDeltaPicOrderCntBottom( + strmData_t *pStrmData, + seqParamSet_t *pSeqParamSet, + nalUnitType_e nalUnitType, + i32 *deltaPicOrderCntBottom); + +u32 h264bsdCheckDeltaPicOrderCnt( + strmData_t *pStrmData, + seqParamSet_t *pSeqParamSet, + nalUnitType_e nalUnitType, + u32 picOrderPresentFlag, + i32 *deltaPicOrderCnt); + +u32 h264bsdCheckRedundantPicCnt( + strmData_t *pStrmData, + seqParamSet_t *pSeqParamSet, + picParamSet_t *pPicParamSet, + nalUnitType_e nalUnitType, + u32 *redundantPicCnt); + +u32 h264bsdCheckPriorPicsFlag(u32 * noOutputOfPriorPicsFlag, + const strmData_t * pStrmData, + const seqParamSet_t * pSeqParamSet, + const picParamSet_t * pPicParamSet, + nalUnitType_e nalUnitType); + +#endif /* #ifdef H264SWDEC_SLICE_HEADER_H */ + diff --git a/src/video/h264_decoder/h264bsd_storage.c b/src/video/h264_decoder/h264bsd_storage.c new file mode 100644 index 000000000..e432ad40e --- /dev/null +++ b/src/video/h264_decoder/h264bsd_storage.c @@ -0,0 +1,984 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdInitStorage + h264bsdStoreSeqParamSet + h264bsdStorePicParamSet + h264bsdActivateParamSets + h264bsdResetStorage + h264bsdIsStartOfPicture + h264bsdIsEndOfPicture + h264bsdComputeSliceGroupMap + h264bsdCheckAccessUnitBoundary + CheckPps + h264bsdValidParamSets + +------------------------------------------------------------------------------*/ + +#include "opttarget.h" + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_storage.h" +#include "h264bsd_util.h" +#include "h264bsd_neighbour.h" +#include "h264bsd_slice_group_map.h" +#include "h264bsd_dpb.h" +#include "h264bsd_nal_unit.h" +#include "h264bsd_slice_header.h" +#include "h264bsd_seq_param_set.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +static u32 CheckPps(picParamSet_t *pps, seqParamSet_t *sps); + +/*------------------------------------------------------------------------------ + + Function name: h264bsdInitStorage + + Functional description: + Initialize storage structure. Sets contents of the storage to '0' + except for the active parameter set ids, which are initialized + to invalid values. + + Inputs: + + Outputs: + pStorage initialized data stored here + + Returns: + none + +------------------------------------------------------------------------------*/ + +void h264bsdInitStorage(storage_t *pStorage) +{ + +/* Variables */ + +/* Code */ + + ASSERT(pStorage); + + H264SwDecMemset(pStorage, 0, sizeof(storage_t)); + + pStorage->activeSpsId = MAX_NUM_SEQ_PARAM_SETS; + pStorage->activePpsId = MAX_NUM_PIC_PARAM_SETS; + + pStorage->aub->firstCallFlag = HANTRO_TRUE; +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdStoreSeqParamSet + + Functional description: + Store sequence parameter set into the storage. If active SPS is + overwritten -> check if contents changes and if it does, set + parameters to force reactivation of parameter sets + + Inputs: + pStorage pointer to storage structure + pSeqParamSet pointer to param set to be stored + + Outputs: + none + + Returns: + HANTRO_OK success + MEMORY_ALLOCATION_ERROR failure in memory allocation + + +------------------------------------------------------------------------------*/ + +u32 h264bsdStoreSeqParamSet(storage_t *pStorage, seqParamSet_t *pSeqParamSet) +{ + +/* Variables */ + + u32 id; + +/* Code */ + + ASSERT(pStorage); + ASSERT(pSeqParamSet); + ASSERT(pSeqParamSet->seqParameterSetId < MAX_NUM_SEQ_PARAM_SETS); + + id = pSeqParamSet->seqParameterSetId; + + /* seq parameter set with id not used before -> allocate memory */ + if (pStorage->sps[id] == NULL) + { + ALLOCATE(pStorage->sps[id], 1, seqParamSet_t); + if (pStorage->sps[id] == NULL) + return(MEMORY_ALLOCATION_ERROR); + } + /* sequence parameter set with id equal to id of active sps */ + else if (id == pStorage->activeSpsId) + { + /* if seq parameter set contents changes + * -> overwrite and re-activate when next IDR picture decoded + * ids of active param sets set to invalid values to force + * re-activation. Memories allocated for old sps freed + * otherwise free memeries allocated for just decoded sps and + * continue */ + if (h264bsdCompareSeqParamSets(pSeqParamSet, pStorage->activeSps) != 0) + { + FREE(pStorage->sps[id]->offsetForRefFrame); + FREE(pStorage->sps[id]->vuiParameters); + pStorage->activeSpsId = MAX_NUM_SEQ_PARAM_SETS + 1; + pStorage->activePpsId = MAX_NUM_PIC_PARAM_SETS + 1; + pStorage->activeSps = NULL; + pStorage->activePps = NULL; + } + else + { + FREE(pSeqParamSet->offsetForRefFrame); + FREE(pSeqParamSet->vuiParameters); + return(HANTRO_OK); + } + } + /* overwrite seq param set other than active one -> free memories + * allocated for old param set */ + else + { + FREE(pStorage->sps[id]->offsetForRefFrame); + FREE(pStorage->sps[id]->vuiParameters); + } + + *pStorage->sps[id] = *pSeqParamSet; + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdStorePicParamSet + + Functional description: + Store picture parameter set into the storage. If active PPS is + overwritten -> check if active SPS changes and if it does -> set + parameters to force reactivation of parameter sets + + Inputs: + pStorage pointer to storage structure + pPicParamSet pointer to param set to be stored + + Outputs: + none + + Returns: + HANTRO_OK success + MEMORY_ALLOCATION_ERROR failure in memory allocation + +------------------------------------------------------------------------------*/ + +u32 h264bsdStorePicParamSet(storage_t *pStorage, picParamSet_t *pPicParamSet) +{ + +/* Variables */ + + u32 id; + +/* Code */ + + ASSERT(pStorage); + ASSERT(pPicParamSet); + ASSERT(pPicParamSet->picParameterSetId < MAX_NUM_PIC_PARAM_SETS); + ASSERT(pPicParamSet->seqParameterSetId < MAX_NUM_SEQ_PARAM_SETS); + + id = pPicParamSet->picParameterSetId; + + /* pic parameter set with id not used before -> allocate memory */ + if (pStorage->pps[id] == NULL) + { + ALLOCATE(pStorage->pps[id], 1, picParamSet_t); + if (pStorage->pps[id] == NULL) + return(MEMORY_ALLOCATION_ERROR); + } + /* picture parameter set with id equal to id of active pps */ + else if (id == pStorage->activePpsId) + { + /* check whether seq param set changes, force re-activation of + * param set if it does. Set activeSpsId to invalid value to + * accomplish this */ + if (pPicParamSet->seqParameterSetId != pStorage->activeSpsId) + { + pStorage->activePpsId = MAX_NUM_PIC_PARAM_SETS + 1; + } + /* free memories allocated for old param set */ + FREE(pStorage->pps[id]->runLength); + FREE(pStorage->pps[id]->topLeft); + FREE(pStorage->pps[id]->bottomRight); + FREE(pStorage->pps[id]->sliceGroupId); + } + /* overwrite pic param set other than active one -> free memories + * allocated for old param set */ + else + { + FREE(pStorage->pps[id]->runLength); + FREE(pStorage->pps[id]->topLeft); + FREE(pStorage->pps[id]->bottomRight); + FREE(pStorage->pps[id]->sliceGroupId); + } + + *pStorage->pps[id] = *pPicParamSet; + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdActivateParamSets + + Functional description: + Activate certain SPS/PPS combination. This function shall be + called in the beginning of each picture. Picture parameter set + can be changed as wanted, but sequence parameter set may only be + changed when the starting picture is an IDR picture. + + When new SPS is activated the function allocates memory for + macroblock storages and slice group map and (re-)initializes the + decoded picture buffer. If this is not the first activation the old + allocations are freed and FreeDpb called before new allocations. + + Inputs: + pStorage pointer to storage data structure + ppsId identifies the PPS to be activated, SPS id obtained + from the PPS + isIdr flag to indicate if the picture is an IDR picture + + Outputs: + none + + Returns: + HANTRO_OK success + HANTRO_NOK non-existing or invalid param set combination, + trying to change SPS with non-IDR picture + MEMORY_ALLOCATION_ERROR failure in memory allocation + +------------------------------------------------------------------------------*/ + +u32 h264bsdActivateParamSets(storage_t *pStorage, u32 ppsId, u32 isIdr) +{ + +/* Variables */ + + u32 tmp; + u32 flag; + +/* Code */ + + ASSERT(pStorage); + ASSERT(ppsId < MAX_NUM_PIC_PARAM_SETS); + + /* check that pps and corresponding sps exist */ + if ( (pStorage->pps[ppsId] == NULL) || + (pStorage->sps[pStorage->pps[ppsId]->seqParameterSetId] == NULL) ) + { + return(HANTRO_NOK); + } + + /* check that pps parameters do not violate picture size constraints */ + tmp = CheckPps(pStorage->pps[ppsId], + pStorage->sps[pStorage->pps[ppsId]->seqParameterSetId]); + if (tmp != HANTRO_OK) + return(tmp); + + /* first activation part1 */ + if (pStorage->activePpsId == MAX_NUM_PIC_PARAM_SETS) + { + pStorage->activePpsId = ppsId; + pStorage->activePps = pStorage->pps[ppsId]; + pStorage->activeSpsId = pStorage->activePps->seqParameterSetId; + pStorage->activeSps = pStorage->sps[pStorage->activeSpsId]; + pStorage->picSizeInMbs = + pStorage->activeSps->picWidthInMbs * + pStorage->activeSps->picHeightInMbs; + + pStorage->currImage->width = pStorage->activeSps->picWidthInMbs; + pStorage->currImage->height = pStorage->activeSps->picHeightInMbs; + + pStorage->pendingActivation = HANTRO_TRUE; + } + /* first activation part2 */ + else if (pStorage->pendingActivation) + { + pStorage->pendingActivation = HANTRO_FALSE; + + FREE(pStorage->mb); + FREE(pStorage->sliceGroupMap); + + ALLOCATE(pStorage->mb, pStorage->picSizeInMbs, mbStorage_t); + ALLOCATE(pStorage->sliceGroupMap, pStorage->picSizeInMbs, u32); + if (pStorage->mb == NULL || pStorage->sliceGroupMap == NULL) + return(MEMORY_ALLOCATION_ERROR); + + H264SwDecMemset(pStorage->mb, 0, + pStorage->picSizeInMbs * sizeof(mbStorage_t)); + + h264bsdInitMbNeighbours(pStorage->mb, + pStorage->activeSps->picWidthInMbs, + pStorage->picSizeInMbs); + + /* dpb output reordering disabled if + * 1) application set noReordering flag + * 2) POC type equal to 2 + * 3) num_reorder_frames in vui equal to 0 */ + if ( pStorage->noReordering || + pStorage->activeSps->picOrderCntType == 2 || + (pStorage->activeSps->vuiParametersPresentFlag && + pStorage->activeSps->vuiParameters->bitstreamRestrictionFlag && + !pStorage->activeSps->vuiParameters->numReorderFrames) ) + flag = HANTRO_TRUE; + else + flag = HANTRO_FALSE; + + tmp = h264bsdResetDpb(pStorage->dpb, + pStorage->activeSps->picWidthInMbs * + pStorage->activeSps->picHeightInMbs, + pStorage->activeSps->maxDpbSize, + pStorage->activeSps->numRefFrames, + pStorage->activeSps->maxFrameNum, + flag, + pStorage->maxNumBufferedPics); + if (tmp != HANTRO_OK) + return(tmp); + } + else if (ppsId != pStorage->activePpsId) + { + /* sequence parameter set shall not change but before an IDR picture */ + if (pStorage->pps[ppsId]->seqParameterSetId != pStorage->activeSpsId) + { + DEBUG(("SEQ PARAM SET CHANGING...\n")); + if (isIdr) + { + pStorage->activePpsId = ppsId; + pStorage->activePps = pStorage->pps[ppsId]; + pStorage->activeSpsId = pStorage->activePps->seqParameterSetId; + pStorage->activeSps = pStorage->sps[pStorage->activeSpsId]; + pStorage->picSizeInMbs = + pStorage->activeSps->picWidthInMbs * + pStorage->activeSps->picHeightInMbs; + + pStorage->currImage->width = pStorage->activeSps->picWidthInMbs; + pStorage->currImage->height = + pStorage->activeSps->picHeightInMbs; + + pStorage->pendingActivation = HANTRO_TRUE; + } + else + { + DEBUG(("TRYING TO CHANGE SPS IN NON-IDR SLICE\n")); + return(HANTRO_NOK); + } + } + else + { + pStorage->activePpsId = ppsId; + pStorage->activePps = pStorage->pps[ppsId]; + } + } + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdResetStorage + + Functional description: + Reset contents of the storage. This should be called before + processing of new image is started. + + Inputs: + pStorage pointer to storage structure + + Outputs: + none + + Returns: + none + + +------------------------------------------------------------------------------*/ + +void h264bsdResetStorage(storage_t *pStorage) +{ + +/* Variables */ + + u32 i; + +/* Code */ + + ASSERT(pStorage); + + pStorage->slice->numDecodedMbs = 0; + pStorage->slice->sliceId = 0; + pStorage->sliceDataPending = 0; + + for (i = 0; i < pStorage->picSizeInMbs; i++) + { + pStorage->mb[i].sliceId = 0; +#ifndef OPTIMIZE_NO_DECODED_FLAG + pStorage->mb[i].decoded = 0; +#endif + } + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdRewindStorage + + Functional description: + Reset decoder state for video rewind/loop without freeing any + allocated memory. Parameter sets (SPS/PPS), macroblock storage, + slice group map, and DPB pixel buffers are preserved. Per-frame + state (slice, POC, AUB, stream pointers, etc.) is reset so the + decoder can re-parse the stream from the beginning. + + Inputs: + pStorage pointer to storage structure + + Outputs: + pStorage state fields reset, allocations preserved + + Returns: + none + +------------------------------------------------------------------------------*/ + +void h264bsdRewindStorage(storage_t *pStorage) +{ + +/* Variables */ + + u32 i; + +/* Code */ + + ASSERT(pStorage); + + /* reset slice state */ + pStorage->slice->numDecodedMbs = 0; + pStorage->slice->sliceId = 0; + + /* reset per-frame flags */ + pStorage->skipRedundantSlices = 0; + pStorage->picStarted = 0; + pStorage->validSliceInAccessUnit = 0; + pStorage->numConcealedMbs = 0; + pStorage->currentPicId = 0; + pStorage->pendingActivation = 0; + pStorage->mbLayerIdx = 0; + + /* reset stream pointers and mid-slice timeslice state */ + pStorage->prevBufNotFinished = 0; + pStorage->prevBufPointer = NULL; + pStorage->prevBytesConsumed = 0; + pStorage->sliceDataPending = 0; + H264SwDecMemset(pStorage->strm, 0, sizeof(strmData_t)); + + /* reset POC, AUB, NAL unit, and slice header state */ + H264SwDecMemset(pStorage->poc, 0, sizeof(pocStorage_t)); + H264SwDecMemset(pStorage->aub, 0, sizeof(aubCheck_t)); + pStorage->aub->firstCallFlag = HANTRO_TRUE; + H264SwDecMemset(pStorage->prevNalUnit, 0, sizeof(nalUnit_t)); + H264SwDecMemset(pStorage->sliceHeader, 0, 2 * sizeof(sliceHeader_t)); + + /* reset current image data pointer (will be reassigned by + * h264bsdAllocateDpbImage during decode) */ + pStorage->currImage->data = NULL; + + /* reset macroblock slice state (same as h264bsdResetStorage) */ + if (pStorage->mb) + { + for (i = 0; i < pStorage->picSizeInMbs; i++) + { + pStorage->mb[i].sliceId = 0; +#ifndef OPTIMIZE_NO_DECODED_FLAG + pStorage->mb[i].decoded = 0; +#endif + } + } + + /* clear DPB state, keeping allocated buffers */ + h264bsdClearDpb(pStorage->dpb); +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdIsStartOfPicture + + Functional description: + Determine if the decoder is in the start of a picture. This + information is needed to decide if h264bsdActivateParamSets and + h264bsdCheckGapsInFrameNum functions should be called. Function + considers that new picture is starting if no slice headers + have been successfully decoded for the current access unit. + + Inputs: + pStorage pointer to storage structure + + Outputs: + none + + Returns: + HANTRO_TRUE new picture is starting + HANTRO_FALSE not starting + +------------------------------------------------------------------------------*/ + +u32 h264bsdIsStartOfPicture(storage_t *pStorage) +{ + +/* Variables */ + + +/* Code */ + + if (pStorage->validSliceInAccessUnit == HANTRO_FALSE) + return(HANTRO_TRUE); + else + return(HANTRO_FALSE); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdIsEndOfPicture + + Functional description: + Determine if the decoder is in the end of a picture. This + information is needed to determine when deblocking filtering + and reference picture marking processes should be performed. + + If the decoder is processing primary slices the return value + is determined by checking the value of numDecodedMbs in the + storage. On the other hand, if the decoder is processing + redundant slices the numDecodedMbs may not contain valid + informationa and each macroblock has to be checked separately. + + Inputs: + pStorage pointer to storage structure + + Outputs: + none + + Returns: + HANTRO_TRUE end of picture + HANTRO_FALSE noup + +------------------------------------------------------------------------------*/ + +u32 h264bsdIsEndOfPicture(storage_t *pStorage) +{ +#ifdef SINGLE_SLICE + return(HANTRO_TRUE); +#else + +/* Variables */ + +#ifndef OPTIMIZE_NO_DECODED_FLAG + u32 i; +#endif + u32 tmp; + +/* Code */ + + /* primary picture */ + if (!pStorage->sliceHeader[0].redundantPicCnt) + { + if (pStorage->slice->numDecodedMbs == pStorage->picSizeInMbs) + return(HANTRO_TRUE); + } + else + { +#ifndef OPTIMIZE_NO_DECODED_FLAG + for (i = 0, tmp = 0; i < pStorage->picSizeInMbs; i++) + tmp += pStorage->mb[i].decoded ? 1 : 0; +#else + tmp = pStorage->picSizeInMbs; +#endif + + if (tmp == pStorage->picSizeInMbs) + return(HANTRO_TRUE); + } + + return(HANTRO_FALSE); +#endif +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdComputeSliceGroupMap + + Functional description: + Compute slice group map. Just call h264bsdDecodeSliceGroupMap with + appropriate parameters. + + Inputs: + pStorage pointer to storage structure + sliceGroupChangeCycle + + Outputs: + none + + Returns: + none + +------------------------------------------------------------------------------*/ + +void h264bsdComputeSliceGroupMap(storage_t *pStorage, u32 sliceGroupChangeCycle) +{ + +/* Variables */ + + +/* Code */ + + h264bsdDecodeSliceGroupMap(pStorage->sliceGroupMap, + pStorage->activePps, sliceGroupChangeCycle, + pStorage->activeSps->picWidthInMbs, + pStorage->activeSps->picHeightInMbs); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdCheckAccessUnitBoundary + + Functional description: + Check if next NAL unit starts a new access unit. Following + conditions specify start of a new access unit: + + -NAL unit types 6-11, 13-18 (e.g. SPS, PPS) + + following conditions checked only for slice NAL units, values + compared to ones obtained from previous slice: + + -NAL unit type differs (slice / IDR slice) + -frame_num differs + -nal_ref_idc differs and one of the values is 0 + -POC information differs + -both are IDR slices and idr_pic_id differs + + Inputs: + strm pointer to stream data structure + nuNext pointer to NAL unit structure + storage pointer to storage structure + + Outputs: + accessUnitBoundaryFlag the result is stored here, TRUE for + access unit boundary, FALSE otherwise + + Returns: + HANTRO_OK success + HANTRO_NOK failure, invalid stream data + PARAM_SET_ERROR invalid param set usage + +------------------------------------------------------------------------------*/ + +u32 h264bsdCheckAccessUnitBoundary( + strmData_t *strm, + nalUnit_t *nuNext, + storage_t *storage, + u32 *accessUnitBoundaryFlag) +{ + +/* Variables */ + + u32 tmp, ppsId, frameNum, idrPicId, picOrderCntLsb; + i32 deltaPicOrderCntBottom, deltaPicOrderCnt[2]; + seqParamSet_t *sps; + picParamSet_t *pps; + +/* Code */ + + ASSERT(strm); + ASSERT(nuNext); + ASSERT(storage); + ASSERT(storage->sps); + ASSERT(storage->pps); + + /* initialize default output to FALSE */ + *accessUnitBoundaryFlag = HANTRO_FALSE; + + if ( ( (nuNext->nalUnitType > 5) && (nuNext->nalUnitType < 12) ) || + ( (nuNext->nalUnitType > 12) && (nuNext->nalUnitType <= 18) ) ) + { + *accessUnitBoundaryFlag = HANTRO_TRUE; + return(HANTRO_OK); + } + else if ( nuNext->nalUnitType != NAL_CODED_SLICE && + nuNext->nalUnitType != NAL_CODED_SLICE_IDR ) + { + return(HANTRO_OK); + } + + /* check if this is the very first call to this function */ + if (storage->aub->firstCallFlag) + { + *accessUnitBoundaryFlag = HANTRO_TRUE; + storage->aub->firstCallFlag = HANTRO_FALSE; + } + + /* get picture parameter set id */ + tmp = h264bsdCheckPpsId(strm, &ppsId); + if (tmp != HANTRO_OK) + return(tmp); + + /* store sps and pps in separate pointers just to make names shorter */ + pps = storage->pps[ppsId]; + if ( pps == NULL || storage->sps[pps->seqParameterSetId] == NULL || + (storage->activeSpsId != MAX_NUM_SEQ_PARAM_SETS && + pps->seqParameterSetId != storage->activeSpsId && + nuNext->nalUnitType != NAL_CODED_SLICE_IDR) ) + return(PARAM_SET_ERROR); + sps = storage->sps[pps->seqParameterSetId]; + + if (storage->aub->nuPrev->nalRefIdc != nuNext->nalRefIdc && + (storage->aub->nuPrev->nalRefIdc == 0 || nuNext->nalRefIdc == 0)) + *accessUnitBoundaryFlag = HANTRO_TRUE; + + if ((storage->aub->nuPrev->nalUnitType == NAL_CODED_SLICE_IDR && + nuNext->nalUnitType != NAL_CODED_SLICE_IDR) || + (storage->aub->nuPrev->nalUnitType != NAL_CODED_SLICE_IDR && + nuNext->nalUnitType == NAL_CODED_SLICE_IDR)) + *accessUnitBoundaryFlag = HANTRO_TRUE; + + tmp = h264bsdCheckFrameNum(strm, sps->maxFrameNum, &frameNum); + if (tmp != HANTRO_OK) + return(HANTRO_NOK); + + if (storage->aub->prevFrameNum != frameNum) + { + storage->aub->prevFrameNum = frameNum; + *accessUnitBoundaryFlag = HANTRO_TRUE; + } + + if (nuNext->nalUnitType == NAL_CODED_SLICE_IDR) + { + tmp = h264bsdCheckIdrPicId(strm, sps->maxFrameNum, nuNext->nalUnitType, + &idrPicId); + if (tmp != HANTRO_OK) + return(HANTRO_NOK); + + if (storage->aub->nuPrev->nalUnitType == NAL_CODED_SLICE_IDR && + storage->aub->prevIdrPicId != idrPicId) + *accessUnitBoundaryFlag = HANTRO_TRUE; + + storage->aub->prevIdrPicId = idrPicId; + } + + if (sps->picOrderCntType == 0) + { + tmp = h264bsdCheckPicOrderCntLsb(strm, sps, nuNext->nalUnitType, + &picOrderCntLsb); + if (tmp != HANTRO_OK) + return(HANTRO_NOK); + + if (storage->aub->prevPicOrderCntLsb != picOrderCntLsb) + { + storage->aub->prevPicOrderCntLsb = picOrderCntLsb; + *accessUnitBoundaryFlag = HANTRO_TRUE; + } + + if (pps->picOrderPresentFlag) + { + tmp = h264bsdCheckDeltaPicOrderCntBottom(strm, sps, + nuNext->nalUnitType, &deltaPicOrderCntBottom); + if (tmp != HANTRO_OK) + return(tmp); + + if (storage->aub->prevDeltaPicOrderCntBottom != + deltaPicOrderCntBottom) + { + storage->aub->prevDeltaPicOrderCntBottom = + deltaPicOrderCntBottom; + *accessUnitBoundaryFlag = HANTRO_TRUE; + } + } + } + else if (sps->picOrderCntType == 1 && !sps->deltaPicOrderAlwaysZeroFlag) + { + tmp = h264bsdCheckDeltaPicOrderCnt(strm, sps, nuNext->nalUnitType, + pps->picOrderPresentFlag, deltaPicOrderCnt); + if (tmp != HANTRO_OK) + return(tmp); + + if (storage->aub->prevDeltaPicOrderCnt[0] != deltaPicOrderCnt[0]) + { + storage->aub->prevDeltaPicOrderCnt[0] = deltaPicOrderCnt[0]; + *accessUnitBoundaryFlag = HANTRO_TRUE; + } + + if (pps->picOrderPresentFlag) + if (storage->aub->prevDeltaPicOrderCnt[1] != deltaPicOrderCnt[1]) + { + storage->aub->prevDeltaPicOrderCnt[1] = deltaPicOrderCnt[1]; + *accessUnitBoundaryFlag = HANTRO_TRUE; + } + } + + *storage->aub->nuPrev = *nuNext; + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: CheckPps + + Functional description: + Check picture parameter set. Contents of the picture parameter + set information that depends on the image dimensions is checked + against the dimensions in the sps. + + Inputs: + pps pointer to picture paramter set + sps pointer to sequence parameter set + + Outputs: + none + + Returns: + HANTRO_OK everything ok + HANTRO_NOK invalid data in picture parameter set + +------------------------------------------------------------------------------*/ +u32 CheckPps(picParamSet_t *pps, seqParamSet_t *sps) +{ + + u32 i; + u32 picSize; + + picSize = sps->picWidthInMbs * sps->picHeightInMbs; + + /* check slice group params */ + if (pps->numSliceGroups > 1) + { + if (pps->sliceGroupMapType == 0) + { + ASSERT(pps->runLength); + for (i = 0; i < pps->numSliceGroups; i++) + { + if (pps->runLength[i] > picSize) + return(HANTRO_NOK); + } + } + else if (pps->sliceGroupMapType == 2) + { + ASSERT(pps->topLeft); + ASSERT(pps->bottomRight); + for (i = 0; i < pps->numSliceGroups-1; i++) + { + if (pps->topLeft[i] > pps->bottomRight[i] || + pps->bottomRight[i] >= picSize) + return(HANTRO_NOK); + + if ( (pps->topLeft[i] % sps->picWidthInMbs) > + (pps->bottomRight[i] % sps->picWidthInMbs) ) + return(HANTRO_NOK); + } + } + else if (pps->sliceGroupMapType > 2 && pps->sliceGroupMapType < 6) + { + if (pps->sliceGroupChangeRate > picSize) + return(HANTRO_NOK); + } + else if (pps->sliceGroupMapType == 6 && + pps->picSizeInMapUnits < picSize) + return(HANTRO_NOK); + } + + return(HANTRO_OK); +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdValidParamSets + + Functional description: + Check if any valid SPS/PPS combination exists in the storage. + Function tries each PPS in the buffer and checks if corresponding + SPS exists and calls CheckPps to determine if the PPS conforms + to image dimensions of the SPS. + + Inputs: + pStorage pointer to storage structure + + Outputs: + HANTRO_OK there is at least one valid combination + HANTRO_NOK no valid combinations found + + +------------------------------------------------------------------------------*/ + +u32 h264bsdValidParamSets(storage_t *pStorage) +{ + +/* Variables */ + + u32 i; + +/* Code */ + + ASSERT(pStorage); + + for (i = 0; i < MAX_NUM_PIC_PARAM_SETS; i++) + { + if ( pStorage->pps[i] && + pStorage->sps[pStorage->pps[i]->seqParameterSetId] && + CheckPps(pStorage->pps[i], + pStorage->sps[pStorage->pps[i]->seqParameterSetId]) == + HANTRO_OK) + { + return(HANTRO_OK); + } + } + + return(HANTRO_NOK); + +} + diff --git a/src/video/h264_decoder/h264bsd_storage.h b/src/video/h264_decoder/h264bsd_storage.h new file mode 100644 index 000000000..ee796b4db --- /dev/null +++ b/src/video/h264_decoder/h264bsd_storage.h @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_STORAGE_H +#define H264SWDEC_STORAGE_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_cfg.h" +#include "h264bsd_seq_param_set.h" +#include "h264bsd_pic_param_set.h" +#include "h264bsd_macroblock_layer.h" +#include "h264bsd_nal_unit.h" +#include "h264bsd_slice_header.h" +#include "h264bsd_seq_param_set.h" +#include "h264bsd_dpb.h" +#include "h264bsd_pic_order_cnt.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +typedef struct +{ + u32 sliceId; + u32 numDecodedMbs; + u32 lastMbAddr; +} sliceStorage_t; + +/* structure to store parameters needed for access unit boundary checking */ +typedef struct +{ + nalUnit_t nuPrev[1]; + u32 prevFrameNum; + u32 prevIdrPicId; + u32 prevPicOrderCntLsb; + i32 prevDeltaPicOrderCntBottom; + i32 prevDeltaPicOrderCnt[2]; + u32 firstCallFlag; +} aubCheck_t; + +#ifndef NUM_PARALLEL_MACROBLOCKS +#define NUM_PARALLEL_MACROBLOCKS 1 +#endif + +typedef void (*h264bsdSeiCallback)( + void *ctx, + u32 payloadType, + const u8 *payload, + u32 payloadSize); + + +/* storage data structure, holds all data of a decoder instance */ +typedef struct +{ + /* active paramet set ids and pointers */ + u32 oldSpsId; + u32 activePpsId; + u32 activeSpsId; + picParamSet_t *activePps; + seqParamSet_t *activeSps; + seqParamSet_t *sps[MAX_NUM_SEQ_PARAM_SETS]; + picParamSet_t *pps[MAX_NUM_PIC_PARAM_SETS]; + + /* current slice group map, recomputed for each slice */ + u32 *sliceGroupMap; + + u32 picSizeInMbs; + + /* this flag is set after all macroblocks of a picture successfully + * decoded -> redundant slices not decoded */ + u32 skipRedundantSlices; + u32 picStarted; + + /* flag to indicate if current access unit contains any valid slices */ + u32 validSliceInAccessUnit; + + /* store information needed for handling of slice decoding */ + sliceStorage_t slice[1]; + + /* number of concealed macroblocks in the current image */ + u32 numConcealedMbs; + + /* picId given by application */ + u32 currentPicId; + + /* macroblock specific storages, size determined by image dimensions */ + mbStorage_t *mb; + + /* flag to store noOutputReordering flag set by the application */ + u32 noReordering; + + /* rasky: max number of fully decoded pictures that can be kept in the DPB output + buffer as a "cushion" for the client (0 = disabled). This is a runtime + configuration set by the application before decoding starts. */ + u32 maxNumBufferedPics; + + /* Optional application callback for decoded SEI payloads. */ + h264bsdSeiCallback seiCallback; + void *seiCallbackCtx; + + /* DPB */ + dpbStorage_t dpb[1]; + + /* structure to store picture order count related information */ + pocStorage_t poc[1]; + + /* access unit boundary checking related data */ + aubCheck_t aub[1]; + + /* current processed image */ + image_t currImage[1]; + + /* last valid NAL unit header is stored here */ + nalUnit_t prevNalUnit[1]; + + /* slice header, second structure used as a temporary storage while + * decoding slice header, first one stores last successfully decoded + * slice header */ + sliceHeader_t sliceHeader[2]; + + /* fields to store old stream buffer pointers, needed when only part of + * a stream buffer is processed by h264bsdDecode function */ + u32 prevBufNotFinished; + u8 *prevBufPointer; + u32 prevBytesConsumed; + strmData_t strm[1]; + + /* Mid-slice timeslice state for h264bsdDecodePartial. Opaque to callers; + * query with h264bsdIsSlicePending(). sliceMbBudget is MBs per call + * (0 = unlimited), set by DecodePartial. */ + u32 sliceDataPending; + u32 sliceMbBudget; + u32 sliceMbAddr; + u32 sliceSkipRun; + u32 slicePrevSkipped; + u32 sliceMbCount; + i32 sliceQpY; + + /* macroblock layer structure, there is no need to store this but it + * would have increased the stack size excessively and needed to be + * allocated from head -> easiest to put it here */ + /* On N64, we want multiple instances of this structure because we + allow the RSP to lag behind the CPU decoding with its task queue. */ + macroblockLayer_t mbLayers[NUM_PARALLEL_MACROBLOCKS]; + int mbLayerIdx; + + u32 pendingActivation; /* Activate parameter sets after returning + HEADERS_RDY to the user */ + u32 intraConcealmentFlag; /* 0 gray picture for corrupted intra + 1 previous frame used if available */ +} storage_t; + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +void h264bsdInitStorage(storage_t *pStorage); +void h264bsdResetStorage(storage_t *pStorage); +void h264bsdRewindStorage(storage_t *pStorage); +u32 h264bsdIsStartOfPicture(storage_t *pStorage); +u32 h264bsdIsEndOfPicture(storage_t *pStorage); +u32 h264bsdStoreSeqParamSet(storage_t *pStorage, seqParamSet_t *pSeqParamSet); +u32 h264bsdStorePicParamSet(storage_t *pStorage, picParamSet_t *pPicParamSet); +u32 h264bsdActivateParamSets(storage_t *pStorage, u32 ppsId, u32 isIdr); +void h264bsdComputeSliceGroupMap(storage_t *pStorage, + u32 sliceGroupChangeCycle); + +u32 h264bsdCheckAccessUnitBoundary( + strmData_t *strm, + nalUnit_t *nuNext, + storage_t *storage, + u32 *accessUnitBoundaryFlag); + +u32 h264bsdValidParamSets(storage_t *pStorage); + +#endif /* #ifdef H264SWDEC_STORAGE_H */ + diff --git a/src/video/h264_decoder/h264bsd_stream.c b/src/video/h264_decoder/h264bsd_stream.c new file mode 100644 index 000000000..36aa5bce2 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_stream.c @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdGetBits + h264bsdShowBits32 + h264bsdFlushBits + h264bsdIsByteAligned + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_util.h" +#include "h264bsd_stream.h" + +#include "../profile.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + + Function: h264bsdGetBits + + Functional description: + Read and remove bits from the stream buffer. + + Input: + pStrmData pointer to stream data structure + numBits number of bits to read + + Output: + none + + Returns: + bits read from stream + END_OF_STREAM if not enough bits left + +------------------------------------------------------------------------------*/ +#ifndef H264BSD_N64 +u32 h264bsdGetBits(strmData_t *pStrmData, u32 numBits) +{ + + u32 out; + + ASSERT(pStrmData); + ASSERT(numBits < 32); + + out = h264bsdShowBits32(pStrmData) >> (32 - numBits); + + if (h264bsdFlushBits(pStrmData, numBits) == HANTRO_OK) + { + return(out); + } + else + { + return(END_OF_STREAM); + } + +} +#endif +/*------------------------------------------------------------------------------ + + Function: h264bsdShowBits32 + + Functional description: + Read 32 bits from the stream buffer. Buffer is left as it is, i.e. + no bits are removed. First bit read from the stream is the MSB of + the return value. If there is not enough bits in the buffer -> + bits beyong the end of the stream are set to '0' in the return + value. + + Input: + pStrmData pointer to stream data structure + + Output: + none + + Returns: + bits read from stream + +------------------------------------------------------------------------------*/ +#ifndef H264BSD_N64 +u32 h264bsdShowBits32(strmData_t *pStrmData) +{ + + i32 bits, shift; + u32 out; + u8 *pStrm; + + ASSERT(pStrmData); + ASSERT(pStrmData->pStrmCurrPos); + ASSERT(pStrmData->bitPosInWord < 8); + ASSERT(pStrmData->bitPosInWord == + (pStrmData->strmBuffReadBits & 0x7)); + + pStrm = pStrmData->pStrmCurrPos; + + /* number of bits left in the buffer */ + bits = (i32)pStrmData->strmBuffSize*8 - (i32)pStrmData->strmBuffReadBits; + + /* at least 32-bits in the buffer */ + if (bits >= 32) + { + u32 bitPosInWord = pStrmData->bitPosInWord; + out = ((u32)pStrm[0] << 24) | ((u32)pStrm[1] << 16) | + ((u32)pStrm[2] << 8) | ((u32)pStrm[3]); + + if (bitPosInWord) + { + u32 byte = (u32)pStrm[4]; + u32 tmp = (8-bitPosInWord); + out <<= bitPosInWord; + out |= byte>>tmp; + } + return (out); + } + /* at least one bit in the buffer */ + else if (bits > 0) + { + shift = (i32)(24 + pStrmData->bitPosInWord); + out = (u32)(*pStrm++) << shift; + bits -= (i32)(8 - pStrmData->bitPosInWord); + while (bits > 0) + { + shift -= 8; + out |= (u32)(*pStrm++) << shift; + bits -= 8; + } + return (out); + } + else { + return (0); + } + +} +#endif /* H264BSD_N64 */ + +/*------------------------------------------------------------------------------ + + Function: h264bsdFlushBits + + Functional description: + Remove bits from the stream buffer + + Input: + pStrmData pointer to stream data structure + numBits number of bits to remove + + Output: + none + + Returns: + HANTRO_OK success + END_OF_STREAM not enough bits left + +------------------------------------------------------------------------------*/ +#ifndef H264BSD_N64 +#ifndef H264DEC_NEON +u32 h264bsdFlushBits(strmData_t *pStrmData, u32 numBits) +{ + + ASSERT(pStrmData); + ASSERT(pStrmData->pStrmBuffStart); + ASSERT(pStrmData->pStrmCurrPos); + ASSERT(pStrmData->bitPosInWord < 8); + ASSERT(pStrmData->bitPosInWord == (pStrmData->strmBuffReadBits & 0x7)); + + pStrmData->strmBuffReadBits += numBits; + pStrmData->bitPosInWord = pStrmData->strmBuffReadBits & 0x7; + if ( (pStrmData->strmBuffReadBits ) <= (8*pStrmData->strmBuffSize) ) + { + pStrmData->pStrmCurrPos = pStrmData->pStrmBuffStart + + (pStrmData->strmBuffReadBits >> 3); + return(HANTRO_OK); + } + else + return(END_OF_STREAM); + +} +#endif +#endif +/*------------------------------------------------------------------------------ + + Function: h264bsdIsByteAligned + + Functional description: + Check if current stream position is byte aligned. + + Inputs: + pStrmData pointer to stream data structure + + Outputs: + none + + Returns: + TRUE stream is byte aligned + FALSE stream is not byte aligned + +------------------------------------------------------------------------------*/ +#ifndef H264BSD_N64 +u32 h264bsdIsByteAligned(strmData_t *pStrmData) +{ + +/* Variables */ + +/* Code */ + + if (!pStrmData->bitPosInWord) + return(HANTRO_TRUE); + else + return(HANTRO_FALSE); + +} +#endif diff --git a/src/video/h264_decoder/h264bsd_stream.h b/src/video/h264_decoder/h264bsd_stream.h new file mode 100644 index 000000000..e4d376308 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_stream.h @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_STREAM_H +#define H264SWDEC_STREAM_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "../profile.h" + + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +#ifdef H264BSD_N64 +// strmData_t structure optimized for speed on N64. By relying on the fact that +// on N64 pointers are 32-bits, we stuff both the byte pointer and the bit offset +// within the same entity pCurr (lowest 3 bits are the bit offset). This way +// it's possible to manipulate bit pointers very easily and still extract the +// actual memory pointer when required. +typedef struct +{ + u64 pCurr; /* bit-pointer to the current position in the buffer */ + u64 pEnd; /* bit-pointer to the end of the buffer */ + u8 *pStart; /* byte pointer to the start of the buffer */ +} strmData_t; + +#define STRM_CURR_PTR(s) ((uint8_t*)(u32)(pStrmData->pCurr>>3)) +#define STRM_CURR_BITOFF(s) (pStrmData->pCurr & 7) + +#else +typedef struct +{ + u8 *pStrmBuffStart; /* pointer to start of stream buffer */ + u8 *pStrmCurrPos; /* current read address in stream buffer */ + u32 bitPosInWord; /* bit position in stream buffer byte */ + u32 strmBuffSize; /* size of stream buffer (bytes) */ + u32 strmBuffReadBits; /* number of bits read from stream buffer */ +} strmData_t; +#endif +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +// On N64, to improve performance, make all functions as inline. +#ifndef H264BSD_N64 +u32 h264bsdIsByteAligned(strmData_t *); +u32 h264bsdFlushBits(strmData_t *pStrmData, u32 numBits); +u32 h264bsdGetBits(strmData_t *pStrmData, u32 numBits); +u32 h264bsdShowBits32(strmData_t *pStrmData); +#else + +static inline u32 h264bsdIsByteAligned(strmData_t *pStrmData) { + return (pStrmData->pCurr & 7) == 0; +} + +static inline u32 h264bsdShowBits32(strmData_t *pStrmData) { + // Access the byte-aligned 64-bit word at the current bitpointer. Since the + // word is not 64-byte aligned, we cannot cast to u64* and instead we need + // to use byte accesses (the compiler will still generate efficient + // code with the MIPS ldl/ldr opcodes). + // Notice that this could potentially overflow the buffer (read past the + // end pointer), but assuming that the caller is not buggy (eg: requesting + // bits past the end of the buffer) it does not really matter on N64, and + // only the requested bits will be masked and returned anyway. + u8 *pStrm = STRM_CURR_PTR(pStrmData); + u64 val = ((u64)pStrm[0] << 56) | ((u64)pStrm[1] << 48) | + ((u64)pStrm[2] << 40) | ((u64)pStrm[3] << 32) | + ((u64)pStrm[4] << 24) | ((u64)pStrm[5] << 16) | + ((u64)pStrm[6] << 8) | ((u64)pStrm[7] << 0); + val <<= STRM_CURR_BITOFF(pStrmData); + val >>= 32; + return (u32)val; +} + +static inline u64 h264bsdShowBits64(strmData_t *pStrmData, u32 *bits) { + u8 *pStrm = STRM_CURR_PTR(pStrmData); + u64 val = ((u64)pStrm[0] << 56) | ((u64)pStrm[1] << 48) | + ((u64)pStrm[2] << 40) | ((u64)pStrm[3] << 32) | + ((u64)pStrm[4] << 24) | ((u64)pStrm[5] << 16) | + ((u64)pStrm[6] << 8) | ((u64)pStrm[7] << 0); + val <<= STRM_CURR_BITOFF(pStrmData); + *bits = 64 - STRM_CURR_BITOFF(pStrmData); + pStrmData->pCurr &= ~7; + return val; +} + +#ifndef HANTRO_OK +#define HANTRO_OK 0 +#endif +#ifndef END_OF_STREAM +#define END_OF_STREAM 0xFFFFFFFFU +#endif + +static inline u32 h264bsdFlushBits(strmData_t *pStrmData, u32 numBits) { + pStrmData->pCurr += numBits; + if ( pStrmData->pCurr < pStrmData->pEnd ) + return(HANTRO_OK); + else + return(END_OF_STREAM); +} + +static inline u32 h264bsdGetBits(strmData_t *pStrmData, u32 numBits) { + u32 out = h264bsdShowBits32(pStrmData) >> (32 - numBits); + if (h264bsdFlushBits(pStrmData, numBits) == HANTRO_OK) + return(out); + else + return(END_OF_STREAM); +} + +#endif + + +#endif /* #ifdef H264SWDEC_STREAM_H */ + diff --git a/src/video/h264_decoder/h264bsd_transform.c b/src/video/h264_decoder/h264bsd_transform.c new file mode 100644 index 000000000..4eb6dd090 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_transform.c @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdProcessBlock + h264bsdProcessLumaDc + h264bsdProcessChromaDc + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_transform.h" +#include "h264bsd_util.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/* Switch off the following Lint messages for this file: + * Info 701: Shift left of signed quantity (int) + * Info 702: Shift right of signed quantity (int) + */ +/*lint -e701 -e702 */ + +/* LevelScale function */ +static const i32 levelScale[6][3] = { + {10,13,16}, {11,14,18}, {13,16,20}, {14,18,23}, {16,20,25}, {18,23,29}}; + +/* qp % 6 as a function of qp */ +static const u8 qpMod6[52] = {0,1,2,3,4,5,0,1,2,3,4,5,0,1,2,3,4,5,0,1,2,3,4,5, + 0,1,2,3,4,5,0,1,2,3,4,5,0,1,2,3,4,5,0,1,2,3,4,5,0,1,2,3}; + +/* qp / 6 as a function of qp */ +static const u8 qpDiv6[52] = {0,0,0,0,0,0,1,1,1,1,1,1,2,2,2,2,2,2,3,3,3,3,3,3, + 4,4,4,4,4,4,5,5,5,5,5,5,6,6,6,6,6,6,7,7,7,7,7,7,8,8,8,8}; + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + + Function: h264bsdProcessBlock + + Functional description: + Function performs inverse zig-zag scan, inverse scaling and + inverse transform for a luma or a chroma residual block + + Inputs: + data pointer to data to be processed + qp quantization parameter + skip skip processing of data[0], set to non-zero value + if dc coeff hanled separately + coeffMap 16 lsb's indicate which coeffs are non-zero, + bit 0 (lsb) for coeff 0, bit 1 for coeff 1 etc. + + Outputs: + data processed data + + Returns: + HANTRO_OK success + HANTRO_NOK processed data not in valid range [-512, 511] + +------------------------------------------------------------------------------*/ +u32 h264bsdProcessBlock(i32 *data, u32 qp, u32 skip, u32 coeffMap) +{ + +/* Variables */ + + i32 tmp0, tmp1, tmp2, tmp3; + i32 d1, d2, d3; + u32 row,col; + u32 qpDiv; + i32 *ptr; + +/* Code */ + + qpDiv = qpDiv6[qp]; + tmp1 = levelScale[qpMod6[qp]][0] << qpDiv; + tmp2 = levelScale[qpMod6[qp]][1] << qpDiv; + tmp3 = levelScale[qpMod6[qp]][2] << qpDiv; + + if (!skip) + data[0] = (data[0] * tmp1); + + /* at least one of the rows 1, 2 or 3 contain non-zero coeffs, mask takes + * the scanning order into account */ + if (coeffMap & 0xFF9C) + { + /* do the zig-zag scan and inverse quantization */ + d1 = data[1]; + d2 = data[14]; + d3 = data[15]; + data[1] = (d1 * tmp2); + data[14] = (d2 * tmp2); + data[15] = (d3 * tmp3); + + d1 = data[2]; + d2 = data[5]; + d3 = data[4]; + data[4] = (d1 * tmp2); + data[2] = (d2 * tmp1); + data[5] = (d3 * tmp3); + + d1 = data[8]; + d2 = data[3]; + d3 = data[6]; + tmp0 = (d1 * tmp2); + data[8] = (d2 * tmp1); + data[3] = (d3 * tmp2); + d1 = data[7]; + d2 = data[12]; + d3 = data[9]; + data[6] = (d1 * tmp2); + data[7] = (d2 * tmp3); + data[12] = (d3 * tmp2); + data[9] = tmp0; + + d1 = data[10]; + d2 = data[11]; + d3 = data[13]; + data[13] = (d1 * tmp3); + data[10] = (d2 * tmp1); + data[11] = (d3 * tmp2); + + /* horizontal transform */ + for (row = 4, ptr = data; row--; ptr += 4) + { + tmp0 = ptr[0] + ptr[2]; + tmp1 = ptr[0] - ptr[2]; + tmp2 = (ptr[1] >> 1) - ptr[3]; + tmp3 = ptr[1] + (ptr[3] >> 1); + ptr[0] = tmp0 + tmp3; + ptr[1] = tmp1 + tmp2; + ptr[2] = tmp1 - tmp2; + ptr[3] = tmp0 - tmp3; + } + + /*lint +e661 +e662*/ + /* then vertical transform */ + for (col = 4; col--; data++) + { + tmp0 = data[0] + data[8]; + tmp1 = data[0] - data[8]; + tmp2 = (data[4] >> 1) - data[12]; + tmp3 = data[4] + (data[12] >> 1); + data[0 ] = (tmp0 + tmp3 + 32)>>6; + data[4 ] = (tmp1 + tmp2 + 32)>>6; + data[8 ] = (tmp1 - tmp2 + 32)>>6; + data[12] = (tmp0 - tmp3 + 32)>>6; + /* check that each value is in the range [-512,511] */ + if (((u32)(data[0] + 512) > 1023) || + ((u32)(data[4] + 512) > 1023) || + ((u32)(data[8] + 512) > 1023) || + ((u32)(data[12] + 512) > 1023) ) + return(HANTRO_NOK); + } + } + else /* rows 1, 2 and 3 are zero */ + { + /* only dc-coeff is non-zero, i.e. coeffs at original positions + * 1, 5 and 6 are zero */ + if ((coeffMap & 0x62) == 0) + { + tmp0 = (data[0] + 32) >> 6; + /* check that value is in the range [-512,511] */ + if ((u32)(tmp0 + 512) > 1023) + return(HANTRO_NOK); + data[0] = data[1] = data[2] = data[3] = data[4] = data[5] = + data[6] = data[7] = data[8] = data[9] = data[10] = + data[11] = data[12] = data[13] = data[14] = data[15] = + tmp0; + } + else /* at least one of the coeffs 1, 5 or 6 is non-zero */ + { + data[1] = (data[1] * tmp2); + data[2] = (data[5] * tmp1); + data[3] = (data[6] * tmp2); + tmp0 = data[0] + data[2]; + tmp1 = data[0] - data[2]; + tmp2 = (data[1] >> 1) - data[3]; + tmp3 = data[1] + (data[3] >> 1); + data[0] = (tmp0 + tmp3 + 32)>>6; + data[1] = (tmp1 + tmp2 + 32)>>6; + data[2] = (tmp1 - tmp2 + 32)>>6; + data[3] = (tmp0 - tmp3 + 32)>>6; + data[4] = data[8] = data[12] = data[0]; + data[5] = data[9] = data[13] = data[1]; + data[6] = data[10] = data[14] = data[2]; + data[7] = data[11] = data[15] = data[3]; + /* check that each value is in the range [-512,511] */ + if (((u32)(data[0] + 512) > 1023) || + ((u32)(data[1] + 512) > 1023) || + ((u32)(data[2] + 512) > 1023) || + ((u32)(data[3] + 512) > 1023) ) + return(HANTRO_NOK); + } + } + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdProcessLumaDc + + Functional description: + Function performs inverse zig-zag scan, inverse transform and + inverse scaling for a luma DC coefficients block + + Inputs: + data pointer to data to be processed + qp quantization parameter + + Outputs: + data processed data + + Returns: + none + +------------------------------------------------------------------------------*/ +void h264bsdProcessLumaDc(i32 *data, u32 qp) +{ + +/* Variables */ + + i32 tmp0, tmp1, tmp2, tmp3; + u32 row,col; + u32 qpMod, qpDiv; + i32 levScale; + i32 *ptr; + +/* Code */ + + qpMod = qpMod6[qp]; + qpDiv = qpDiv6[qp]; + + /* zig-zag scan */ + tmp0 = data[2]; + data[2] = data[5]; + data[5] = data[4]; + data[4] = tmp0; + + tmp0 = data[8]; + data[8] = data[3]; + data[3] = data[6]; + data[6] = data[7]; + data[7] = data[12]; + data[12] = data[9]; + data[9] = tmp0; + + tmp0 = data[10]; + data[10] = data[11]; + data[11] = data[13]; + data[13] = tmp0; + + /* horizontal transform */ + for (row = 4, ptr = data; row--; ptr += 4) + { + tmp0 = ptr[0] + ptr[2]; + tmp1 = ptr[0] - ptr[2]; + tmp2 = ptr[1] - ptr[3]; + tmp3 = ptr[1] + ptr[3]; + ptr[0] = tmp0 + tmp3; + ptr[1] = tmp1 + tmp2; + ptr[2] = tmp1 - tmp2; + ptr[3] = tmp0 - tmp3; + } + + /*lint +e661 +e662*/ + /* then vertical transform and inverse scaling */ + levScale = levelScale[ qpMod ][0]; + if (qp >= 12) + { + levScale <<= (qpDiv-2); + for (col = 4; col--; data++) + { + tmp0 = data[0] + data[8 ]; + tmp1 = data[0] - data[8 ]; + tmp2 = data[4] - data[12]; + tmp3 = data[4] + data[12]; + data[0 ] = ((tmp0 + tmp3)*levScale); + data[4 ] = ((tmp1 + tmp2)*levScale); + data[8 ] = ((tmp1 - tmp2)*levScale); + data[12] = ((tmp0 - tmp3)*levScale); + } + } + else + { + i32 tmp; + tmp = ((1 - qpDiv) == 0) ? 1 : 2; + for (col = 4; col--; data++) + { + tmp0 = data[0] + data[8 ]; + tmp1 = data[0] - data[8 ]; + tmp2 = data[4] - data[12]; + tmp3 = data[4] + data[12]; + data[0 ] = ((tmp0 + tmp3)*levScale+tmp) >> (2-qpDiv); + data[4 ] = ((tmp1 + tmp2)*levScale+tmp) >> (2-qpDiv); + data[8 ] = ((tmp1 - tmp2)*levScale+tmp) >> (2-qpDiv); + data[12] = ((tmp0 - tmp3)*levScale+tmp) >> (2-qpDiv); + } + } + +} + +/*------------------------------------------------------------------------------ + + Function: h264bsdProcessChromaDc + + Functional description: + Function performs inverse transform and inverse scaling for a + chroma DC coefficients block + + Inputs: + data pointer to data to be processed + qp quantization parameter + + Outputs: + data processed data + + Returns: + none + +------------------------------------------------------------------------------*/ +void h264bsdProcessChromaDc(i32 *data, u32 qp) +{ + +/* Variables */ + + i32 tmp0, tmp1, tmp2, tmp3; + u32 qpDiv; + i32 levScale; + u32 levShift; + +/* Code */ + + qpDiv = qpDiv6[qp]; + levScale = levelScale[ qpMod6[qp] ][0]; + + if (qp >= 6) + { + levScale <<= (qpDiv-1); + levShift = 0; + } + else + { + levShift = 1; + } + + tmp0 = data[0] + data[2]; + tmp1 = data[0] - data[2]; + tmp2 = data[1] - data[3]; + tmp3 = data[1] + data[3]; + data[0] = ((tmp0 + tmp3) * levScale) >> levShift; + data[1] = ((tmp0 - tmp3) * levScale) >> levShift; + data[2] = ((tmp1 + tmp2) * levScale) >> levShift; + data[3] = ((tmp1 - tmp2) * levScale) >> levShift; + tmp0 = data[4] + data[6]; + tmp1 = data[4] - data[6]; + tmp2 = data[5] - data[7]; + tmp3 = data[5] + data[7]; + data[4] = ((tmp0 + tmp3) * levScale) >> levShift; + data[5] = ((tmp0 - tmp3) * levScale) >> levShift; + data[6] = ((tmp1 + tmp2) * levScale) >> levShift; + data[7] = ((tmp1 - tmp2) * levScale) >> levShift; + +} + +/*lint +e701 +e702 */ + + diff --git a/src/video/h264_decoder/h264bsd_transform.h b/src/video/h264_decoder/h264bsd_transform.h new file mode 100644 index 000000000..4f41a23e4 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_transform.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_TRANSFORM_H +#define H264SWDEC_TRANSFORM_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +u32 h264bsdProcessBlock(i32 *data, u32 qp, u32 skip, u32 coeffMap); +void h264bsdProcessLumaDc(i32 *data, u32 qp); +void h264bsdProcessChromaDc(i32 *data, u32 qp); + +#endif /* #ifdef H264SWDEC_TRANSFORM_H */ + diff --git a/src/video/h264_decoder/h264bsd_util.c b/src/video/h264_decoder/h264bsd_util.c new file mode 100644 index 000000000..728078382 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_util.c @@ -0,0 +1,459 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdCountLeadingZeros + h264bsdRbspTrailingBits + h264bsdMoreRbspData + h264bsdNextMbAddress + h264bsdSetCurrImageMbPointers + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_util.h" +#include + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/* look-up table for expected values of stuffing bits */ +static const u32 stuffingTable[8] = {0x1,0x2,0x4,0x8,0x10,0x20,0x40,0x80}; + +/* look-up table for chroma quantization parameter as a function of luma QP */ +const u32 h264bsdQpC[52] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19, + 20,21,22,23,24,25,26,27,28,29,29,30,31,32,32,33,34,34,35,35,36,36,37,37,37, + 38,38,38,39,39,39,39}; + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + + 5.1 Function: h264bsdCountLeadingZeros + + Functional description: + Count leading zeros in a code word. Code word is assumed to be + right-aligned, last bit of the code word in the lsb of the value. + + Inputs: + value code word + length number of bits in the code word + + Outputs: + none + + Returns: + number of leading zeros in the code word + +------------------------------------------------------------------------------*/ +#ifndef H264DEC_NEON +u32 h264bsdCountLeadingZeros(u32 value, u32 length) +{ + +/* Variables */ + + u32 zeros = 0; + u32 mask = 1 << (length - 1); + +/* Code */ + + ASSERT(length <= 32); + + while (mask && !(value & mask)) + { + zeros++; + mask >>= 1; + } + return(zeros); + +} +#endif +/*------------------------------------------------------------------------------ + + 5.2 Function: h264bsdRbspTrailingBits + + Functional description: + Check Raw Byte Stream Payload (RBSP) trailing bits, i.e. stuffing. + Rest of the current byte (whole byte if allready byte aligned) + in the stream buffer shall contain a '1' bit followed by zero or + more '0' bits. + + Inputs: + pStrmData pointer to stream data structure + + Outputs: + none + + Returns: + HANTRO_OK RBSP trailing bits found + HANTRO_NOK otherwise + +------------------------------------------------------------------------------*/ + +u32 h264bsdRbspTrailingBits(strmData_t *pStrmData) +{ + +/* Variables */ + + u32 stuffing; + u32 stuffingLength; + +/* Code */ + + ASSERT(pStrmData); + + #ifndef H264BSD_N64 + ASSERT(pStrmData->bitPosInWord < 8); + stuffingLength = 8 - pStrmData->bitPosInWord; + #else + ASSERT(STRM_CURR_BITOFF(pStrmData) < 8); + stuffingLength = 8 - STRM_CURR_BITOFF(pStrmData); + #endif + + stuffing = h264bsdGetBits(pStrmData, stuffingLength); + if (stuffing == END_OF_STREAM) + return(HANTRO_NOK); + + if (stuffing != stuffingTable[stuffingLength - 1]) + return(HANTRO_NOK); + else + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + 5.3 Function: h264bsdMoreRbspData + + Functional description: + Check if there is more data in the current RBSP. The standard + defines this function so that there is more data if + -more than 8 bits left or + -last bits are not RBSP trailing bits + + Inputs: + pStrmData pointer to stream data structure + + Outputs: + none + + Returns: + HANTRO_TRUE there is more data + HANTRO_FALSE no more data + +------------------------------------------------------------------------------*/ + +u32 h264bsdMoreRbspData(strmData_t *pStrmData) +{ + +/* Variables */ + + u32 bits; + +/* Code */ + + ASSERT(pStrmData); + + #ifndef H264BSD_N64 + ASSERT(pStrmData->strmBuffReadBits <= 8 * pStrmData->strmBuffSize); + bits = pStrmData->strmBuffSize * 8 - pStrmData->strmBuffReadBits; + #else + ASSERT(pStrmData->pCurr <= pStrmData->pEnd); + bits = pStrmData->pEnd - pStrmData->pCurr; + #endif + + if (bits == 0) + return(HANTRO_FALSE); + + if ( (bits > 8) || + ((h264bsdShowBits32(pStrmData)>>(32-bits)) != (1ul << (bits-1))) ) + return(HANTRO_TRUE); + else + return(HANTRO_FALSE); + +} + +/*------------------------------------------------------------------------------ + + 5.4 Function: h264bsdNextMbAddress + + Functional description: + Get address of the next macroblock in the current slice group. + + Inputs: + pSliceGroupMap slice group for each macroblock + picSizeInMbs size of the picture + currMbAddr where to start + + Outputs: + none + + Returns: + address of the next macroblock + 0 if none of the following macroblocks belong to same slice + group as currMbAddr + +------------------------------------------------------------------------------*/ + +u32 h264bsdNextMbAddress(u32 *pSliceGroupMap, u32 picSizeInMbs, u32 currMbAddr) +{ + +/* Variables */ + + u32 i, sliceGroup; + +/* Code */ + + ASSERT(pSliceGroupMap); + ASSERT(picSizeInMbs); + ASSERT(currMbAddr < picSizeInMbs); + + sliceGroup = pSliceGroupMap[currMbAddr]; + + i = currMbAddr + 1; + while ((i < picSizeInMbs) && (pSliceGroupMap[i] != sliceGroup)) + { + i++; + } + + if (i == picSizeInMbs) + i = 0; + + return(i); + +} + + +/*------------------------------------------------------------------------------ + + 5.5 Function: h264bsdSetCurrImageMbPointers + + Functional description: + Set luma and chroma pointers in image_t for current MB + + Inputs: + image Current image + mbNum number of current MB + + Outputs: + none + + Returns: + none +------------------------------------------------------------------------------*/ +void h264bsdSetCurrImageMbPointers(image_t *image, u32 mbNum) +{ + u32 width, height; + u32 picSize; + u32 row, col; + u32 tmp; + + width = image->width; + height = image->height; + row = mbNum / width; + col = mbNum % width; + + tmp = row * width; + picSize = width * height; + + image->luma = (u8*)(image->data + col * 16 + tmp * 256); + image->cb = (u8*)(image->data + picSize * 256 + tmp * 64 + col * 8); + image->cr = (u8*)(image->cb + picSize * 64); +} + + +// useful to export slice information +void h264bsdWriteSliceMbData(image_t *image, u32 firstMbNum, u32 lastMbNum, u32* pData) +{ + u32 width, height; + u32 picSize; + u32 tmp; + + width = image->width; + height = image->height; + picSize = width * height; + + u32 firstRow = firstMbNum / width; + u32 firstCol = firstMbNum % width; + + u32 lastRow = lastMbNum / width; + u32 lastCol = lastMbNum % width; + + // number of cols per row + //u32 cols = width; + + u32 cb = picSize * 256; + u32 cr = cb + picSize * 64; + + // len 6 + 3 = 18 + + if (firstRow == lastRow){ + tmp = firstRow * width; + *pData = (tmp * 256) + (firstCol * 16); + pData += 1; + + *pData = (tmp * 256) + (lastCol * 16) + 16; // + 16 to make it after last col + pData += 1; + + *pData = cb + (tmp * 64) + (firstCol * 8); + pData += 1; + + *pData = cb + (tmp * 64) + (lastCol * 8) + 8; // + 16 to make it after last col + pData += 1; + + *pData = cr + (tmp * 64) + (firstCol * 8); + pData += 1; + + *pData = cr + (tmp * 64) + (lastCol * 8) + 8; // + 16 to make it after last col + pData += 1; + + // rest + memset(pData, 0, 12 * 4); + + + }else{ + tmp = firstRow * width; + if (firstCol > 0){ + *pData = (tmp * 256) + (firstCol * 16); + pData += 1; + *pData = (tmp * 256) + (width * 16); // after last col + pData += 1; + + *pData = cb + (tmp * 64) + (firstCol * 8); + pData += 1; + *pData = cb + (tmp * 64) + (width * 8); // after last col + pData += 1; + + *pData = cr + (tmp * 64) + (firstCol * 8); + pData += 1; + *pData = cr + (tmp * 64) + (width * 8); // after last col + pData += 1; + + firstRow += 1; + }else{ + *pData = 0; + pData += 1; + *pData = 0; + pData += 1; + + *pData = 0; + pData += 1; + *pData = 0; + pData += 1; + + *pData = 0; + pData += 1; + *pData = 0; + pData += 1; + }; + + if (lastCol + 1 != width){ + tmp = lastRow * width; + + *pData = (tmp * 256); + pData += 1; + *pData = (tmp * 256) + (lastCol * 16) + 16; // + 16 to make it after last col + pData += 1; + + *pData = cb + (tmp * 64); + pData += 1; + *pData = cb + (tmp * 64) + (lastCol * 8) + 8; // + 16 to make it after last col + pData += 1; + + *pData = cr + (tmp * 64); + pData += 1; + *pData = cr + (tmp * 64) + (lastCol * 8) + 8; // + 16 to make it after last col + pData += 1; + + lastRow -= 1; + }else{ + *pData = 0; + pData += 1; + *pData = 0; + pData += 1; + + *pData = 0; + pData += 1; + *pData = 0; + pData += 1; + + *pData = 0; + pData += 1; + *pData = 0; + pData += 1; + }; + + if (lastRow >= firstRow){ + tmp = firstRow * width; + + *pData = (tmp * 256); + pData += 1; + + *pData = cb + (tmp * 64); + pData += 1; + + *pData = cr + (tmp * 64); + pData += 1; + + + tmp = (lastRow + 1) * width; + + *pData = (tmp * 256); // after last row + pData += 1; + + *pData = cb + (tmp * 64); // after last row + pData += 1; + + *pData = cr + (tmp * 64); // after last row + pData += 1; + + }else{ + *pData = 0; + pData += 1; + *pData = 0; + pData += 1; + *pData = 0; + pData += 1; + *pData = 0; + pData += 1; + *pData = 0; + pData += 1; + *pData = 0; + pData += 1; + }; + + }; + +} + diff --git a/src/video/h264_decoder/h264bsd_util.h b/src/video/h264_decoder/h264bsd_util.h new file mode 100644 index 000000000..2e5d25dd8 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_util.h @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_UTIL_H +#define H264SWDEC_UTIL_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#ifdef _ASSERT_USED +#include +#endif + +#include "H264SwDecApi.h" + +#if defined(_RANGE_CHECK) || defined(_DEBUG_PRINT) || defined(_ERROR_PRINT) +#include +#endif + +#include +#include "basetype.h" +#include "h264bsd_stream.h" +#include "h264bsd_image.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +#define HANTRO_OK 0 +#define HANTRO_NOK 1 + +#define HANTRO_TRUE (1) +#define HANTRO_FALSE (0) + +#ifndef NULL +#define NULL 0 +#endif + +#define MEMORY_ALLOCATION_ERROR 0xFFFF +#define PARAM_SET_ERROR 0xFFF0 + +/* value to be returned by GetBits if stream buffer is empty */ +#define END_OF_STREAM 0xFFFFFFFFU + +#define EMPTY_RESIDUAL_INDICATOR 0xFFFFFF + +/* macro to mark a residual block empty, i.e. contain zero coefficients */ +#define MARK_RESIDUAL_EMPTY(residual) ((residual)[0] = EMPTY_RESIDUAL_INDICATOR) +/* macro to check if residual block is empty */ +#define IS_RESIDUAL_EMPTY(residual) ((residual)[0] == EMPTY_RESIDUAL_INDICATOR) + +/* macro for assertion, used only if compiler flag _ASSERT_USED is defined */ +#ifdef _ASSERT_USED +#define ASSERT(expr) assert(expr) +#else +#define ASSERT(expr) +#endif + +/* macro for range checking an value, used only if compiler flag _RANGE_CHECK + * is defined */ +#ifdef _RANGE_CHECK +#define RANGE_CHECK(value, minBound, maxBound) \ +{ \ + if ((value) < (minBound) || (value) > (maxBound)) \ + fprintf(stderr, "Warning: Value exceeds given limit(s)!\n"); \ +} +#else +#define RANGE_CHECK(value, minBound, maxBound) +#endif + +/* macro for range checking an array, used only if compiler flag _RANGE_CHECK + * is defined */ +#ifdef _RANGE_CHECK +#define RANGE_CHECK_ARRAY(array, minBound, maxBound, length) \ +{ \ + i32 i; \ + for (i = 0; i < (length); i++) \ + if ((array)[i] < (minBound) || (array)[i] > (maxBound)) \ + fprintf(stderr,"Warning: Value [%d] exceeds given limit(s)!\n",i); \ +} +#else +#define RANGE_CHECK_ARRAY(array, minBound, maxBound, length) +#endif + +/* macro for debug printing, used only if compiler flag _DEBUG_PRINT is + * defined */ +#ifdef _DEBUG_PRINT +#define DEBUG(args) debugf args +#else +#define DEBUG(args) +#endif + +/* macro for error printing, used only if compiler flag _ERROR_PRINT is + * defined */ +#ifdef _ERROR_PRINT +#define EPRINT(msg) fprintf(stderr,"ERROR: %s\n",msg) +#else +#define EPRINT(msg) +#endif + +/* macro to get smaller of two values */ +#define MIN(a, b) (((a) < (b)) ? (a) : (b)) + +/* macro to get greater of two values */ +#define MAX(a, b) (((a) > (b)) ? (a) : (b)) + +/* macro to get absolute value */ +#define ABS(a) (((a) < 0) ? -(a) : (a)) + +/* macro to clip a value z, so that x <= z =< y */ +#define CLIP3(x,y,z) (((z) < (x)) ? (x) : (((z) > (y)) ? (y) : (z))) + +/* macro to clip a value z, so that 0 <= z =< 255 */ +#define CLIP1(z) (((z) < 0) ? 0 : (((z) > 255) ? 255 : (z))) + +/* macro to allocate memory */ +#define ALLOCATE(ptr, count, type) \ +{ \ + (ptr) = H264SwDecMalloc((count) * sizeof(type)); \ +} + +#if H264BSD_N64 +#define ALLOCATE_PIXELS(ptr, count, type) \ +{ \ + (ptr) = malloc_uncached((count) * sizeof(type)); \ + assertf((ptr), "Out of memory"); \ +} +#else +#define ALLOCATE_PIXELS(ptr, count, type) \ +{ \ + (ptr) = H264SwDecMalloc((count) * sizeof(type)); \ +} +#endif + +/* macro to free allocated memory */ +#define FREE(ptr) \ +{ \ + H264SwDecFree((ptr)); (ptr) = NULL; \ +} + +#if H264BSD_N64 +#define FREE_PIXELS(ptr) \ +{ \ + free_uncached((ptr)); (ptr) = NULL; \ +} +#else +#define FREE_PIXELS(ptr) \ +{ \ + H264SwDecFree((ptr)); (ptr) = NULL; \ +} +#endif + +#define ALIGN(ptr, bytePos) \ + (ptr + ( ((bytePos - (uintptr_t)ptr) & (bytePos - 1)) / sizeof(*ptr) )) + +extern const u32 h264bsdQpC[52]; + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ +#ifndef H264DEC_NEON +u32 h264bsdCountLeadingZeros(u32 value, u32 length); +#else +u32 h264bsdCountLeadingZeros(u32 value); +#endif +u32 h264bsdRbspTrailingBits(strmData_t *strmData); + +u32 h264bsdMoreRbspData(strmData_t *strmData); + +u32 h264bsdNextMbAddress(u32 *pSliceGroupMap, u32 picSizeInMbs, u32 currMbAddr); + +void h264bsdSetCurrImageMbPointers(image_t *image, u32 mbNum); + +void h264bsdWriteSliceMbData(image_t *image, u32 firstMbNum, u32 lastMbNum, u32* pData); + +#endif /* #ifdef H264SWDEC_UTIL_H */ + diff --git a/src/video/h264_decoder/h264bsd_vlc.c b/src/video/h264_decoder/h264bsd_vlc.c new file mode 100644 index 000000000..060f35e31 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_vlc.c @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdDecodeExpGolombUnsigned + h264bsdDecodeExpGolombSigned + h264bsdDecodeExpGolombMapped + h264bsdDecodeExpGolombTruncated + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_vlc.h" +#include "basetype.h" +#include "h264bsd_stream.h" +#include "h264bsd_util.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +/* definition of special code num, this along with the return value is used + * to handle code num in the range [0, 2^32] in the DecodeExpGolombUnsigned + * function */ +#define BIG_CODE_NUM 0xFFFFFFFFU + +/* Mapping tables for coded_block_pattern, used for decoding of mapped + * Exp-Golomb codes */ +static const u8 codedBlockPatternIntra4x4[48] = { + 47,31,15,0,23,27,29,30,7,11,13,14,39,43,45,46,16,3,5,10,12,19,21,26,28,35, + 37,42,44,1,2,4,8,17,18,20,24,6,9,22,25,32,33,34,36,40,38,41}; + +static const u8 codedBlockPatternInter[48] = { + 0,16,1,2,4,8,32,3,5,10,12,15,47,7,11,13,14,6,9,31,35,37,42,44,33,34,36,40, + 39,43,45,46,17,18,20,24,19,21,26,28,23,27,29,30,22,25,38,41}; + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + + 5.1 Function: h264bsdDecodeExpGolombUnsigned + + Functional description: + Decode unsigned Exp-Golomb code. This is the same as codeNum used + in other Exp-Golomb code mappings. Code num (i.e. the decoded + symbol) is determined as + + codeNum = 2^leadingZeros - 1 + GetBits(leadingZeros) + + Normal decoded symbols are in the range [0, 2^32 - 2]. Symbol + 2^32-1 is indicated by BIG_CODE_NUM with return value HANTRO_OK + while symbol 2^32 is indicated by BIG_CODE_NUM with return value + HANTRO_NOK. These two symbols are special cases with code length + of 65, i.e. 32 '0' bits, a '1' bit, and either 0 or 1 represented + by 32 bits. + + Symbol 2^32 is out of unsigned 32-bit range but is needed for + DecodeExpGolombSigned to express value -2^31. + + Inputs: + pStrmData pointer to stream data structure + + Outputs: + codeNum decoded code word is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK failure, no valid code word found, note exception + with BIG_CODE_NUM + +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeExpGolombUnsigned(strmData_t *pStrmData, u32 *codeNum) +{ + +/* Variables */ + + u32 bits, numZeros; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(codeNum); + + bits = h264bsdShowBits32(pStrmData); + + /* first bit is 1 -> code length 1 */ + if (bits >= 0x80000000) + { + h264bsdFlushBits(pStrmData, 1); + *codeNum = 0; + return(HANTRO_OK); + } + /* second bit is 1 -> code length 3 */ + else if (bits >= 0x40000000) + { + if (h264bsdFlushBits(pStrmData, 3) == END_OF_STREAM) + return(HANTRO_NOK); + *codeNum = 1 + ((bits >> 29) & 0x1); + return(HANTRO_OK); + } + /* third bit is 1 -> code length 5 */ + else if (bits >= 0x20000000) + { + if (h264bsdFlushBits(pStrmData, 5) == END_OF_STREAM) + return(HANTRO_NOK); + *codeNum = 3 + ((bits >> 27) & 0x3); + return(HANTRO_OK); + } + /* fourth bit is 1 -> code length 7 */ + else if (bits >= 0x10000000) + { + if (h264bsdFlushBits(pStrmData, 7) == END_OF_STREAM) + return(HANTRO_NOK); + *codeNum = 7 + ((bits >> 25) & 0x7); + return(HANTRO_OK); + } + /* other code lengths */ + else + { +#ifndef H264DEC_NEON + numZeros = 4 + h264bsdCountLeadingZeros(bits, 28); +#else + numZeros = h264bsdCountLeadingZeros(bits); +#endif + /* all 32 bits are zero */ + if (numZeros == 32) + { + *codeNum = 0; + h264bsdFlushBits(pStrmData,32); + bits = h264bsdGetBits(pStrmData, 1); + /* check 33rd bit, must be 1 */ + if (bits == 1) + { + /* cannot use h264bsdGetBits, limited to 31 bits */ + bits = h264bsdShowBits32(pStrmData); + if (h264bsdFlushBits(pStrmData, 32) == END_OF_STREAM) + return(HANTRO_NOK); + /* code num 2^32 - 1, needed for unsigned mapping */ + if (bits == 0) + { + *codeNum = BIG_CODE_NUM; + return(HANTRO_OK); + } + /* code num 2^32, needed for unsigned mapping + * (results in -2^31) */ + else if (bits == 1) + { + *codeNum = BIG_CODE_NUM; + return(HANTRO_NOK); + } + } + /* if more zeros than 32, it is an error */ + return(HANTRO_NOK); + } + else + h264bsdFlushBits(pStrmData,numZeros+1); + + bits = h264bsdGetBits(pStrmData, numZeros); + if (bits == END_OF_STREAM) + return(HANTRO_NOK); + + *codeNum = (1 << numZeros) - 1 + bits; + + } + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + 5.2 Function: h264bsdDecodeExpGolombSigned + + Functional description: + Decode signed Exp-Golomb code. Code num is determined by + h264bsdDecodeExpGolombUnsigned and then mapped to signed + representation as + + symbol = (-1)^(codeNum+1) * (codeNum+1)/2 + + Signed symbols shall be in the range [-2^31, 2^31 - 1]. Symbol + -2^31 is obtained when codeNum is 2^32, which cannot be expressed + by unsigned 32-bit value. This is signaled as a special case from + the h264bsdDecodeExpGolombUnsigned by setting codeNum to + BIG_CODE_NUM and returning HANTRO_NOK status. + + Inputs: + pStrmData pointer to stream data structure + + Outputs: + value decoded code word is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK failure, no valid code word found + +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeExpGolombSigned(strmData_t *pStrmData, i32 *value) +{ + +/* Variables */ + + u32 status, codeNum = 0; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(value); + + status = h264bsdDecodeExpGolombUnsigned(pStrmData, &codeNum); + + if (codeNum == BIG_CODE_NUM) + { + /* BIG_CODE_NUM and HANTRO_OK status means codeNum 2^32-1 which would + * result in signed integer valued 2^31 (i.e. out of 32-bit signed + * integer range) */ + if (status == HANTRO_OK) + return(HANTRO_NOK); + /* BIG_CODE_NUM and HANTRO_NOK status means codeNum 2^32 which results + * in signed integer valued -2^31 */ + else + { + *value = (i32)(2147483648U); + return (HANTRO_OK); + } + } + else if (status == HANTRO_OK) + { + /* (-1)^(codeNum+1) results in positive sign if codeNum is odd, + * negative when it is even. (codeNum+1)/2 is obtained as + * (codeNum+1)>>1 when value is positive and as (-codeNum)>>1 for + * negative value */ + /*lint -e702 */ + *value = (codeNum & 0x1) ? (i32)((codeNum + 1) >> 1) : + -(i32)((codeNum + 1) >> 1); + /*lint +e702 */ + return(HANTRO_OK); + } + + return(HANTRO_NOK); + +} + +/*------------------------------------------------------------------------------ + + 5.3 Function: h264bsdDecodeExpGolombMapped + + Functional description: + Decode mapped Exp-Golomb code. Code num is determined by + h264bsdDecodeExpGolombUnsigned and then mapped to codedBlockPattern + either for intra or inter macroblock. The mapping is implemented by + look-up tables defined in the beginning of the file. + + Inputs: + pStrmData pointer to stream data structure + isIntra flag to indicate if intra or inter mapping is to + be used + + Outputs: + value decoded code word is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK failure, no valid code word found + +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeExpGolombMapped(strmData_t *pStrmData, u32 *value, + u32 isIntra) +{ + +/* Variables */ + + u32 status, codeNum; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(value); + + status = h264bsdDecodeExpGolombUnsigned(pStrmData, &codeNum); + + if (status != HANTRO_OK) + return (HANTRO_NOK); + else + { + /* range of valid codeNums [0,47] */ + if (codeNum > 47) + return (HANTRO_NOK); + if (isIntra) + *value = codedBlockPatternIntra4x4[codeNum]; + else + *value = codedBlockPatternInter[codeNum]; + return(HANTRO_OK); + } + +} + +/*------------------------------------------------------------------------------ + + 5.4 Function: h264bsdDecodeExpGolombTruncated + + Functional description: + Decode truncated Exp-Golomb code. greaterThanOne flag indicates + the range of the symbol to be decoded as follows: + FALSE -> [0,1] + TRUE -> [0,2^32-1] + + If flag is false the decoding is performed by reading one bit + from the stream with h264bsdGetBits and mapping this to decoded + symbol as + symbol = bit ? 0 : 1 + + Otherwise, i.e. when flag is TRUE, code num is determined by + h264bsdDecodeExpGolombUnsigned and this is used as the decoded + symbol. + + Inputs: + pStrmData pointer to stream data structure + greaterThanOne flag to indicate if range is wider than [0,1] + + Outputs: + value decoded code word is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK failure, no valid code word found + +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeExpGolombTruncated( + strmData_t *pStrmData, + u32 *value, + u32 greaterThanOne) +{ + +/* Variables */ + +/* Code */ + + ASSERT(pStrmData); + ASSERT(value); + + if (greaterThanOne) + { + return(h264bsdDecodeExpGolombUnsigned(pStrmData, value)); + } + else + { + *value = h264bsdGetBits(pStrmData,1); + if (*value == END_OF_STREAM) + return (HANTRO_NOK); + *value ^= 0x1; + } + + return (HANTRO_OK); + +} + diff --git a/src/video/h264_decoder/h264bsd_vlc.h b/src/video/h264_decoder/h264bsd_vlc.h new file mode 100644 index 000000000..4c16773ca --- /dev/null +++ b/src/video/h264_decoder/h264bsd_vlc.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_VLC_H +#define H264SWDEC_VLC_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_stream.h" +#include "h264bsd_transform.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeExpGolombUnsigned(strmData_t *pStrmData, u32 *value); + +u32 h264bsdDecodeExpGolombSigned(strmData_t *pStrmData, i32 *value); + +u32 h264bsdDecodeExpGolombMapped(strmData_t *pStrmData, u32 *value, + u32 isIntra); + +u32 h264bsdDecodeExpGolombTruncated(strmData_t *pStrmData, u32 *value, + u32 greaterThanOne); + +#endif /* #ifdef H264SWDEC_VLC_H */ + diff --git a/src/video/h264_decoder/h264bsd_vui.c b/src/video/h264_decoder/h264bsd_vui.c new file mode 100644 index 000000000..4a9335ad7 --- /dev/null +++ b/src/video/h264_decoder/h264bsd_vui.c @@ -0,0 +1,490 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. External compiler flags + 3. Module defines + 4. Local function prototypes + 5. Functions + h264bsdDecodeVuiParameters + DecodeHrdParameters + +------------------------------------------------------------------------------*/ + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "h264bsd_vui.h" +#include "basetype.h" +#include "h264bsd_vlc.h" +#include "h264bsd_stream.h" +#include "h264bsd_util.h" + +/*------------------------------------------------------------------------------ + 2. External compiler flags +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + 3. Module defines +------------------------------------------------------------------------------*/ + +#define MAX_DPB_SIZE 16 +#define MAX_BR 240000 /* for level 5.1 */ +#define MAX_CPB 240000 /* for level 5.1 */ + +/*------------------------------------------------------------------------------ + 4. Local function prototypes +------------------------------------------------------------------------------*/ + +static u32 DecodeHrdParameters( + strmData_t *pStrmData, + hrdParameters_t *pHrdParameters); + +/*------------------------------------------------------------------------------ + + Function: h264bsdDecodeVuiParameters + + Functional description: + Decode VUI parameters from the stream. See standard for details. + + Inputs: + pStrmData pointer to stream data structure + + Outputs: + pVuiParameters decoded information is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK invalid stream data or end of stream + +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeVuiParameters(strmData_t *pStrmData, + vuiParameters_t *pVuiParameters) +{ + +/* Variables */ + + u32 tmp; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(pVuiParameters); + + H264SwDecMemset(pVuiParameters, 0, sizeof(vuiParameters_t)); + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->aspectRatioPresentFlag = (tmp == 1) ? + HANTRO_TRUE : HANTRO_FALSE; + + if (pVuiParameters->aspectRatioPresentFlag) + { + tmp = h264bsdGetBits(pStrmData, 8); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->aspectRatioIdc = tmp; + + if (pVuiParameters->aspectRatioIdc == ASPECT_RATIO_EXTENDED_SAR) + { + tmp = h264bsdGetBits(pStrmData, 16); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->sarWidth = tmp; + + tmp = h264bsdGetBits(pStrmData, 16); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->sarHeight = tmp; + } + } + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->overscanInfoPresentFlag = (tmp == 1) ? + HANTRO_TRUE : HANTRO_FALSE; + + if (pVuiParameters->overscanInfoPresentFlag) + { + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->overscanAppropriateFlag = (tmp == 1) ? + HANTRO_TRUE : HANTRO_FALSE; + } + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->videoSignalTypePresentFlag = (tmp == 1) ? + HANTRO_TRUE : HANTRO_FALSE; + + if (pVuiParameters->videoSignalTypePresentFlag) + { + tmp = h264bsdGetBits(pStrmData, 3); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->videoFormat = tmp; + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->videoFullRangeFlag = (tmp == 1) ? + HANTRO_TRUE : HANTRO_FALSE; + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->colourDescriptionPresentFlag = + (tmp == 1) ? HANTRO_TRUE : HANTRO_FALSE; + + if (pVuiParameters->colourDescriptionPresentFlag) + { + tmp = h264bsdGetBits(pStrmData, 8); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->colourPrimaries = tmp; + + tmp = h264bsdGetBits(pStrmData, 8); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->transferCharacteristics = tmp; + + tmp = h264bsdGetBits(pStrmData, 8); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->matrixCoefficients = tmp; + } + else + { + pVuiParameters->colourPrimaries = 2; + pVuiParameters->transferCharacteristics = 2; + pVuiParameters->matrixCoefficients = 2; + } + } + else + { + pVuiParameters->videoFormat = 5; + pVuiParameters->colourPrimaries = 2; + pVuiParameters->transferCharacteristics = 2; + pVuiParameters->matrixCoefficients = 2; + } + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->chromaLocInfoPresentFlag = + (tmp == 1) ? HANTRO_TRUE : HANTRO_FALSE; + + if (pVuiParameters->chromaLocInfoPresentFlag) + { + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pVuiParameters->chromaSampleLocTypeTopField); + if (tmp != HANTRO_OK) + return(tmp); + if (pVuiParameters->chromaSampleLocTypeTopField > 5) + return(HANTRO_NOK); + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pVuiParameters->chromaSampleLocTypeBottomField); + if (tmp != HANTRO_OK) + return(tmp); + if (pVuiParameters->chromaSampleLocTypeBottomField > 5) + return(HANTRO_NOK); + } + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->timingInfoPresentFlag = + (tmp == 1) ? HANTRO_TRUE : HANTRO_FALSE; + + if (pVuiParameters->timingInfoPresentFlag) + { + tmp = h264bsdShowBits32(pStrmData); + if (h264bsdFlushBits(pStrmData, 32) == END_OF_STREAM) + return(HANTRO_NOK); + if (tmp == 0) + return(HANTRO_NOK); + pVuiParameters->numUnitsInTick = tmp; + + tmp = h264bsdShowBits32(pStrmData); + if (h264bsdFlushBits(pStrmData, 32) == END_OF_STREAM) + return(HANTRO_NOK); + if (tmp == 0) + return(HANTRO_NOK); + pVuiParameters->timeScale = tmp; + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->fixedFrameRateFlag = + (tmp == 1) ? HANTRO_TRUE : HANTRO_FALSE; + } + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->nalHrdParametersPresentFlag = + (tmp == 1) ? HANTRO_TRUE : HANTRO_FALSE; + + if (pVuiParameters->nalHrdParametersPresentFlag) + { + tmp = DecodeHrdParameters(pStrmData, &pVuiParameters->nalHrdParameters); + if (tmp != HANTRO_OK) + return(tmp); + } + else + { + pVuiParameters->nalHrdParameters.cpbCnt = 1; + /* MaxBR and MaxCPB should be the values correspondig to the levelIdc + * in the SPS containing these VUI parameters. However, these values + * are not used anywhere and maximum for any level will be used here */ + pVuiParameters->nalHrdParameters.bitRateValue[0] = 1200 * MAX_BR + 1; + pVuiParameters->nalHrdParameters.cpbSizeValue[0] = 1200 * MAX_CPB + 1; + pVuiParameters->nalHrdParameters.initialCpbRemovalDelayLength = 24; + pVuiParameters->nalHrdParameters.cpbRemovalDelayLength = 24; + pVuiParameters->nalHrdParameters.dpbOutputDelayLength = 24; + pVuiParameters->nalHrdParameters.timeOffsetLength = 24; + } + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->vclHrdParametersPresentFlag = + (tmp == 1) ? HANTRO_TRUE : HANTRO_FALSE; + + if (pVuiParameters->vclHrdParametersPresentFlag) + { + tmp = DecodeHrdParameters(pStrmData, &pVuiParameters->vclHrdParameters); + if (tmp != HANTRO_OK) + return(tmp); + } + else + { + pVuiParameters->vclHrdParameters.cpbCnt = 1; + /* MaxBR and MaxCPB should be the values correspondig to the levelIdc + * in the SPS containing these VUI parameters. However, these values + * are not used anywhere and maximum for any level will be used here */ + pVuiParameters->vclHrdParameters.bitRateValue[0] = 1000 * MAX_BR + 1; + pVuiParameters->vclHrdParameters.cpbSizeValue[0] = 1000 * MAX_CPB + 1; + pVuiParameters->vclHrdParameters.initialCpbRemovalDelayLength = 24; + pVuiParameters->vclHrdParameters.cpbRemovalDelayLength = 24; + pVuiParameters->vclHrdParameters.dpbOutputDelayLength = 24; + pVuiParameters->vclHrdParameters.timeOffsetLength = 24; + } + + if (pVuiParameters->nalHrdParametersPresentFlag || + pVuiParameters->vclHrdParametersPresentFlag) + { + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->lowDelayHrdFlag = + (tmp == 1) ? HANTRO_TRUE : HANTRO_FALSE; + } + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->picStructPresentFlag = + (tmp == 1) ? HANTRO_TRUE : HANTRO_FALSE; + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->bitstreamRestrictionFlag = + (tmp == 1) ? HANTRO_TRUE : HANTRO_FALSE; + + if (pVuiParameters->bitstreamRestrictionFlag) + { + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pVuiParameters->motionVectorsOverPicBoundariesFlag = + (tmp == 1) ? HANTRO_TRUE : HANTRO_FALSE; + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pVuiParameters->maxBytesPerPicDenom); + if (tmp != HANTRO_OK) + return(tmp); + if (pVuiParameters->maxBytesPerPicDenom > 16) + return(HANTRO_NOK); + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pVuiParameters->maxBitsPerMbDenom); + if (tmp != HANTRO_OK) + return(tmp); + if (pVuiParameters->maxBitsPerMbDenom > 16) + return(HANTRO_NOK); + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pVuiParameters->log2MaxMvLengthHorizontal); + if (tmp != HANTRO_OK) + return(tmp); + if (pVuiParameters->log2MaxMvLengthHorizontal > 16) + return(HANTRO_NOK); + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pVuiParameters->log2MaxMvLengthVertical); + if (tmp != HANTRO_OK) + return(tmp); + if (pVuiParameters->log2MaxMvLengthVertical > 16) + return(HANTRO_NOK); + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pVuiParameters->numReorderFrames); + if (tmp != HANTRO_OK) + return(tmp); + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pVuiParameters->maxDecFrameBuffering); + if (tmp != HANTRO_OK) + return(tmp); + } + else + { + pVuiParameters->motionVectorsOverPicBoundariesFlag = HANTRO_TRUE; + pVuiParameters->maxBytesPerPicDenom = 2; + pVuiParameters->maxBitsPerMbDenom = 1; + pVuiParameters->log2MaxMvLengthHorizontal = 16; + pVuiParameters->log2MaxMvLengthVertical = 16; + pVuiParameters->numReorderFrames = MAX_DPB_SIZE; + pVuiParameters->maxDecFrameBuffering = MAX_DPB_SIZE; + } + + return(HANTRO_OK); + +} + +/*------------------------------------------------------------------------------ + + Function: DecodeHrdParameters + + Functional description: + Decode HRD parameters from the stream. See standard for details. + + Inputs: + pStrmData pointer to stream data structure + + Outputs: + pHrdParameters decoded information is stored here + + Returns: + HANTRO_OK success + HANTRO_NOK invalid stream data + +------------------------------------------------------------------------------*/ + +static u32 DecodeHrdParameters( + strmData_t *pStrmData, + hrdParameters_t *pHrdParameters) +{ + +/* Variables */ + + u32 tmp, i; + +/* Code */ + + ASSERT(pStrmData); + ASSERT(pHrdParameters); + + + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, &pHrdParameters->cpbCnt); + if (tmp != HANTRO_OK) + return(tmp); + /* cpbCount = cpb_cnt_minus1 + 1 */ + pHrdParameters->cpbCnt++; + if (pHrdParameters->cpbCnt > MAX_CPB_CNT) + return(HANTRO_NOK); + + tmp = h264bsdGetBits(pStrmData, 4); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pHrdParameters->bitRateScale = tmp; + + tmp = h264bsdGetBits(pStrmData, 4); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pHrdParameters->cpbSizeScale = tmp; + + for (i = 0; i < pHrdParameters->cpbCnt; i++) + { + /* bit_rate_value_minus1 in the range [0, 2^32 - 2] */ + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pHrdParameters->bitRateValue[i]); + if (tmp != HANTRO_OK) + return(tmp); + if (pHrdParameters->bitRateValue[i] > 4294967294U) + return(HANTRO_NOK); + pHrdParameters->bitRateValue[i]++; + /* this may result in overflow, but this value is not used for + * anything */ + pHrdParameters->bitRateValue[i] *= + 1 << (6 + pHrdParameters->bitRateScale); + + /* cpb_size_value_minus1 in the range [0, 2^32 - 2] */ + tmp = h264bsdDecodeExpGolombUnsigned(pStrmData, + &pHrdParameters->cpbSizeValue[i]); + if (tmp != HANTRO_OK) + return(tmp); + if (pHrdParameters->cpbSizeValue[i] > 4294967294U) + return(HANTRO_NOK); + pHrdParameters->cpbSizeValue[i]++; + /* this may result in overflow, but this value is not used for + * anything */ + pHrdParameters->cpbSizeValue[i] *= + 1 << (4 + pHrdParameters->cpbSizeScale); + + tmp = h264bsdGetBits(pStrmData, 1); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pHrdParameters->cbrFlag[i] = (tmp == 1) ? HANTRO_TRUE : HANTRO_FALSE; + } + + tmp = h264bsdGetBits(pStrmData, 5); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pHrdParameters->initialCpbRemovalDelayLength = tmp + 1; + + tmp = h264bsdGetBits(pStrmData, 5); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pHrdParameters->cpbRemovalDelayLength = tmp + 1; + + tmp = h264bsdGetBits(pStrmData, 5); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pHrdParameters->dpbOutputDelayLength = tmp + 1; + + tmp = h264bsdGetBits(pStrmData, 5); + if (tmp == END_OF_STREAM) + return(HANTRO_NOK); + pHrdParameters->timeOffsetLength = tmp; + + return(HANTRO_OK); + +} + diff --git a/src/video/h264_decoder/h264bsd_vui.h b/src/video/h264_decoder/h264bsd_vui.h new file mode 100644 index 000000000..05d52a45a --- /dev/null +++ b/src/video/h264_decoder/h264bsd_vui.h @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*------------------------------------------------------------------------------ + + Table of contents + + 1. Include headers + 2. Module defines + 3. Data types + 4. Function prototypes + +------------------------------------------------------------------------------*/ + +#ifndef H264SWDEC_VUI_H +#define H264SWDEC_VUI_H + +/*------------------------------------------------------------------------------ + 1. Include headers +------------------------------------------------------------------------------*/ + +#include "basetype.h" +#include "h264bsd_stream.h" + +/*------------------------------------------------------------------------------ + 2. Module defines +------------------------------------------------------------------------------*/ + +#define MAX_CPB_CNT 32 + +/*------------------------------------------------------------------------------ + 3. Data types +------------------------------------------------------------------------------*/ + +/* enumerated sample aspect ratios, ASPECT_RATIO_M_N means M:N */ +enum +{ + ASPECT_RATIO_UNSPECIFIED = 0, + ASPECT_RATIO_1_1, + ASPECT_RATIO_12_11, + ASPECT_RATIO_10_11, + ASPECT_RATIO_16_11, + ASPECT_RATIO_40_33, + ASPECT_RATIO_24_11, + ASPECT_RATIO_20_11, + ASPECT_RATIO_32_11, + ASPECT_RATIO_80_33, + ASPECT_RATIO_18_11, + ASPECT_RATIO_15_11, + ASPECT_RATIO_64_33, + ASPECT_RATIO_160_99, + ASPECT_RATIO_EXTENDED_SAR = 255 +}; + +/* structure to store Hypothetical Reference Decoder (HRD) parameters */ +typedef struct +{ + u32 cpbCnt; + u32 bitRateScale; + u32 cpbSizeScale; + u32 bitRateValue[MAX_CPB_CNT]; + u32 cpbSizeValue[MAX_CPB_CNT]; + u32 cbrFlag[MAX_CPB_CNT]; + u32 initialCpbRemovalDelayLength; + u32 cpbRemovalDelayLength; + u32 dpbOutputDelayLength; + u32 timeOffsetLength; +} hrdParameters_t; + +/* storage for VUI parameters */ +typedef struct +{ + u32 aspectRatioPresentFlag; + u32 aspectRatioIdc; + u32 sarWidth; + u32 sarHeight; + u32 overscanInfoPresentFlag; + u32 overscanAppropriateFlag; + u32 videoSignalTypePresentFlag; + u32 videoFormat; + u32 videoFullRangeFlag; + u32 colourDescriptionPresentFlag; + u32 colourPrimaries; + u32 transferCharacteristics; + u32 matrixCoefficients; + u32 chromaLocInfoPresentFlag; + u32 chromaSampleLocTypeTopField; + u32 chromaSampleLocTypeBottomField; + u32 timingInfoPresentFlag; + u32 numUnitsInTick; + u32 timeScale; + u32 fixedFrameRateFlag; + u32 nalHrdParametersPresentFlag; + hrdParameters_t nalHrdParameters; + u32 vclHrdParametersPresentFlag; + hrdParameters_t vclHrdParameters; + u32 lowDelayHrdFlag; + u32 picStructPresentFlag; + u32 bitstreamRestrictionFlag; + u32 motionVectorsOverPicBoundariesFlag; + u32 maxBytesPerPicDenom; + u32 maxBitsPerMbDenom; + u32 log2MaxMvLengthHorizontal; + u32 log2MaxMvLengthVertical; + u32 numReorderFrames; + u32 maxDecFrameBuffering; +} vuiParameters_t; + +/*------------------------------------------------------------------------------ + 4. Function prototypes +------------------------------------------------------------------------------*/ + +u32 h264bsdDecodeVuiParameters(strmData_t *pStrmData, + vuiParameters_t *pVuiParameters); + +#endif /* #ifdef H264SWDEC_VUI_H */ + diff --git a/src/video/h264_decoder/omxdl/armCOMM.c b/src/video/h264_decoder/omxdl/armCOMM.c new file mode 100644 index 000000000..e572a8967 --- /dev/null +++ b/src/video/h264_decoder/omxdl/armCOMM.c @@ -0,0 +1,936 @@ +/** + * + * File Name: armCOMM.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * Defines Common APIs used across OpenMAX API's + */ + +#include "omxtypes.h" +#include "armCOMM.h" + +/***********************************************************************/ + /* Miscellaneous Arithmetic operations */ + +/** + * Function: armRoundFloatToS16 + * + * Description: + * Converts a double precision value into a short int after rounding + * + * Parameters: + * [in] Value Float value to be converted + * + * Return Value: + * [out] converted value in OMX_S16 format + * + */ + +OMX_S16 armRoundFloatToS16 (OMX_F64 Value) +{ + if (Value > 0) + { + return (OMX_S16)(Value + .5); + } + else + { + return (OMX_S16)(Value - .5); + } +} + +/** + * Function: armRoundFloatToS32 + * + * Description: + * Converts a double precision value into a int after rounding + * + * Parameters: + * [in] Value Float value to be converted + * + * Return Value: + * [out] converted value in OMX_S32 format + * + */ + +OMX_S32 armRoundFloatToS32 (OMX_F64 Value) +{ + if (Value > 0) + { + return (OMX_S32)(Value + .5); + } + else + { + return (OMX_S32)(Value - .5); + } +} +/** + * Function: armSatRoundFloatToS16 + * + * Description: + * Converts a double precision value into a short int after rounding and saturation + * + * Parameters: + * [in] Value Float value to be converted + * + * Return Value: + * [out] converted value in OMX_S16 format + * + */ + +OMX_S16 armSatRoundFloatToS16 (OMX_F64 Value) +{ + if (Value > 0) + { + Value += 0.5; + + if(Value > (OMX_S16)OMX_MAX_S16 ) + { + return (OMX_S16)OMX_MAX_S16; + } + else + { + return (OMX_S16)Value; + } + } + else + { + Value -= 0.5; + + if(Value < (OMX_S16)OMX_MIN_S16 ) + { + return (OMX_S16)OMX_MIN_S16; + } + else + { + return (OMX_S16)Value; + } + } +} + +/** + * Function: armSatRoundFloatToS32 + * + * Description: + * Converts a double precision value into a int after rounding and saturation + * + * Parameters: + * [in] Value Float value to be converted + * + * Return Value: + * [out] converted value in OMX_S32 format + * + */ + +OMX_S32 armSatRoundFloatToS32 (OMX_F64 Value) +{ + if (Value > 0) + { + Value += 0.5; + + if(Value > (OMX_S32)OMX_MAX_S32 ) + { + return (OMX_S32)OMX_MAX_S32; + } + else + { + return (OMX_S32)Value; + } + } + else + { + Value -= 0.5; + + if(Value < (OMX_S32)OMX_MIN_S32 ) + { + return (OMX_S32)OMX_MIN_S32; + } + else + { + return (OMX_S32)Value; + } + } +} + +/** + * Function: armSatRoundFloatToU16 + * + * Description: + * Converts a double precision value into a unsigned short int after rounding and saturation + * + * Parameters: + * [in] Value Float value to be converted + * + * Return Value: + * [out] converted value in OMX_U16 format + * + */ + +OMX_U16 armSatRoundFloatToU16 (OMX_F64 Value) +{ + Value += 0.5; + + if(Value > (OMX_U16)OMX_MAX_U16 ) + { + return (OMX_U16)OMX_MAX_U16; + } + else + { + return (OMX_U16)Value; + } +} + +/** + * Function: armSatRoundFloatToU32 + * + * Description: + * Converts a double precision value into a unsigned int after rounding and saturation + * + * Parameters: + * [in] Value Float value to be converted + * + * Return Value: + * [out] converted value in OMX_U32 format + * + */ + +OMX_U32 armSatRoundFloatToU32 (OMX_F64 Value) +{ + Value += 0.5; + + if(Value > (OMX_U32)OMX_MAX_U32 ) + { + return (OMX_U32)OMX_MAX_U32; + } + else + { + return (OMX_U32)Value; + } +} + +/** + * Function: armRoundFloatToS64 + * + * Description: + * Converts a double precision value into a 64 bit int after rounding + * + * Parameters: + * [in] Value Float value to be converted + * + * Return Value: + * [out] converted value in OMX_S64 format + * + */ + +OMX_S64 armRoundFloatToS64 (OMX_F64 Value) +{ + if (Value > 0) + { + return (OMX_S64)(Value + .5); + } + else + { + return (OMX_S64)(Value - .5); + } +} + +/** + * Function: armSignCheck + * + * Description: + * Checks the sign of a variable: + * returns 1 if it is Positive + * returns 0 if it is 0 + * returns -1 if it is Negative + * + * Remarks: + * + * Parameters: + * [in] var Variable to be checked + * + * Return Value: + * OMX_INT -- returns 1 if it is Positive + * returns 0 if it is 0 + * returns -1 if it is Negative + */ + +OMX_INT armSignCheck ( + OMX_S16 var +) + +{ + OMX_INT Sign; + + if (var < 0) + { + Sign = -1; + } + else if ( var > 0) + { + Sign = 1; + } + else + { + Sign = 0; + } + + return Sign; +} + +/** + * Function: armClip + * + * Description: Clips the input between MAX and MIN value + * + * + * Remarks: + * + * Parameters: + * [in] Min lower bound + * [in] Max upper bound + * [in] src variable to the clipped + * + * Return Value: + * OMX_S32 -- returns clipped value + */ + +OMX_S32 armClip ( + OMX_INT min, + OMX_INT max, + OMX_S32 src +) + +{ + if (src > max) + { + src = max; + } + else if (src < min) + { + src = min; + } + + return src; +} + +/** + * Function: armClip_F32 + * + * Description: Clips the input between MAX and MIN value + * + * + * Remarks: + * + * Parameters: + * [in] Min lower bound + * [in] Max upper bound + * [in] src variable to the clipped + * + * Return Value: + * OMX_F32 -- returns clipped value + */ + +OMX_F32 armClip_F32 ( + OMX_F32 min, + OMX_F32 max, + OMX_F32 src +) + +{ + if (src > max) + { + src = max; + } + else if (src < min) + { + src = min; + } + + return src; +} + +/** + * Function: armShiftSat_F32 + * + * Description: Divides a float value by 2^shift and + * saturates it for unsigned value range for satBits. + * Second parameter is like "shifting" the corresponding + * integer value. Takes care of rounding while clipping the final + * value. + * + * Parameters: + * [in] v Number to be operated upon + * [in] shift Divides the input "v" by "2^shift" + * [in] satBits Final range is [0, 2^satBits) + * + * Return Value: + * OMX_S32 -- returns "shifted" saturated value + */ + +OMX_U32 armShiftSat_F32(OMX_F32 v, OMX_INT shift, OMX_INT satBits) +{ + OMX_U32 allOnes = (OMX_U32)(-1); + OMX_U32 maxV = allOnes >> (32-satBits); + OMX_F32 vShifted, vRounded, shiftDiv = (OMX_F32)(1 << shift); + OMX_U32 vInt; + OMX_U32 vIntSat; + + if(v <= 0) + return 0; + + vShifted = v / shiftDiv; + vRounded = (OMX_F32)(vShifted + 0.5); + vInt = (OMX_U32)vRounded; + vIntSat = vInt; + if(vIntSat > maxV) + vIntSat = maxV; + return vIntSat; +} + +/** + * Functions: armSwapElem + * + * Description: + * These function swaps two elements at the specified pointer locations. + * The size of each element could be anything as specified by + * + * Return Value: + * OMXResult -- Error status from the function + */ +OMXResult armSwapElem( + OMX_U8 *pBuf1, + OMX_U8 *pBuf2, + OMX_INT elemSize + ) +{ + OMX_INT i; + OMX_U8 temp; + armRetArgErrIf(!pBuf1 || !pBuf2, OMX_Sts_BadArgErr); + + for(i = 0; i < elemSize; i++) + { + temp = *(pBuf1 + i); + *(pBuf1 + i) = *(pBuf2 + i); + *(pBuf2 + i) = temp; + } + return OMX_Sts_NoErr; +} + +/** + * Function: armMedianOf3 + * + * Description: Finds the median of three numbers + * + * Remarks: + * + * Parameters: + * [in] fEntry First entry + * [in] sEntry second entry + * [in] tEntry Third entry + * + * Return Value: + * OMX_S32 -- returns the median value + */ + +OMX_S32 armMedianOf3 ( + OMX_S32 fEntry, + OMX_S32 sEntry, + OMX_S32 tEntry +) +{ + OMX_S32 a, b, c; + + a = armMin (fEntry, sEntry); + b = armMax (fEntry, sEntry); + c = armMin (b, tEntry); + return (armMax (a, c)); +} + +/** + * Function: armLogSize + * + * Description: Finds the size of a positive value and returns the same + * + * Remarks: + * + * Parameters: + * [in] value Positive value + * + * Return Value: + * OMX_U8 -- Returns the minimum number of bits required to represent the positive value. + This is the smallest k>=0 such that that value is less than (1< 0; value = value >> 1) + { + i++; + } + return i; +} + +/***********************************************************************/ + /* Saturating Arithmetic operations */ + +/** + * Function :armSatAdd_S32() + * + * Description : + * Returns the result of saturated addition of the two inputs Value1, Value2 + * + * Parametrs: + * [in] Value1 First Operand + * [in] Value2 Second Operand + * + * Return: + * [out] Result of operation + * + * + **/ + +OMX_S32 armSatAdd_S32(OMX_S32 Value1,OMX_S32 Value2) +{ + OMX_S32 Result; + + Result = Value1 + Value2; + + if( (Value1^Value2) >= 0) + { + /*Same sign*/ + if( (Result^Value1) >= 0) + { + /*Result has not saturated*/ + return Result; + } + else + { + if(Value1 >= 0) + { + /*Result has saturated in positive side*/ + return OMX_MAX_S32; + } + else + { + /*Result has saturated in negative side*/ + return OMX_MIN_S32; + } + + } + + } + else + { + return Result; + } + +} + +/** + * Function :armSatAdd_S64() + * + * Description : + * Returns the result of saturated addition of the two inputs Value1, Value2 + * + * Parametrs: + * [in] Value1 First Operand + * [in] Value2 Second Operand + * + * Return: + * [out] Result of operation + * + * + **/ + +OMX_S64 armSatAdd_S64(OMX_S64 Value1,OMX_S64 Value2) +{ + OMX_S64 Result; + + Result = Value1 + Value2; + + if( (Value1^Value2) >= 0) + { + /*Same sign*/ + if( (Result^Value1) >= 0) + { + /*Result has not saturated*/ + return Result; + } + else + { + if(Value1 >= 0) + { + /*Result has saturated in positive side*/ + Result = OMX_MAX_S64; + return Result; + } + else + { + /*Result has saturated in negative side*/ + return OMX_MIN_S64; + } + + } + + } + else + { + return Result; + } + +} + +/** Function :armSatSub_S32() + * + * Description : + * Returns the result of saturated substraction of the two inputs Value1, Value2 + * + * Parametrs: + * [in] Value1 First Operand + * [in] Value2 Second Operand + * + * Return: + * [out] Result of operation + * + **/ + +OMX_S32 armSatSub_S32(OMX_S32 Value1,OMX_S32 Value2) +{ + OMX_S32 Result; + + Result = Value1 - Value2; + + if( (Value1^Value2) < 0) + { + /*Opposite sign*/ + if( (Result^Value1) >= 0) + { + /*Result has not saturated*/ + return Result; + } + else + { + if(Value1 >= 0) + { + /*Result has saturated in positive side*/ + return OMX_MAX_S32; + } + else + { + /*Result has saturated in negative side*/ + return OMX_MIN_S32; + } + + } + + } + else + { + return Result; + } + +} + +/** + * Function :armSatMac_S32() + * + * Description : + * Returns the result of Multiplication of Value1 and Value2 and subesquent saturated + * accumulation with Mac + * + * Parametrs: + * [in] Value1 First Operand + * [in] Value2 Second Operand + * [in] Mac Accumulator + * + * Return: + * [out] Result of operation + **/ + +OMX_S32 armSatMac_S32(OMX_S32 Mac,OMX_S16 Value1,OMX_S16 Value2) +{ + OMX_S32 Result; + + Result = (OMX_S32)(Value1*Value2); + Result = armSatAdd_S32( Mac , Result ); + + return Result; +} + +/** + * Function :armSatMac_S16S32_S32 + * + * Description : + * Returns the result of saturated MAC operation of the three inputs delayElem, filTap , mac + * + * mac = mac + Saturate_in_32Bits(delayElem * filTap) + * + * Parametrs: + * [in] delayElem First 32 bit Operand + * [in] filTap Second 16 bit Operand + * [in] mac Result of MAC operation + * + * Return: + * [out] mac Result of operation + * + **/ + +OMX_S32 armSatMac_S16S32_S32(OMX_S32 mac, OMX_S32 delayElem, OMX_S16 filTap ) +{ + + OMX_S32 result; + + result = armSatMulS16S32_S32(filTap,delayElem); + + if ( result > OMX_MAX_S16 ) + { + result = OMX_MAX_S32; + } + else if( result < OMX_MIN_S16 ) + { + result = OMX_MIN_S32; + } + else + { + result = delayElem * filTap; + } + + mac = armSatAdd_S32(mac,result); + + return mac; +} + + +/** + * Function :armSatRoundRightShift_S32_S16 + * + * Description : + * Returns the result of rounded right shift operation of input by the scalefactor + * + * output = Saturate_in_16Bits( ( Right/LeftShift( (Round(input) , shift ) ) + * + * Parametrs: + * [in] input The input to be operated on + * [in] shift The shift number + * + * Return: + * [out] Result of operation + * + **/ + + +OMX_S16 armSatRoundRightShift_S32_S16(OMX_S32 input, OMX_INT shift) +{ + input = armSatRoundLeftShift_S32(input,-shift); + + if ( input > OMX_MAX_S16 ) + { + return (OMX_S16)OMX_MAX_S16; + } + else if (input < OMX_MIN_S16) + { + return (OMX_S16)OMX_MIN_S16; + } + else + { + return (OMX_S16)input; + } + +} + +/** + * Function :armSatRoundLeftShift_S32() + * + * Description : + * Returns the result of saturating left-shift operation on input + * Or rounded Right shift if the input Shift is negative. + * + * Parametrs: + * [in] Value Operand + * [in] Shift Operand for shift operation + * + * Return: + * [out] Result of operation + * + **/ + +OMX_S32 armSatRoundLeftShift_S32(OMX_S32 Value, OMX_INT Shift) +{ + OMX_INT i; + + if (Shift < 0) + { + Shift = -Shift; + Value = armSatAdd_S32(Value, (1 << (Shift - 1))); + Value = Value >> Shift; + } + else + { + for (i = 0; i < Shift; i++) + { + Value = armSatAdd_S32(Value, Value); + } + } + return Value; +} + +/** + * Function :armSatRoundLeftShift_S64() + * + * Description : + * Returns the result of saturating left-shift operation on input + * Or rounded Right shift if the input Shift is negative. + * + * Parametrs: + * [in] Value Operand + * [in] shift Operand for shift operation + * + * Return: + * [out] Result of operation + * + **/ + +OMX_S64 armSatRoundLeftShift_S64(OMX_S64 Value, OMX_INT Shift) +{ + OMX_INT i; + + if (Shift < 0) + { + Shift = -Shift; + Value = armSatAdd_S64(Value, ((OMX_S64)1 << (Shift - 1))); + Value = Value >> Shift; + } + else + { + for (i = 0; i < Shift; i++) + { + Value = armSatAdd_S64(Value, Value); + } + } + return Value; +} + +/** + * Function :armSatMulS16S32_S32() + * + * Description : + * Returns the result of a S16 data type multiplied with an S32 data type + * in a S32 container + * + * Parametrs: + * [in] input1 Operand 1 + * [in] input2 Operand 2 + * + * Return: + * [out] Result of operation + * + **/ + + +OMX_S32 armSatMulS16S32_S32(OMX_S16 input1,OMX_S32 input2) +{ + OMX_S16 hi2,lo1; + OMX_U16 lo2; + + OMX_S32 temp1,temp2; + OMX_S32 result; + + lo1 = input1; + + hi2 = ( input2 >> 16 ); + lo2 = ( (OMX_U32)( input2 << 16 ) >> 16 ); + + temp1 = hi2 * lo1; + temp2 = ( lo2* lo1 ) >> 16; + + result = armSatAdd_S32(temp1,temp2); + + return result; +} + +/** + * Function :armSatMulS32S32_S32() + * + * Description : + * Returns the result of a S32 data type multiplied with an S32 data type + * in a S32 container + * + * Parametrs: + * [in] input1 Operand 1 + * [in] input2 Operand 2 + * + * Return: + * [out] Result of operation + * + **/ + +OMX_S32 armSatMulS32S32_S32(OMX_S32 input1,OMX_S32 input2) +{ + OMX_S16 hi1,hi2; + OMX_U16 lo1,lo2; + + OMX_S32 temp1,temp2,temp3; + OMX_S32 result; + + hi1 = ( input1 >> 16 ); + lo1 = ( (OMX_U32)( input1 << 16 ) >> 16 ); + + hi2 = ( input2 >> 16 ); + lo2 = ( (OMX_U32)( input2 << 16 ) >> 16 ); + + temp1 = hi1 * hi2; + temp2 = ( hi1* lo2 ) >> 16; + temp3 = ( hi2* lo1 ) >> 16; + + result = armSatAdd_S32(temp1,temp2); + result = armSatAdd_S32(result,temp3); + + return result; +} + +/** + * Function :armIntDivAwayFromZero() + * + * Description : Integer division with rounding to the nearest integer. + * Half-integer values are rounded away from zero + * unless otherwise specified. For example 3//2 is rounded + * to 2, and -3//2 is rounded to -2. + * + * Parametrs: + * [in] Num Operand 1 + * [in] Deno Operand 2 + * + * Return: + * [out] Result of operation input1//input2 + * + **/ + +OMX_S32 armIntDivAwayFromZero (OMX_S32 Num, OMX_S32 Deno) +{ + OMX_F64 result; + + result = ((OMX_F64)Num)/((OMX_F64)Deno); + + if (result >= 0) + { + result += 0.5; + } + else + { + result -= 0.5; + } + + return (OMX_S32)(result); +} + + +/*End of File*/ + diff --git a/src/video/h264_decoder/omxdl/armCOMM.h b/src/video/h264_decoder/omxdl/armCOMM.h new file mode 100644 index 000000000..129cf700a --- /dev/null +++ b/src/video/h264_decoder/omxdl/armCOMM.h @@ -0,0 +1,792 @@ +/** + * + * File Name: armCOMM.h + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * File: armCOMM.h + * Brief: Declares Common APIs/Data Types used across OpenMAX API's + * + */ + + +#ifndef _armCommon_H_ +#define _armCommon_H_ + +#include "omxtypes.h" + +typedef struct +{ + OMX_F32 Re; /** Real part */ + OMX_F32 Im; /** Imaginary part */ + +} OMX_FC32; /** single precision floating point complex number */ + +typedef struct +{ + OMX_F64 Re; /** Real part */ + OMX_F64 Im; /** Imaginary part */ + +} OMX_FC64; /** double precision floating point complex number */ + + +/* Used by both IP and IC domains for 8x8 JPEG blocks. */ +typedef OMX_S16 ARM_BLOCK8x8[64]; + + +#include "armOMX.h" + +#define armPI (OMX_F64)(3.1415926535897932384626433832795) + +/***********************************************************************/ + +/* Compiler extensions */ +#ifdef ARM_DEBUG +/* debug version */ +#include +#include +#include +#define armError(str) {printf((str)); printf("\n"); exit(-1);} +#define armWarn(str) {printf((str)); printf("\n");} +#define armIgnore(a) ((void)a) +#define armAssert(a) assert(a) +#else +/* release version */ +#define armError(str) ((void) (str)) +#define armWarn(str) ((void) (str)) +#define armIgnore(a) ((void) (a)) +#define armAssert(a) ((void) (a)) +#endif /* ARM_DEBUG */ + +/* Arithmetic operations */ + +#define armMin(a,b) ( (a) > (b) ? (b):(a) ) +#define armMax(a,b) ( (a) > (b) ? (a):(b) ) +#define armAbs(a) ( (a) < 0 ? -(a):(a) ) + +/* Alignment operation */ + +#define armAlignToBytes(Ptr,N) (Ptr + ( ((N-(int)Ptr)&(N-1)) / sizeof(*Ptr) )) +#define armAlignTo2Bytes(Ptr) armAlignToBytes(Ptr,2) +#define armAlignTo4Bytes(Ptr) armAlignToBytes(Ptr,4) +#define armAlignTo8Bytes(Ptr) armAlignToBytes(Ptr,8) +#define armAlignTo16Bytes(Ptr) armAlignToBytes(Ptr,16) + +/* Error and Alignment check */ + +#define armRetArgErrIf(condition, code) if(condition) { return (code); } +#define armRetDataErrIf(condition, code) if(condition) { return (code); } + +#ifndef ALIGNMENT_DOESNT_MATTER +#define armIsByteAligned(Ptr,N) ((((int)(Ptr)) % N)==0) +#define armNotByteAligned(Ptr,N) ((((int)(Ptr)) % N)!=0) +#else +#define armIsByteAligned(Ptr,N) (1) +#define armNotByteAligned(Ptr,N) (0) +#endif + +#define armIs2ByteAligned(Ptr) armIsByteAligned(Ptr,2) +#define armIs4ByteAligned(Ptr) armIsByteAligned(Ptr,4) +#define armIs8ByteAligned(Ptr) armIsByteAligned(Ptr,8) +#define armIs16ByteAligned(Ptr) armIsByteAligned(Ptr,16) + +#define armNot2ByteAligned(Ptr) armNotByteAligned(Ptr,2) +#define armNot4ByteAligned(Ptr) armNotByteAligned(Ptr,4) +#define armNot8ByteAligned(Ptr) armNotByteAligned(Ptr,8) +#define armNot16ByteAligned(Ptr) armNotByteAligned(Ptr,16) +#define armNot32ByteAligned(Ptr) armNotByteAligned(Ptr,32) + +/** + * Function: armRoundFloatToS16_ref/armRoundFloatToS32_ref/armRoundFloatToS64 + * + * Description: + * Converts a double precision value into a short int/int after rounding + * + * Parameters: + * [in] Value Float value to be converted + * + * Return Value: + * [out] converted value in OMX_S16/OMX_S32 format + * + */ +__attribute__((weak)) +OMX_S16 armRoundFloatToS16 (OMX_F64 Value); +__attribute__((weak)) +OMX_S32 armRoundFloatToS32 (OMX_F64 Value); +__attribute__((weak)) +OMX_S64 armRoundFloatToS64 (OMX_F64 Value); + +/** + * Function: armSatRoundFloatToS16_ref/armSatRoundFloatToS32 + * + * Description: + * Converts a double precision value into a short int/int after rounding and saturation + * + * Parameters: + * [in] Value Float value to be converted + * + * Return Value: + * [out] converted value in OMX_S16/OMX_S32 format + * + */ + +__attribute__((weak)) +OMX_S16 armSatRoundFloatToS16 (OMX_F64 Value); +__attribute__((weak)) +OMX_S32 armSatRoundFloatToS32 (OMX_F64 Value); + +/** + * Function: armSatRoundFloatToU16_ref/armSatRoundFloatToU32 + * + * Description: + * Converts a double precision value into a unsigned short int/int after rounding and saturation + * + * Parameters: + * [in] Value Float value to be converted + * + * Return Value: + * [out] converted value in OMX_U16/OMX_U32 format + * + */ + + __attribute__((weak)) + OMX_U16 armSatRoundFloatToU16 (OMX_F64 Value); +__attribute__((weak)) +OMX_U32 armSatRoundFloatToU32 (OMX_F64 Value); + +/** + * Function: armSignCheck + * + * Description: + * Checks the sign of a variable: + * returns 1 if it is Positive + * returns 0 if it is 0 + * returns -1 if it is Negative + * + * Remarks: + * + * Parameters: + * [in] var Variable to be checked + * + * Return Value: + * OMX_INT -- returns 1 if it is Positive + * returns 0 if it is 0 + * returns -1 if it is Negative + */ + __attribute__((weak)) +OMX_INT armSignCheck (OMX_S16 var); + +/** + * Function: armClip + * + * Description: Clips the input between MAX and MIN value + * + * + * Remarks: + * + * Parameters: + * [in] Min lower bound + * [in] Max upper bound + * [in] src variable to the clipped + * + * Return Value: + * OMX_S32 -- returns clipped value + */ + __attribute__((weak)) +OMX_S32 armClip ( + OMX_INT min, + OMX_INT max, + OMX_S32 src + ); + +/** + * Function: armClip_F32 + * + * Description: Clips the input between MAX and MIN value + * + * + * Remarks: + * + * Parameters: + * [in] Min lower bound + * [in] Max upper bound + * [in] src variable to the clipped + * + * Return Value: + * OMX_F32 -- returns clipped value + */ + __attribute__((weak)) +OMX_F32 armClip_F32 ( + OMX_F32 min, + OMX_F32 max, + OMX_F32 src + ); + +/** + * Function: armShiftSat_F32 + * + * Description: Divides a float value by 2^shift and + * saturates it for unsigned value range for satBits. + * Second parameter is like "shifting" the corresponding + * integer value. Takes care of rounding while clipping the final + * value. + * + * Parameters: + * [in] v Number to be operated upon + * [in] shift Divides the input "v" by "2^shift" + * [in] satBits Final range is [0, 2^satBits) + * + * Return Value: + * OMX_S32 -- returns "shifted" saturated value + */ + __attribute__((weak)) +OMX_U32 armShiftSat_F32( + OMX_F32 v, + OMX_INT shift, + OMX_INT satBits + ); + +/** + * Functions: armSwapElem + * + * Description: + * This function swaps two elements at the specified pointer locations. + * The size of each element could be anything as specified by + * + * Return Value: + * OMXResult -- Error status from the function + */ + __attribute__((weak)) + OMXResult armSwapElem(OMX_U8 *pBuf1, OMX_U8 *pBuf2, OMX_INT elemSize); + + +/** + * Function: armMedianOf3 + * + * Description: Finds the median of three numbers + * + * Remarks: + * + * Parameters: + * [in] fEntry First entry + * [in] sEntry second entry + * [in] tEntry Third entry + * + * Return Value: + * OMX_S32 -- returns the median value + */ + __attribute__((weak)) +OMX_S32 armMedianOf3 ( + OMX_S32 fEntry, + OMX_S32 sEntry, + OMX_S32 tEntry + ); + +/** + * Function: armLogSize + * + * Description: Finds the size of a positive value and returns the same + * + * Remarks: + * + * Parameters: + * [in] value Positive value + * + * Return Value: + * OMX_U8 -- returns the size of the positive value + */ + __attribute__((weak)) +OMX_U8 armLogSize ( + OMX_U16 value + ); + +/***********************************************************************/ + /* Saturating Arithmetic operations */ + +/** + * Function :armSatAdd_S32() + * + * Description : + * Returns the result of saturated addition of the two inputs Value1, Value2 + * + * Parametrs: + * [in] Value1 First Operand + * [in] Value2 Second Operand + * + * Return: + * [out] Result of operation + * + * + **/ + __attribute__((weak)) +OMX_S32 armSatAdd_S32( + OMX_S32 Value1, + OMX_S32 Value2 + ); + +/** + * Function :armSatAdd_S64() + * + * Description : + * Returns the result of saturated addition of the two inputs Value1, Value2 + * + * Parametrs: + * [in] Value1 First Operand + * [in] Value2 Second Operand + * + * Return: + * [out] Result of operation + * + * + **/ + __attribute__((weak)) +OMX_S64 armSatAdd_S64( + OMX_S64 Value1, + OMX_S64 Value2 + ); + +/** Function :armSatSub_S32() + * + * Description : + * Returns the result of saturated substraction of the two inputs Value1, Value2 + * + * Parametrs: + * [in] Value1 First Operand + * [in] Value2 Second Operand + * + * Return: + * [out] Result of operation + * + **/ + __attribute__((weak)) +OMX_S32 armSatSub_S32( + OMX_S32 Value1, + OMX_S32 Value2 + ); + +/** + * Function :armSatMac_S32() + * + * Description : + * Returns the result of Multiplication of Value1 and Value2 and subesquent saturated + * accumulation with Mac + * + * Parametrs: + * [in] Value1 First Operand + * [in] Value2 Second Operand + * [in] Mac Accumulator + * + * Return: + * [out] Result of operation + **/ + __attribute__((weak)) +OMX_S32 armSatMac_S32( + OMX_S32 Mac, + OMX_S16 Value1, + OMX_S16 Value2 + ); + +/** + * Function :armSatMac_S16S32_S32 + * + * Description : + * Returns the result of saturated MAC operation of the three inputs delayElem, filTap , mac + * + * mac = mac + Saturate_in_32Bits(delayElem * filTap) + * + * Parametrs: + * [in] delayElem First 32 bit Operand + * [in] filTap Second 16 bit Operand + * [in] mac Result of MAC operation + * + * Return: + * [out] mac Result of operation + * + **/ + __attribute__((weak)) +OMX_S32 armSatMac_S16S32_S32( + OMX_S32 mac, + OMX_S32 delayElem, + OMX_S16 filTap ); + +/** + * Function :armSatRoundRightShift_S32_S16 + * + * Description : + * Returns the result of rounded right shift operation of input by the scalefactor + * + * output = Saturate_in_16Bits( ( RightShift( (Round(input) , scaleFactor ) ) + * + * Parametrs: + * [in] input The input to be operated on + * [in] scaleFactor The shift number + * + * Return: + * [out] Result of operation + * + **/ + + __attribute__((weak)) +OMX_S16 armSatRoundRightShift_S32_S16( + OMX_S32 input, + OMX_INT scaleFactor); + +/** + * Function :armSatRoundLeftShift_S32() + * + * Description : + * Returns the result of saturating left-shift operation on input + * Or rounded Right shift if the input Shift is negative. + * + * Parametrs: + * [in] Value Operand + * [in] shift Operand for shift operation + * + * Return: + * [out] Result of operation + * + **/ + __attribute__((weak)) +OMX_S32 armSatRoundLeftShift_S32( + OMX_S32 Value, + OMX_INT shift + ); + +/** + * Function :armSatRoundLeftShift_S64() + * + * Description : + * Returns the result of saturating left-shift operation on input + * Or rounded Right shift if the input Shift is negative. + * + * Parametrs: + * [in] Value Operand + * [in] shift Operand for shift operation + * + * Return: + * [out] Result of operation + * + **/ + __attribute__((weak)) +OMX_S64 armSatRoundLeftShift_S64( + OMX_S64 Value, + OMX_INT shift + ); + +/** + * Function :armSatMulS16S32_S32() + * + * Description : + * Returns the result of a S16 data type multiplied with an S32 data type + * in a S32 container + * + * Parametrs: + * [in] input1 Operand 1 + * [in] input2 Operand 2 + * + * Return: + * [out] Result of operation + * + **/ + + __attribute__((weak)) +OMX_S32 armSatMulS16S32_S32( + OMX_S16 input1, + OMX_S32 input2); + +/** + * Function :armSatMulS32S32_S32() + * + * Description : + * Returns the result of a S32 data type multiplied with an S32 data type + * in a S32 container + * + * Parametrs: + * [in] input1 Operand 1 + * [in] input2 Operand 2 + * + * Return: + * [out] Result of operation + * + **/ + __attribute__((weak)) +OMX_S32 armSatMulS32S32_S32( + OMX_S32 input1, + OMX_S32 input2); + + +/** + * Function :armIntDivAwayFromZero() + * + * Description : Integer division with rounding to the nearest integer. + * Half-integer values are rounded away from zero + * unless otherwise specified. For example 3//2 is rounded + * to 2, and -3//2 is rounded to -2. + * + * Parametrs: + * [in] Num Operand 1 + * [in] Deno Operand 2 + * + * Return: + * [out] Result of operation input1//input2 + * + **/ + __attribute__((weak)) +OMX_S32 armIntDivAwayFromZero (OMX_S32 Num, OMX_S32 Deno); + + +/***********************************************************************/ +/* + * Debugging macros + * + */ + + +/* + * Definition of output stream - change to stderr if necessary + */ +#define DEBUG_STREAM stdout + +/* + * Debug printf macros, one for each argument count. + * Add more if needed. + */ +#ifdef DEBUG_ON +#include + +#define DEBUG_PRINTF_0(a) fprintf(DEBUG_STREAM, a) +#define DEBUG_PRINTF_1(a, b) fprintf(DEBUG_STREAM, a, b) +#define DEBUG_PRINTF_2(a, b, c) fprintf(DEBUG_STREAM, a, b, c) +#define DEBUG_PRINTF_3(a, b, c, d) fprintf(DEBUG_STREAM, a, b, c, d) +#define DEBUG_PRINTF_4(a, b, c, d, e) fprintf(DEBUG_STREAM, a, b, c, d, e) +#define DEBUG_PRINTF_5(a, b, c, d, e, f) fprintf(DEBUG_STREAM, a, b, c, d, e, f) +#define DEBUG_PRINTF_6(a, b, c, d, e, f, g) fprintf(DEBUG_STREAM, a, b, c, d, e, f, g) +#define DEBUG_PRINTF_7(a, b, c, d, e, f, g, h) fprintf(DEBUG_STREAM, a, b, c, d, e, f, g, h) +#define DEBUG_PRINTF_8(a, b, c, d, e, f, g, h, i) fprintf(DEBUG_STREAM, a, b, c, d, e, f, g, h, i) +#define DEBUG_PRINTF_9(a, b, c, d, e, f, g, h, i, j) fprintf(DEBUG_STREAM, a, b, c, d, e, f, g, h, i, j) +#define DEBUG_PRINTF_10(a, b, c, d, e, f, g, h, i, j, k) fprintf(DEBUG_STREAM, a, b, c, d, e, f, g, h, i, j, k) +#define DEBUG_PRINTF_11(a, b, c, d, e, f, g, h, i, j, k, l) fprintf(DEBUG_STREAM, a, b, c, d, e, f, g, h, i, j, k, l) +#define DEBUG_PRINTF_12(a, b, c, d, e, f, g, h, i, j, k, l, m) fprintf(DEBUG_STREAM, a, b, c, d, e, f, g, h, i, j, k, l, m) +#define DEBUG_PRINTF_13(a, b, c, d, e, f, g, h, i, j, k, l, m, n) fprintf(DEBUG_STREAM, a, b, c, d, e, f, g, h, i, j, k, l, m, n) +#define DEBUG_PRINTF_14(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) fprintf(DEBUG_STREAM, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) +#else /* DEBUG_ON */ +#define DEBUG_PRINTF_0(a) +#define DEBUG_PRINTF_1(a, b) +#define DEBUG_PRINTF_2(a, b, c) +#define DEBUG_PRINTF_3(a, b, c, d) +#define DEBUG_PRINTF_4(a, b, c, d, e) +#define DEBUG_PRINTF_5(a, b, c, d, e, f) +#define DEBUG_PRINTF_6(a, b, c, d, e, f, g) +#define DEBUG_PRINTF_7(a, b, c, d, e, f, g, h) +#define DEBUG_PRINTF_8(a, b, c, d, e, f, g, h, i) +#define DEBUG_PRINTF_9(a, b, c, d, e, f, g, h, i, j) +#define DEBUG_PRINTF_10(a, b, c, d, e, f, g, h, i, j, k) +#define DEBUG_PRINTF_11(a, b, c, d, e, f, g, h, i, j, k, l) +#define DEBUG_PRINTF_12(a, b, c, d, e, f, g, h, i, j, k, l, m) +#define DEBUG_PRINTF_13(a, b, c, d, e, f, g, h, i, j, k, l, m, n) +#define DEBUG_PRINTF_14(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) +#endif /* DEBUG_ON */ + + +/* + * Domain and sub domain definitions + * + * In order to turn on debug for an entire domain or sub-domain + * at compile time, one of the DEBUG_DOMAIN_* below may be defined, + * which will activate debug in all of the defines it contains. + */ + +#ifdef DEBUG_DOMAIN_AC +#define DEBUG_OMXACAAC_DECODECHANPAIRELT_MPEG4 +#define DEBUG_OMXACAAC_DECODECHANPAIRELT +#define DEBUG_OMXACAAC_DECODEDATSTRELT +#define DEBUG_OMXACAAC_DECODEFILLELT +#define DEBUG_OMXACAAC_DECODEISSTEREO_S32 +#define DEBUG_OMXACAAC_DECODEMSPNS_S32 +#define DEBUG_OMXACAAC_DECODEMSSTEREO_S32_I +#define DEBUG_OMXACAAC_DECODEPRGCFGELT +#define DEBUG_OMXACAAC_DECODETNS_S32_I +#define DEBUG_OMXACAAC_DEINTERLEAVESPECTRUM_S32 +#define DEBUG_OMXACAAC_ENCODETNS_S32_I +#define DEBUG_OMXACAAC_LONGTERMPREDICT_S32 +#define DEBUG_OMXACAAC_LONGTERMRECONSTRUCT_S32 +#define DEBUG_OMXACAAC_MDCTFWD_S32 +#define DEBUG_OMXACAAC_MDCTINV_S32_S16 +#define DEBUG_OMXACAAC_NOISELESSDECODE +#define DEBUG_OMXACAAC_QUANTINV_S32_I +#define DEBUG_OMXACAAC_UNPACKADIFHEADER +#define DEBUG_OMXACAAC_UNPACKADTSFRAMEHEADER +#define DEBUG_OMXACMP3_HUFFMANDECODESFBMBP_S32 +#define DEBUG_OMXACMP3_HUFFMANDECODESFB_S32 +#define DEBUG_OMXACMP3_HUFFMANDECODE_S32 +#define DEBUG_OMXACMP3_MDCTINV_S32 +#define DEBUG_OMXACMP3_REQUANTIZESFB_S32_I +#define DEBUG_OMXACMP3_REQUANTIZE_S32_I +#define DEBUG_OMXACMP3_SYNTHPQMF_S32_S16 +#define DEBUG_OMXACMP3_UNPACKFRAMEHEADER +#define DEBUG_OMXACMP3_UNPACKSCALEFACTORS_S8 +#define DEBUG_OMXACMP3_UNPACKSIDEINFO +#endif /* DEBUG_DOMAIN_AC */ + + +#ifdef DEBUG_DOMAIN_VC +#define DEBUG_OMXVCM4P10_AVERAGE_16X +#define DEBUG_OMXVCM4P10_AVERAGE_4X +#define DEBUG_OMXVCM4P10_AVERAGE_8X +#define DEBUG_OMXVCM4P10_DEBLOCKCHROMA_U8_C1IR +#define DEBUG_OMXVCM4P10_DEBLOCKLUMA_U8_C1IR +#define DEBUG_OMXVCM4P10_DECODECHROMADCCOEFFSTOPAIRCAVLC_U8 +#define DEBUG_OMXVCM4P10_DECODECOEFFSTOPAIRCAVLC_U8 +#define DEBUG_OMXVCM4P10_DEQUANTTRANSFORMACFROMPAIR_U8_S16_C1_DLX +#define DEBUG_OMXVCM4P10_EXPANDFRAME +#define DEBUG_OMXVCM4P10_FILTERDEBLOCKINGCHROMA_HOREDGE_U8_C1IR +#define DEBUG_OMXVCM4P10_FILTERDEBLOCKINGCHROMA_VEREDGE_U8_C1IR +#define DEBUG_OMXVCM4P10_FILTERDEBLOCKINGLUMA_HOREDGE_U8_C1IR +#define DEBUG_OMXVCM4P10_FILTERDEBLOCKINGLUMA_VEREDGE_U8_C1IR +#define DEBUG_OMXVCM4P10_PREDICTINTRACHROMA8X8_U8_C1R +#define DEBUG_OMXVCM4P10_PREDICTINTRA_16X16_U8_C1R +#define DEBUG_OMXVCM4P10_PREDICTINTRA_4X4_U8_C1R +#define DEBUG_OMXVCM4P10_SADQUAR_16X +#define DEBUG_OMXVCM4P10_SADQUAR_4X +#define DEBUG_OMXVCM4P10_SADQUAR_8X +#define DEBUG_OMXVCM4P10_SAD_16X +#define DEBUG_OMXVCM4P10_SAD_4X +#define DEBUG_OMXVCM4P10_SAD_8X +#define DEBUG_OMXVCM4P10_SATD_4X4 +#define DEBUG_OMXVCM4P10_TRANSFORMDEQUANTCHROMADCFROMPAIR_U8_S16_C1 +#define DEBUG_OMXVCM4P10_TRANSFORMDEQUANTLUMADCFROMPAIR_U8_S16_C1 +#define DEBUG_OMXVCM4P10_TRANSFORMQUANT_CHROMADC +#define DEBUG_OMXVCM4P10_TRANSFORMQUANT_LUMADC +#define DEBUG_OMXVCM4P2_BLOCKMATCH_HALF_16X16 +#define DEBUG_OMXVCM4P2_BLOCKMATCH_HALF_8X8 +#define DEBUG_OMXVCM4P2_BLOCKMATCH_INTEGER_16X16 +#define DEBUG_OMXVCM4P2_BLOCKMATCH_INTEGER_8X8 +#define DEBUG_OMXVCM4P2_COMPUTETEXTUREERRORBLOCK_SAD_U8_S16 +#define DEBUG_OMXVCM4P2_COMPUTETEXTUREERRORBLOCK_U8_S16 +#define DEBUG_OMXVCM4P2_DCT8X8BLKDLX +#define DEBUG_OMXVCM4P2_DECODEBLOCKCOEF_INTER_S16 +#define DEBUG_OMXVCM4P2_DECODEPADMV_PVOP +#define DEBUG_OMXVCM4P2_DECODEVLCZIGZAG_INTER_S16 +#define DEBUG_OMXVCM4P2_DECODEVLCZIGZAG_INTRAACVLC_S16 +#define DEBUG_OMXVCM4P2_DECODEVLCZIGZAG_INTRADCVLC_S16 +#define DEBUG_OMXVCM4P2_ENCODEMV_U8_S16 +#define DEBUG_OMXVCM4P2_ENCODEVLCZIGZAG_INTER_S16 +#define DEBUG_OMXVCM4P2_ENCODEVLCZIGZAG_INTRAACVLC_S16 +#define DEBUG_OMXVCM4P2_ENCODEVLCZIGZAG_INTRADCVLC_S16 +#define DEBUG_OMXVCM4P2_FINDMVPRED +#define DEBUG_OMXVCM4P2_IDCT8X8BLKDLX +#define DEBUG_OMXVCM4P2_LIMITMVTORECT +#define DEBUG_OMXVCM4P2_MOTIONESTIMATIONMB +#define DEBUG_OMXVCM4P2_PADMBGRAY_U8 +#define DEBUG_OMXVCM4P2_PADMBHORIZONTAL_U8 +#define DEBUG_OMXVCM4P2_PADMBVERTICAL_U8 +#define DEBUG_OMXVCM4P2_PADMV +#define DEBUG_OMXVCM4P2_QUANTINTER_S16_I +#define DEBUG_OMXVCM4P2_QUANTINTRA_S16_I +#define DEBUG_OMXVCM4P2_QUANTINVINTER_S16_I +#define DEBUG_OMXVCM4P2_QUANTINVINTRA_S16_I +#define DEBUG_OMXVCM4P2_TRANSRECBLOCKCEOF_INTER +#define DEBUG_OMXVCM4P2_TRANSRECBLOCKCEOF_INTRA +#endif /* DEBUG_DOMAIN_VC */ + + +#ifdef DEBUG_DOMAIN_IC +/* To be filled in */ +#endif /* DEBUG_DOMAIN_IC */ + + +#ifdef DEBUG_DOMAIN_SP +#define DEBUG_OMXACSP_DOTPROD_S16 +#define DEBUG_OMXACSP_BLOCKEXP_S16 +#define DEBUG_OMXACSP_BLOCKEXP_S32 +#define DEBUG_OMXACSP_COPY_S16 +#define DEBUG_OMXACSP_DOTPROD_S16 +#define DEBUG_OMXACSP_DOTPROD_S16_SFS +#define DEBUG_OMXACSP_FFTFWD_CTOC_SC16_SFS +#define DEBUG_OMXACSP_FFTFWD_CTOC_SC32_SFS +#define DEBUG_OMXACSP_FFTFWD_RTOCCS_S16S32_SFS +#define DEBUG_OMXACSP_FFTFWD_RTOCCS_S32_SFS +#define DEBUG_OMXACSP_FFTGETBUFSIZE_C_SC16 +#define DEBUG_OMXACSP_FFTGETBUFSIZE_C_SC32 +#define DEBUG_OMXACSP_FFTGETBUFSIZE_R_S16_S32 +#define DEBUG_OMXACSP_FFTGETBUFSIZE_R_S32 +#define DEBUG_OMXACSP_FFTINIT_C_SC16 +#define DEBUG_OMXACSP_FFTINIT_C_SC32 +#define DEBUG_OMXACSP_FFTINIT_R_S16_S32 +#define DEBUG_OMXACSP_FFTINIT_R_S32 +#define DEBUG_OMXACSP_FFTINV_CCSTOR_S32S16_SFS +#define DEBUG_OMXACSP_FFTINV_CCSTOR_S32_SFS +#define DEBUG_OMXACSP_FFTINV_CTOC_SC16_SFS +#define DEBUG_OMXACSP_FFTINV_CTOC_SC32_SFS +#define DEBUG_OMXACSP_FILTERMEDIAN_S32_I +#define DEBUG_OMXACSP_FILTERMEDIAN_S32 +#define DEBUG_OMXACSP_FIRONE_DIRECT_S16_ISFS +#define DEBUG_OMXACSP_FIRONE_DIRECT_S16_I +#define DEBUG_OMXACSP_FIRONE_DIRECT_S16 +#define DEBUG_OMXACSP_FIRONE_DIRECT_S16_SFS +#define DEBUG_OMXACSP_FIR_DIRECT_S16_ISFS +#define DEBUG_OMXACSP_FIR_DIRECT_S16_I +#define DEBUG_OMXACSP_FIR_DIRECT_S16 +#define DEBUG_OMXACSP_FIR_DIRECT_S16_SFS +#define DEBUG_OMXACSP_IIRONE_BIQUADDIRECT_S16_I +#define DEBUG_OMXACSP_IIRONE_BIQUADDIRECT_S16 +#define DEBUG_OMXACSP_IIRONE_DIRECT_S16_I +#define DEBUG_OMXACSP_IIRONE_DIRECT_S16 +#define DEBUG_OMXACSP_IIR_BIQUADDIRECT_S16_I +#define DEBUG_OMXACSP_IIR_BIQUADDIRECT_S16 +#define DEBUG_OMXACSP_IIR_DIRECT_S16_I +#define DEBUG_OMXACSP_IIR_DIRECT_S16 +#endif /* DEBUG_DOMAIN_SP */ + + +#ifdef DEBUG_DOMAIN_IP +#define DEBUG_OMXIPBM_ADDC_U8_C1R_SFS +#define DEBUG_OMXIPBM_COPY_U8_C1R +#define DEBUG_OMXIPBM_COPY_U8_C3R +#define DEBUG_OMXIPBM_MIRROR_U8_C1R +#define DEBUG_OMXIPBM_MULC_U8_C1R_SFS +#define DEBUG_OMXIPCS_COLORTWISTQ14_U8_C3R +#define DEBUG_OMXIPCS_RGB565TOYCBCR420LS_MCU_U16_S16_C3P3R +#define DEBUG_OMXIPCS_RGB565TOYCBCR422LS_MCU_U16_S16_C3P3R +#define DEBUG_OMXIPCS_RGB565TOYCBCR444LS_MCU_U16_S16_C3P3R +#define DEBUG_OMXIPCS_RGBTOYCBCR420LS_MCU_U8_S16_C3P3R +#define DEBUG_OMXIPCS_RGBTOYCBCR422LS_MCU_U8_S16_C3P3R +#define DEBUG_OMXIPCS_RGBTOYCBCR444LS_MCU_U8_S16_C3P3R +#define DEBUG_OMXIPCS_YCBCR420RSZROT_U8_P3R +#define DEBUG_OMXIPCS_YCBCR420TORGB565LS_MCU_S16_U16_P3C3R +#define DEBUG_OMXIPCS_YCBCR420TORGB565_U8_U16_P3C3R +#define DEBUG_OMXIPCS_YCBCR420TORGBLS_MCU_S16_U8_P3C3R +#define DEBUG_OMXIPCS_YCBCR422RSZCSCROTRGB_U8_C2R +#define DEBUG_OMXIPCS_YCBCR422RSZROT_U8_P3R +#define DEBUG_OMXIPCS_YCBCR422TORGB565LS_MCU_S16_U16_P3C3R +#define DEBUG_OMXIPCS_YCBCR422TORGB565_U8_U16_C2C3R +#define DEBUG_OMXIPCS_YCBCR422TORGBLS_MCU_S16_U8_P3C3R +#define DEBUG_OMXIPCS_YCBCR422TORGB_U8_C2C3R +#define DEBUG_OMXIPCS_YCBCR422TOYCBCR420ROTATE_U8_C2P3R +#define DEBUG_OMXIPCS_YCBCR422TOYCBCR420ROTATE_U8_P3R +#define DEBUG_OMXIPCS_YCBCR444TORGB565LS_MCU_S16_U16_P3C3R +#define DEBUG_OMXIPCS_YCBCR444TORGBLS_MCU_S16_U8_P3C3R +#define DEBUG_OMXIPCS_YCBCRTORGB565_U8_U16_C3R +#define DEBUG_OMXIPCS_YCBCRTORGB565_U8_U16_P3C3R +#define DEBUG_OMXIPCS_YCBCRTORGB_U8_C3R +#define DEBUG_OMXIPPP_GETCENTRALMOMENT_S64 +#define DEBUG_OMXIPPP_GETSPATIALMOMENT_S64 +#define DEBUG_OMXIPPP_MOMENTGETSTATESIZE_S64 +#define DEBUG_OMXIPPP_MOMENTINIT_S64 +#define DEBUG_OMXIPPP_MOMENTS64S_U8_C1R +#define DEBUG_OMXIPPP_MOMENTS64S_U8_C3R +#endif /* DEBUG_DOMAIN_IP */ + + +#endif /* _armCommon_H_ */ + +/*End of File*/ + + + + diff --git a/src/video/h264_decoder/omxdl/armCOMM_Bitstream.c b/src/video/h264_decoder/omxdl/armCOMM_Bitstream.c new file mode 100644 index 000000000..9ef9319d3 --- /dev/null +++ b/src/video/h264_decoder/omxdl/armCOMM_Bitstream.c @@ -0,0 +1,329 @@ +/** + * + * File Name: armCOMM_Bitstream.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * Defines bitstream encode and decode functions common to all codecs + */ + +#include "omxtypes.h" +#include "armCOMM.h" +#include "armCOMM_Bitstream.h" + +/*************************************** + * Fixed bit length Decode + ***************************************/ + +/** + * Function: armLookAheadBits() + * + * Description: + * Get the next N bits from the bitstream without advancing the bitstream pointer + * + * Parameters: + * [in] **ppBitStream + * [in] *pOffset + * [in] N=1...32 + * + * Returns Value + */ + +OMX_U32 armLookAheadBits(const OMX_U8 **ppBitStream, OMX_INT *pOffset, OMX_INT N) +{ + const OMX_U8 *pBitStream = *ppBitStream; + OMX_INT Offset = *pOffset; + OMX_U32 Value; + + armAssert(Offset>=0 && Offset<=7); + armAssert(N>=1 && N<=32); + + /* Read next 32 bits from stream */ + Value = (pBitStream[0] << 24 ) | ( pBitStream[1] << 16) | (pBitStream[2] << 8 ) | (pBitStream[3]) ; + Value = (Value << Offset ) | (pBitStream[4] >> (8-Offset)); + + /* Return N bits */ + return Value >> (32-N); +} + + +/** + * Function: armGetBits() + * + * Description: + * Read N bits from the bitstream + * + * Parameters: + * [in] *ppBitStream + * [in] *pOffset + * [in] N=1..32 + * + * [out] *ppBitStream + * [out] *pOffset + * Returns Value + */ + + +OMX_U32 armGetBits(const OMX_U8 **ppBitStream, OMX_INT *pOffset, OMX_INT N) +{ + const OMX_U8 *pBitStream = *ppBitStream; + OMX_INT Offset = *pOffset; + OMX_U32 Value; + + if(N == 0) + { + return 0; + } + + armAssert(Offset>=0 && Offset<=7); + armAssert(N>=1 && N<=32); + + /* Read next 32 bits from stream */ + Value = (pBitStream[0] << 24 ) | ( pBitStream[1] << 16) | (pBitStream[2] << 8 ) | (pBitStream[3]) ; + Value = (Value << Offset ) | (pBitStream[4] >> (8-Offset)); + + /* Advance bitstream pointer by N bits */ + Offset += N; + *ppBitStream = pBitStream + (Offset>>3); + *pOffset = Offset & 7; + + /* Return N bits */ + return Value >> (32-N); +} + +/** + * Function: armByteAlign() + * + * Description: + * Align the pointer *ppBitStream to the next byte boundary + * + * Parameters: + * [in] *ppBitStream + * [in] *pOffset + * + * [out] *ppBitStream + * [out] *pOffset + * + **/ + +OMXVoid armByteAlign(const OMX_U8 **ppBitStream,OMX_INT *pOffset) +{ + if(*pOffset > 0) + { + *ppBitStream += 1; + *pOffset = 0; + } +} + +/** + * Function: armSkipBits() + * + * Description: + * Skip N bits from the value at *ppBitStream + * + * Parameters: + * [in] *ppBitStream + * [in] *pOffset + * [in] N + * + * [out] *ppBitStream + * [out] *pOffset + * + **/ + + +OMXVoid armSkipBits(const OMX_U8 **ppBitStream,OMX_INT *pOffset,OMX_INT N) +{ + OMX_INT Offset = *pOffset; + const OMX_U8 *pBitStream = *ppBitStream; + + /* Advance bitstream pointer by N bits */ + Offset += N; + *ppBitStream = pBitStream + (Offset>>3); + *pOffset = Offset & 7; +} + +/*************************************** + * Variable bit length Decode + ***************************************/ + +/** + * Function: armUnPackVLC32() + * + * Description: + * Variable length decode of variable length symbol (max size 32 bits) read from + * the bit stream pointed by *ppBitStream at *pOffset by using the table + * pointed by pCodeBook + * + * Parameters: + * [in] *pBitStream + * [in] *pOffset + * [in] pCodeBook + * + * [out] *pBitStream + * [out] *pOffset + * + * Returns : Code Book Index if successfull. + * : ARM_NO_CODEBOOK_INDEX = -1 if search fails. + **/ +#ifndef C_OPTIMIZED_IMPLEMENTATION + +OMX_U16 armUnPackVLC32( + const OMX_U8 **ppBitStream, + OMX_INT *pOffset, + const ARM_VLC32 *pCodeBook +) +{ + const OMX_U8 *pBitStream = *ppBitStream; + OMX_INT Offset = *pOffset; + OMX_U32 Value; + OMX_INT Index; + + armAssert(Offset>=0 && Offset<=7); + + /* Read next 32 bits from stream */ + Value = (pBitStream[0] << 24 ) | ( pBitStream[1] << 16) | (pBitStream[2] << 8 ) | (pBitStream[3]) ; + Value = (Value << Offset ) | (pBitStream[4] >> (8-Offset)); + + /* Search through the codebook */ + for (Index=0; pCodeBook->codeLen != 0; Index++) + { + if (pCodeBook->codeWord == (Value >> (32 - pCodeBook->codeLen))) + { + Offset = Offset + pCodeBook->codeLen; + *ppBitStream = pBitStream + (Offset >> 3) ; + *pOffset = Offset & 7; + + return Index; + } + pCodeBook++; + } + + /* No code match found */ + return ARM_NO_CODEBOOK_INDEX; +} + +#endif + +/*************************************** + * Fixed bit length Encode + ***************************************/ + +/** + * Function: armPackBits + * + * Description: + * Pack a VLC code word into the bitstream + * + * Remarks: + * + * Parameters: + * [in] ppBitStream pointer to the pointer to the current byte + * in the bit stream. + * [in] pOffset pointer to the bit position in the byte + * pointed by *ppBitStream. Valid within 0 + * to 7. + * [in] codeWord Code word that need to be inserted in to the + * bitstream + * [in] codeLength Length of the code word valid range 1...32 + * + * [out] ppBitStream *ppBitStream is updated after the block is encoded, + * so that it points to the current byte in the bit + * stream buffer. + * [out] pBitOffset *pBitOffset is updated so that it points to the + * current bit position in the byte pointed by + * *ppBitStream. + * + * Return Value: + * Standard OMX_RESULT result. See enumeration for possible result codes. + * + */ + +OMXResult armPackBits ( + OMX_U8 **ppBitStream, + OMX_INT *pOffset, + OMX_U32 codeWord, + OMX_INT codeLength +) +{ + OMX_U8 *pBitStream = *ppBitStream; + OMX_INT Offset = *pOffset; + OMX_U32 Value; + + /* checking argument validity */ + armRetArgErrIf(Offset < 0, OMX_Sts_BadArgErr); + armRetArgErrIf(Offset > 7, OMX_Sts_BadArgErr); + armRetArgErrIf(codeLength < 1, OMX_Sts_BadArgErr); + armRetArgErrIf(codeLength > 32, OMX_Sts_BadArgErr); + + /* Prepare the first byte */ + codeWord = codeWord << (32-codeLength); + Value = (pBitStream[0] >> (8-Offset)) << (8-Offset); + Value = Value | (codeWord >> (24+Offset)); + + /* Write out whole bytes */ + while (8-Offset <= codeLength) + { + *pBitStream++ = (OMX_U8)Value; + codeWord = codeWord << (8-Offset); + codeLength = codeLength - (8-Offset); + Offset = 0; + Value = codeWord >> 24; + } + + /* Write out final partial byte */ + *pBitStream = (OMX_U8)Value; + *ppBitStream = pBitStream; + *pOffset = Offset + codeLength; + + return OMX_Sts_NoErr; +} + +/*************************************** + * Variable bit length Encode + ***************************************/ + +/** + * Function: armPackVLC32 + * + * Description: + * Pack a VLC code word into the bitstream + * + * Remarks: + * + * Parameters: + * [in] ppBitStream pointer to the pointer to the current byte + * in the bit stream. + * [in] pBitOffset pointer to the bit position in the byte + * pointed by *ppBitStream. Valid within 0 + * to 7. + * [in] code VLC code word that need to be inserted in to the + * bitstream + * + * [out] ppBitStream *ppBitStream is updated after the block is encoded, + * so that it points to the current byte in the bit + * stream buffer. + * [out] pBitOffset *pBitOffset is updated so that it points to the + * current bit position in the byte pointed by + * *ppBitStream. + * + * Return Value: + * Standard OMX_RESULT result. See enumeration for possible result codes. + * + */ + +OMXResult armPackVLC32 ( + OMX_U8 **ppBitStream, + OMX_INT *pBitOffset, + ARM_VLC32 code +) +{ + return (armPackBits(ppBitStream, pBitOffset, code.codeWord, code.codeLen)); +} + +/*End of File*/ diff --git a/src/video/h264_decoder/omxdl/armCOMM_Bitstream.h b/src/video/h264_decoder/omxdl/armCOMM_Bitstream.h new file mode 100644 index 000000000..4f9bc3bee --- /dev/null +++ b/src/video/h264_decoder/omxdl/armCOMM_Bitstream.h @@ -0,0 +1,212 @@ +/** + * + * File Name: armCOMM_Bitstream.h + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * File: armCOMM_Bitstream.h + * Brief: Declares common API's/Data types used across the OpenMax Encoders/Decoders. + * + */ + +#ifndef _armCodec_H_ +#define _armCodec_H_ + +#include "omxtypes.h" + +typedef struct { + OMX_U8 codeLen; + OMX_U32 codeWord; +} ARM_VLC32; + +/* The above should be renamed as "ARM_VLC32" */ + +/** + * Function: armLookAheadBits() + * + * Description: + * Get the next N bits from the bitstream without advancing the bitstream pointer + * + * Parameters: + * [in] **ppBitStream + * [in] *pOffset + * [in] N=1...32 + * + * Returns Value + */ + +OMX_U32 armLookAheadBits(const OMX_U8 **ppBitStream, OMX_INT *pOffset, OMX_INT N); + +/** + * Function: armGetBits() + * + * Description: + * Read N bits from the bitstream + * + * Parameters: + * [in] *ppBitStream + * [in] *pOffset + * [in] N=1..32 + * + * [out] *ppBitStream + * [out] *pOffset + * Returns Value + */ + +OMX_U32 armGetBits(const OMX_U8 **ppBitStream, OMX_INT *pOffset, OMX_INT N); + +/** + * Function: armByteAlign() + * + * Description: + * Align the pointer *ppBitStream to the next byte boundary + * + * Parameters: + * [in] *ppBitStream + * [in] *pOffset + * + * [out] *ppBitStream + * [out] *pOffset + * + **/ + +OMXVoid armByteAlign(const OMX_U8 **ppBitStream,OMX_INT *pOffset); + +/** + * Function: armSkipBits() + * + * Description: + * Skip N bits from the value at *ppBitStream + * + * Parameters: + * [in] *ppBitStream + * [in] *pOffset + * [in] N + * + * [out] *ppBitStream + * [out] *pOffset + * + **/ + +OMXVoid armSkipBits(const OMX_U8 **ppBitStream,OMX_INT *pOffset,OMX_INT N); + +/*************************************** + * Variable bit length Decode + ***************************************/ + +/** + * Function: armUnPackVLC32() + * + * Description: + * Variable length decode of variable length symbol (max size 32 bits) read from + * the bit stream pointed by *ppBitStream at *pOffset by using the table + * pointed by pCodeBook + * + * Parameters: + * [in] **ppBitStream + * [in] *pOffset + * [in] pCodeBook + * + * [out] **ppBitStream + * [out] *pOffset + * + * Returns : Code Book Index if successfull. + * : "ARM_NO_CODEBOOK_INDEX = 0xFFFF" if search fails. + **/ + +#define ARM_NO_CODEBOOK_INDEX (OMX_U16)(0xFFFF) + +OMX_U16 armUnPackVLC32( + const OMX_U8 **ppBitStream, + OMX_INT *pOffset, + const ARM_VLC32 *pCodeBook +); + +/*************************************** + * Fixed bit length Encode + ***************************************/ + +/** + * Function: armPackBits + * + * Description: + * Pack a VLC code word into the bitstream + * + * Remarks: + * + * Parameters: + * [in] ppBitStream pointer to the pointer to the current byte + * in the bit stream. + * [in] pOffset pointer to the bit position in the byte + * pointed by *ppBitStream. Valid within 0 + * to 7. + * [in] codeWord Code word that need to be inserted in to the + * bitstream + * [in] codeLength Length of the code word valid range 1...32 + * + * [out] ppBitStream *ppBitStream is updated after the block is encoded, + * so that it points to the current byte in the bit + * stream buffer. + * [out] pBitOffset *pBitOffset is updated so that it points to the + * current bit position in the byte pointed by + * *ppBitStream. + * + * Return Value: + * Standard OMX_RESULT result. See enumeration for possible result codes. + * + */ + +OMXResult armPackBits ( + OMX_U8 **ppBitStream, + OMX_INT *pOffset, + OMX_U32 codeWord, + OMX_INT codeLength +); + +/*************************************** + * Variable bit length Encode + ***************************************/ + +/** + * Function: armPackVLC32 + * + * Description: + * Pack a VLC code word into the bitstream + * + * Remarks: + * + * Parameters: + * [in] ppBitStream pointer to the pointer to the current byte + * in the bit stream. + * [in] pBitOffset pointer to the bit position in the byte + * pointed by *ppBitStream. Valid within 0 + * to 7. + * [in] code VLC code word that need to be inserted in to the + * bitstream + * + * [out] ppBitStream *ppBitStream is updated after the block is encoded, + * so that it points to the current byte in the bit + * stream buffer. + * [out] pBitOffset *pBitOffset is updated so that it points to the + * current bit position in the byte pointed by + * *ppBitStream. + * + * Return Value: + * Standard OMX_RESULT result. See enumeration for possible result codes. + * + */ + +OMXResult armPackVLC32 ( + OMX_U8 **ppBitStream, + OMX_INT *pBitOffset, + ARM_VLC32 code +); + +#endif /*_armCodec_H_*/ + +/*End of File*/ diff --git a/src/video/h264_decoder/omxdl/armOMX.h b/src/video/h264_decoder/omxdl/armOMX.h new file mode 100644 index 000000000..f629f72b2 --- /dev/null +++ b/src/video/h264_decoder/omxdl/armOMX.h @@ -0,0 +1,274 @@ +/* + * + * File Name: armOMX_ReleaseVersion.h + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * This file allows a version of the OMX DL libraries to be built where some or + * all of the function names can be given a user specified suffix. + * + * You might want to use it where: + * + * - you want to rename a function "out of the way" so that you could replace + * a function with a different version (the original version would still be + * in the library just with a different name - so you could debug the new + * version by comparing it to the output of the old) + * + * - you want to rename all the functions to versions with a suffix so that + * you can include two versions of the library and choose between functions + * at runtime. + * + * e.g. omxIPBM_Copy_U8_C1R could be renamed omxIPBM_Copy_U8_C1R_CortexA8 + * + */ + + +#ifndef _armOMX_H_ +#define _armOMX_H_ + + +/* We need to define these two macros in order to expand and concatenate the names */ +#define OMXCAT2BAR(A, B) omx ## A ## B +#define OMXCATBAR(A, B) OMXCAT2BAR(A, B) + +/* Define the suffix to add to all functions - the default is no suffix */ +#define BARE_SUFFIX + + + +/* Define what happens to the bare suffix-less functions, down to the sub-domain accuracy */ +#define OMXACAAC_SUFFIX BARE_SUFFIX +#define OMXACMP3_SUFFIX BARE_SUFFIX +#define OMXICJP_SUFFIX BARE_SUFFIX +#define OMXIPBM_SUFFIX BARE_SUFFIX +#define OMXIPCS_SUFFIX BARE_SUFFIX +#define OMXIPPP_SUFFIX BARE_SUFFIX +#define OMXSP_SUFFIX BARE_SUFFIX +#define OMXVCCOMM_SUFFIX BARE_SUFFIX +#define OMXVCM4P10_SUFFIX BARE_SUFFIX +#define OMXVCM4P2_SUFFIX BARE_SUFFIX + + + + +/* Define what the each bare, un-suffixed OpenMAX API function names is to be renamed */ +#define omxACAAC_DecodeChanPairElt OMXCATBAR(ACAAC_DecodeChanPairElt, OMXACAAC_SUFFIX) +#define omxACAAC_DecodeDatStrElt OMXCATBAR(ACAAC_DecodeDatStrElt, OMXACAAC_SUFFIX) +#define omxACAAC_DecodeFillElt OMXCATBAR(ACAAC_DecodeFillElt, OMXACAAC_SUFFIX) +#define omxACAAC_DecodeIsStereo_S32 OMXCATBAR(ACAAC_DecodeIsStereo_S32, OMXACAAC_SUFFIX) +#define omxACAAC_DecodeMsPNS_S32_I OMXCATBAR(ACAAC_DecodeMsPNS_S32_I, OMXACAAC_SUFFIX) +#define omxACAAC_DecodeMsStereo_S32_I OMXCATBAR(ACAAC_DecodeMsStereo_S32_I, OMXACAAC_SUFFIX) +#define omxACAAC_DecodePrgCfgElt OMXCATBAR(ACAAC_DecodePrgCfgElt, OMXACAAC_SUFFIX) +#define omxACAAC_DecodeTNS_S32_I OMXCATBAR(ACAAC_DecodeTNS_S32_I, OMXACAAC_SUFFIX) +#define omxACAAC_DeinterleaveSpectrum_S32 OMXCATBAR(ACAAC_DeinterleaveSpectrum_S32, OMXACAAC_SUFFIX) +#define omxACAAC_EncodeTNS_S32_I OMXCATBAR(ACAAC_EncodeTNS_S32_I, OMXACAAC_SUFFIX) +#define omxACAAC_LongTermPredict_S32 OMXCATBAR(ACAAC_LongTermPredict_S32, OMXACAAC_SUFFIX) +#define omxACAAC_LongTermReconstruct_S32_I OMXCATBAR(ACAAC_LongTermReconstruct_S32_I, OMXACAAC_SUFFIX) +#define omxACAAC_MDCTFwd_S32 OMXCATBAR(ACAAC_MDCTFwd_S32, OMXACAAC_SUFFIX) +#define omxACAAC_MDCTInv_S32_S16 OMXCATBAR(ACAAC_MDCTInv_S32_S16, OMXACAAC_SUFFIX) +#define omxACAAC_NoiselessDecode OMXCATBAR(ACAAC_NoiselessDecode, OMXACAAC_SUFFIX) +#define omxACAAC_QuantInv_S32_I OMXCATBAR(ACAAC_QuantInv_S32_I, OMXACAAC_SUFFIX) +#define omxACAAC_UnpackADIFHeader OMXCATBAR(ACAAC_UnpackADIFHeader, OMXACAAC_SUFFIX) +#define omxACAAC_UnpackADTSFrameHeader OMXCATBAR(ACAAC_UnpackADTSFrameHeader, OMXACAAC_SUFFIX) + + +#define omxACMP3_HuffmanDecode_S32 OMXCATBAR(ACMP3_HuffmanDecode_S32, OMXACMP3_SUFFIX) +#define omxACMP3_HuffmanDecodeSfb_S32 OMXCATBAR(ACMP3_HuffmanDecodeSfb_S32, OMXACMP3_SUFFIX) +#define omxACMP3_HuffmanDecodeSfbMbp_S32 OMXCATBAR(ACMP3_HuffmanDecodeSfbMbp_S32, OMXACMP3_SUFFIX) +#define omxACMP3_MDCTInv_S32 OMXCATBAR(ACMP3_MDCTInv_S32, OMXACMP3_SUFFIX) +#define omxACMP3_ReQuantize_S32_I OMXCATBAR(ACMP3_ReQuantize_S32_I, OMXACMP3_SUFFIX) +#define omxACMP3_ReQuantizeSfb_S32_I OMXCATBAR(ACMP3_ReQuantizeSfb_S32_I, OMXACMP3_SUFFIX) +#define omxACMP3_SynthPQMF_S32_S16 OMXCATBAR(ACMP3_SynthPQMF_S32_S16, OMXACMP3_SUFFIX) +#define omxACMP3_UnpackFrameHeader OMXCATBAR(ACMP3_UnpackFrameHeader, OMXACMP3_SUFFIX) +#define omxACMP3_UnpackScaleFactors_S8 OMXCATBAR(ACMP3_UnpackScaleFactors_S8, OMXACMP3_SUFFIX) +#define omxACMP3_UnpackSideInfo OMXCATBAR(ACMP3_UnpackSideInfo, OMXACMP3_SUFFIX) + +#define omxICJP_CopyExpand_U8_C3 OMXCATBAR(ICJP_CopyExpand_U8_C3, OMXICJP_SUFFIX) +#define omxICJP_DCTFwd_S16 OMXCATBAR(ICJP_DCTFwd_S16, OMXICJP_SUFFIX) +#define omxICJP_DCTFwd_S16_I OMXCATBAR(ICJP_DCTFwd_S16_I, OMXICJP_SUFFIX) +#define omxICJP_DCTInv_S16 OMXCATBAR(ICJP_DCTInv_S16, OMXICJP_SUFFIX) +#define omxICJP_DCTInv_S16_I OMXCATBAR(ICJP_DCTInv_S16_I, OMXICJP_SUFFIX) +#define omxICJP_DCTQuantFwd_Multiple_S16 OMXCATBAR(ICJP_DCTQuantFwd_Multiple_S16, OMXICJP_SUFFIX) +#define omxICJP_DCTQuantFwd_S16 OMXCATBAR(ICJP_DCTQuantFwd_S16, OMXICJP_SUFFIX) +#define omxICJP_DCTQuantFwd_S16_I OMXCATBAR(ICJP_DCTQuantFwd_S16_I, OMXICJP_SUFFIX) +#define omxICJP_DCTQuantFwdTableInit OMXCATBAR(ICJP_DCTQuantFwdTableInit, OMXICJP_SUFFIX) +#define omxICJP_DCTQuantInv_Multiple_S16 OMXCATBAR(ICJP_DCTQuantInv_Multiple_S16, OMXICJP_SUFFIX) +#define omxICJP_DCTQuantInv_S16 OMXCATBAR(ICJP_DCTQuantInv_S16, OMXICJP_SUFFIX) +#define omxICJP_DCTQuantInv_S16_I OMXCATBAR(ICJP_DCTQuantInv_S16_I, OMXICJP_SUFFIX) +#define omxICJP_DCTQuantInvTableInit OMXCATBAR(ICJP_DCTQuantInvTableInit, OMXICJP_SUFFIX) +#define omxICJP_DecodeHuffman8x8_Direct_S16_C1 OMXCATBAR(ICJP_DecodeHuffman8x8_Direct_S16_C1, OMXICJP_SUFFIX) +#define omxICJP_DecodeHuffmanSpecGetBufSize_U8 OMXCATBAR(ICJP_DecodeHuffmanSpecGetBufSize_U8, OMXICJP_SUFFIX) +#define omxICJP_DecodeHuffmanSpecInit_U8 OMXCATBAR(ICJP_DecodeHuffmanSpecInit_U8, OMXICJP_SUFFIX) +#define omxICJP_EncodeHuffman8x8_Direct_S16_U1_C1 OMXCATBAR(ICJP_EncodeHuffman8x8_Direct_S16_U1_C1, OMXICJP_SUFFIX) +#define omxICJP_EncodeHuffmanSpecGetBufSize_U8 OMXCATBAR(ICJP_EncodeHuffmanSpecGetBufSize_U8, OMXICJP_SUFFIX) +#define omxICJP_EncodeHuffmanSpecInit_U8 OMXCATBAR(ICJP_EncodeHuffmanSpecInit_U8, OMXICJP_SUFFIX) + +#define omxIPBM_AddC_U8_C1R_Sfs OMXCATBAR(IPBM_AddC_U8_C1R_Sfs, OMXIPBM_SUFFIX) +#define omxIPBM_Copy_U8_C1R OMXCATBAR(IPBM_Copy_U8_C1R, OMXIPBM_SUFFIX) +#define omxIPBM_Copy_U8_C3R OMXCATBAR(IPBM_Copy_U8_C3R, OMXIPBM_SUFFIX) +#define omxIPBM_Mirror_U8_C1R OMXCATBAR(IPBM_Mirror_U8_C1R, OMXIPBM_SUFFIX) +#define omxIPBM_MulC_U8_C1R_Sfs OMXCATBAR(IPBM_MulC_U8_C1R_Sfs, OMXIPBM_SUFFIX) + +#define omxIPCS_ColorTwistQ14_U8_C3R OMXCATBAR(IPCS_ColorTwistQ14_U8_C3R, OMXIPCS_SUFFIX) +#define omxIPCS_BGR565ToYCbCr420LS_MCU_U16_S16_C3P3R OMXCATBAR(IPCS_BGR565ToYCbCr420LS_MCU_U16_S16_C3P3R, OMXIPCS_SUFFIX) +#define omxIPCS_BGR565ToYCbCr422LS_MCU_U16_S16_C3P3R OMXCATBAR(IPCS_BGR565ToYCbCr422LS_MCU_U16_S16_C3P3R, OMXIPCS_SUFFIX) +#define omxIPCS_BGR565ToYCbCr444LS_MCU_U16_S16_C3P3R OMXCATBAR(IPCS_BGR565ToYCbCr444LS_MCU_U16_S16_C3P3R, OMXIPCS_SUFFIX) +#define omxIPCS_BGR888ToYCbCr420LS_MCU_U8_S16_C3P3R OMXCATBAR(IPCS_BGR888ToYCbCr420LS_MCU_U8_S16_C3P3R, OMXIPCS_SUFFIX) +#define omxIPCS_BGR888ToYCbCr422LS_MCU_U8_S16_C3P3R OMXCATBAR(IPCS_BGR888ToYCbCr422LS_MCU_U8_S16_C3P3R, OMXIPCS_SUFFIX) +#define omxIPCS_BGR888ToYCbCr444LS_MCU_U8_S16_C3P3R OMXCATBAR(IPCS_BGR888ToYCbCr444LS_MCU_U8_S16_C3P3R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbCr420RszCscRotBGR_U8_P3C3R OMXCATBAR(IPCS_YCbCr420RszCscRotBGR_U8_P3C3R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbCr420RszRot_U8_P3R OMXCATBAR(IPCS_YCbCr420RszRot_U8_P3R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbCr420ToBGR565_U8_U16_P3C3R OMXCATBAR(IPCS_YCbCr420ToBGR565_U8_U16_P3C3R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbCr420ToBGR565LS_MCU_S16_U16_P3C3R OMXCATBAR(IPCS_YCbCr420ToBGR565LS_MCU_S16_U16_P3C3R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbCr420ToBGR888LS_MCU_S16_U8_P3C3R OMXCATBAR(IPCS_YCbCr420ToBGR888LS_MCU_S16_U8_P3C3R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbCr422RszCscRotBGR_U8_P3C3R OMXCATBAR(IPCS_YCbCr422RszCscRotBGR_U8_P3C3R, OMXIPCS_SUFFIX) +#define omxIPCS_CbYCrY422RszCscRotBGR_U8_U16_C2R OMXCATBAR(IPCS_CbYCrY422RszCscRotBGR_U8_U16_C2R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbCr422RszRot_U8_P3R OMXCATBAR(IPCS_YCbCr422RszRot_U8_P3R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbYCr422ToBGR565_U8_U16_C2C3R OMXCATBAR(IPCS_YCbYCr422ToBGR565_U8_U16_C2C3R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbCr422ToBGR565LS_MCU_S16_U16_P3C3R OMXCATBAR(IPCS_YCbCr422ToBGR565LS_MCU_S16_U16_P3C3R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbYCr422ToBGR888_U8_C2C3R OMXCATBAR(IPCS_YCbYCr422ToBGR888_U8_C2C3R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbCr422ToBGR888LS_MCU_S16_U8_P3C3R OMXCATBAR(IPCS_YCbCr422ToBGR888LS_MCU_S16_U8_P3C3R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbCr422ToBGR888LS_MCU_S16_U8_P3C3R OMXCATBAR(IPCS_YCbCr422ToBGR888LS_MCU_S16_U8_P3C3R, OMXIPCS_SUFFIX) +#define omxIPCS_CbYCrY422ToYCbCr420Rotate_U8_C2P3R OMXCATBAR(IPCS_CbYCrY422ToYCbCr420Rotate_U8_C2P3R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbCr422ToYCbCr420Rotate_U8_P3R OMXCATBAR(IPCS_YCbCr422ToYCbCr420Rotate_U8_P3R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbCr444ToBGR565_U8_U16_C3R OMXCATBAR(IPCS_YCbCr444ToBGR565_U8_U16_C3R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbCr444ToBGR565_U8_U16_P3C3R OMXCATBAR(IPCS_YCbCr444ToBGR565_U8_U16_P3C3R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbCr444ToBGR565LS_MCU_S16_U16_P3C3R OMXCATBAR(IPCS_YCbCr444ToBGR565LS_MCU_S16_U16_P3C3R, OMXIPCS_SUFFIX) +#define omxIPCS_YCbCr444ToBGR888_U8_C3R OMXCATBAR(IPCS_YCbCr444ToBGR888_U8_C3R, OMXIPCS_SUFFIX) + +#define omxIPPP_Deblock_HorEdge_U8_I OMXCATBAR(IPPP_Deblock_HorEdge_U8_I, OMXIPPP_SUFFIX) +#define omxIPPP_Deblock_VerEdge_U8_I OMXCATBAR(IPPP_Deblock_VerEdge_U8_I, OMXIPPP_SUFFIX) +#define omxIPPP_FilterFIR_U8_C1R OMXCATBAR(IPPP_FilterFIR_U8_C1R, OMXIPPP_SUFFIX) +#define omxIPPP_FilterMedian_U8_C1R OMXCATBAR(IPPP_FilterMedian_U8_C1R, OMXIPPP_SUFFIX) +#define omxIPPP_GetCentralMoment_S64 OMXCATBAR(IPPP_GetCentralMoment_S64, OMXIPPP_SUFFIX) +#define omxIPPP_GetSpatialMoment_S64 OMXCATBAR(IPPP_GetSpatialMoment_S64, OMXIPPP_SUFFIX) +#define omxIPPP_MomentGetStateSize OMXCATBAR(IPPP_MomentGetStateSize, OMXIPPP_SUFFIX) +#define omxIPPP_MomentInit OMXCATBAR(IPPP_MomentInit, OMXIPPP_SUFFIX) +#define omxIPPP_Moments_U8_C1R OMXCATBAR(IPPP_Moments_U8_C1R, OMXIPPP_SUFFIX) +#define omxIPPP_Moments_U8_C3R OMXCATBAR(IPPP_Moments_U8_C3R, OMXIPPP_SUFFIX) + +#define omxSP_BlockExp_S16 OMXCATBAR(SP_BlockExp_S16, OMXSP_SUFFIX) +#define omxSP_BlockExp_S32 OMXCATBAR(SP_BlockExp_S32, OMXSP_SUFFIX) +#define omxSP_Copy_S16 OMXCATBAR(SP_Copy_S16, OMXSP_SUFFIX) +#define omxSP_DotProd_S16 OMXCATBAR(SP_DotProd_S16, OMXSP_SUFFIX) +#define omxSP_DotProd_S16_Sfs OMXCATBAR(SP_DotProd_S16_Sfs, OMXSP_SUFFIX) +#define omxSP_FFTFwd_CToC_SC16_Sfs OMXCATBAR(SP_FFTFwd_CToC_SC16_Sfs, OMXSP_SUFFIX) +#define omxSP_FFTFwd_CToC_SC32_Sfs OMXCATBAR(SP_FFTFwd_CToC_SC32_Sfs, OMXSP_SUFFIX) +#define omxSP_FFTFwd_RToCCS_S16S32_Sfs OMXCATBAR(SP_FFTFwd_RToCCS_S16S32_Sfs, OMXSP_SUFFIX) +#define omxSP_FFTFwd_RToCCS_S32_Sfs OMXCATBAR(SP_FFTFwd_RToCCS_S32_Sfs, OMXSP_SUFFIX) +#define omxSP_FFTGetBufSize_C_SC16 OMXCATBAR(SP_FFTGetBufSize_C_SC16, OMXSP_SUFFIX) +#define omxSP_FFTGetBufSize_C_SC32 OMXCATBAR(SP_FFTGetBufSize_C_SC32, OMXSP_SUFFIX) +#define omxSP_FFTGetBufSize_R_S16S32 OMXCATBAR(SP_FFTGetBufSize_R_S16S32, OMXSP_SUFFIX) +#define omxSP_FFTGetBufSize_R_S32 OMXCATBAR(SP_FFTGetBufSize_R_S32, OMXSP_SUFFIX) +#define omxSP_FFTInit_C_SC16 OMXCATBAR(SP_FFTInit_C_SC16, OMXSP_SUFFIX) +#define omxSP_FFTInit_C_SC32 OMXCATBAR(SP_FFTInit_C_SC32, OMXSP_SUFFIX) +#define omxSP_FFTInit_R_S16S32 OMXCATBAR(SP_FFTInit_R_S16S32, OMXSP_SUFFIX) +#define omxSP_FFTInit_R_S32 OMXCATBAR(SP_FFTInit_R_S32, OMXSP_SUFFIX) +#define omxSP_FFTInv_CCSToR_S32_Sfs OMXCATBAR(SP_FFTInv_CCSToR_S32_Sfs, OMXSP_SUFFIX) +#define omxSP_FFTInv_CCSToR_S32S16_Sfs OMXCATBAR(SP_FFTInv_CCSToR_S32S16_Sfs, OMXSP_SUFFIX) +#define omxSP_FFTInv_CToC_SC16_Sfs OMXCATBAR(SP_FFTInv_CToC_SC16_Sfs, OMXSP_SUFFIX) +#define omxSP_FFTInv_CToC_SC32_Sfs OMXCATBAR(SP_FFTInv_CToC_SC32_Sfs, OMXSP_SUFFIX) +#define omxSP_FilterMedian_S32 OMXCATBAR(SP_FilterMedian_S32, OMXSP_SUFFIX) +#define omxSP_FilterMedian_S32_I OMXCATBAR(SP_FilterMedian_S32_I, OMXSP_SUFFIX) +#define omxSP_FIR_Direct_S16 OMXCATBAR(SP_FIR_Direct_S16, OMXSP_SUFFIX) +#define omxSP_FIR_Direct_S16_I OMXCATBAR(SP_FIR_Direct_S16_I, OMXSP_SUFFIX) +#define omxSP_FIR_Direct_S16_ISfs OMXCATBAR(SP_FIR_Direct_S16_ISfs, OMXSP_SUFFIX) +#define omxSP_FIR_Direct_S16_Sfs OMXCATBAR(SP_FIR_Direct_S16_Sfs, OMXSP_SUFFIX) +#define omxSP_FIROne_Direct_S16 OMXCATBAR(SP_FIROne_Direct_S16, OMXSP_SUFFIX) +#define omxSP_FIROne_Direct_S16_I OMXCATBAR(SP_FIROne_Direct_S16_I, OMXSP_SUFFIX) +#define omxSP_FIROne_Direct_S16_ISfs OMXCATBAR(SP_FIROne_Direct_S16_ISfs, OMXSP_SUFFIX) +#define omxSP_FIROne_Direct_S16_Sfs OMXCATBAR(SP_FIROne_Direct_S16_Sfs, OMXSP_SUFFIX) +#define omxSP_IIR_BiQuadDirect_S16 OMXCATBAR(SP_IIR_BiQuadDirect_S16, OMXSP_SUFFIX) +#define omxSP_IIR_BiQuadDirect_S16_I OMXCATBAR(SP_IIR_BiQuadDirect_S16_I, OMXSP_SUFFIX) +#define omxSP_IIR_Direct_S16 OMXCATBAR(SP_IIR_Direct_S16, OMXSP_SUFFIX) +#define omxSP_IIR_Direct_S16_I OMXCATBAR(SP_IIR_Direct_S16_I, OMXSP_SUFFIX) +#define omxSP_IIROne_BiQuadDirect_S16 OMXCATBAR(SP_IIROne_BiQuadDirect_S16, OMXSP_SUFFIX) +#define omxSP_IIROne_BiQuadDirect_S16_I OMXCATBAR(SP_IIROne_BiQuadDirect_S16_I, OMXSP_SUFFIX) +#define omxSP_IIROne_Direct_S16 OMXCATBAR(SP_IIROne_Direct_S16, OMXSP_SUFFIX) +#define omxSP_IIROne_Direct_S16_I OMXCATBAR(SP_IIROne_Direct_S16_I, OMXSP_SUFFIX) + +#define omxVCCOMM_Average_16x OMXCATBAR(VCCOMM_Average_16x, OMXVCCOMM_SUFFIX) +#define omxVCCOMM_Average_8x OMXCATBAR(VCCOMM_Average_8x, OMXVCCOMM_SUFFIX) +#define omxVCCOMM_ComputeTextureErrorBlock OMXCATBAR(VCCOMM_ComputeTextureErrorBlock, OMXVCCOMM_SUFFIX) +#define omxVCCOMM_ComputeTextureErrorBlock_SAD OMXCATBAR(VCCOMM_ComputeTextureErrorBlock_SAD, OMXVCCOMM_SUFFIX) +#define omxVCCOMM_Copy16x16 OMXCATBAR(VCCOMM_Copy16x16, OMXVCCOMM_SUFFIX) +#define omxVCCOMM_Copy8x8 OMXCATBAR(VCCOMM_Copy8x8, OMXVCCOMM_SUFFIX) +#define omxVCCOMM_ExpandFrame_I OMXCATBAR(VCCOMM_ExpandFrame_I, OMXVCCOMM_SUFFIX) +#define omxVCCOMM_LimitMVToRect OMXCATBAR(VCCOMM_LimitMVToRect, OMXVCCOMM_SUFFIX) +#define omxVCCOMM_SAD_16x OMXCATBAR(VCCOMM_SAD_16x, OMXVCCOMM_SUFFIX) +#define omxVCCOMM_SAD_8x OMXCATBAR(VCCOMM_SAD_8x, OMXVCCOMM_SUFFIX) + +#define omxVCM4P10_Average_4x OMXCATBAR(VCM4P10_Average_4x, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_BlockMatch_Half OMXCATBAR(VCM4P10_BlockMatch_Half, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_BlockMatch_Integer OMXCATBAR(VCM4P10_BlockMatch_Integer, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_BlockMatch_Quarter OMXCATBAR(VCM4P10_BlockMatch_Quarter, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_DeblockChroma_I OMXCATBAR(VCM4P10_DeblockChroma_I, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_DeblockLuma_I OMXCATBAR(VCM4P10_DeblockLuma_I, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_DecodeChromaDcCoeffsToPairCAVLC OMXCATBAR(VCM4P10_DecodeChromaDcCoeffsToPairCAVLC, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_DecodeCoeffsToPairCAVLC OMXCATBAR(VCM4P10_DecodeCoeffsToPairCAVLC, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_DequantTransformResidualFromPairAndAdd OMXCATBAR(VCM4P10_DequantTransformResidualFromPairAndAdd, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_FilterDeblockingChroma_HorEdge_I OMXCATBAR(VCM4P10_FilterDeblockingChroma_HorEdge_I, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_FilterDeblockingChroma_VerEdge_I OMXCATBAR(VCM4P10_FilterDeblockingChroma_VerEdge_I, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_FilterDeblockingLuma_HorEdge_I OMXCATBAR(VCM4P10_FilterDeblockingLuma_HorEdge_I, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_FilterDeblockingLuma_VerEdge_I OMXCATBAR(VCM4P10_FilterDeblockingLuma_VerEdge_I, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_GetVLCInfo OMXCATBAR(VCM4P10_GetVLCInfo, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_InterpolateChroma OMXCATBAR(VCM4P10_InterpolateChroma, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_InterpolateHalfHor_Luma OMXCATBAR(VCM4P10_InterpolateHalfHor_Luma, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_InterpolateHalfVer_Luma OMXCATBAR(VCM4P10_InterpolateHalfVer_Luma, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_InterpolateLuma OMXCATBAR(VCM4P10_InterpolateLuma, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_InvTransformDequant_ChromaDC OMXCATBAR(VCM4P10_InvTransformDequant_ChromaDC, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_InvTransformDequant_LumaDC OMXCATBAR(VCM4P10_InvTransformDequant_LumaDC, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_InvTransformResidualAndAdd OMXCATBAR(VCM4P10_InvTransformResidualAndAdd, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_MEGetBufSize OMXCATBAR(VCM4P10_MEGetBufSize, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_MEInit OMXCATBAR(VCM4P10_MEInit, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_MotionEstimationMB OMXCATBAR(VCM4P10_MotionEstimationMB, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_PredictIntra_16x16 OMXCATBAR(VCM4P10_PredictIntra_16x16, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_PredictIntra_4x4 OMXCATBAR(VCM4P10_PredictIntra_4x4, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_PredictIntraChroma_8x8 OMXCATBAR(VCM4P10_PredictIntraChroma_8x8, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_SAD_4x OMXCATBAR(VCM4P10_SAD_4x, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_SADQuar_16x OMXCATBAR(VCM4P10_SADQuar_16x, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_SADQuar_4x OMXCATBAR(VCM4P10_SADQuar_4x, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_SADQuar_8x OMXCATBAR(VCM4P10_SADQuar_8x, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_SATD_4x4 OMXCATBAR(VCM4P10_SATD_4x4, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_SubAndTransformQDQResidual OMXCATBAR(VCM4P10_SubAndTransformQDQResidual, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_TransformDequantChromaDCFromPair OMXCATBAR(VCM4P10_TransformDequantChromaDCFromPair, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_TransformDequantLumaDCFromPair OMXCATBAR(VCM4P10_TransformDequantLumaDCFromPair, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_TransformQuant_ChromaDC OMXCATBAR(VCM4P10_TransformQuant_ChromaDC, OMXVCM4P10_SUFFIX) +#define omxVCM4P10_TransformQuant_LumaDC OMXCATBAR(VCM4P10_TransformQuant_LumaDC, OMXVCM4P10_SUFFIX) + +#define omxVCM4P2_BlockMatch_Half_16x16 OMXCATBAR(VCM4P2_BlockMatch_Half_16x16, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_BlockMatch_Half_8x8 OMXCATBAR(VCM4P2_BlockMatch_Half_8x8, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_BlockMatch_Integer_16x16 OMXCATBAR(VCM4P2_BlockMatch_Integer_16x16, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_BlockMatch_Integer_8x8 OMXCATBAR(VCM4P2_BlockMatch_Integer_8x8, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_DCT8x8blk OMXCATBAR(VCM4P2_DCT8x8blk, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_DecodeBlockCoef_Inter OMXCATBAR(VCM4P2_DecodeBlockCoef_Inter, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_DecodeBlockCoef_Intra OMXCATBAR(VCM4P2_DecodeBlockCoef_Intra, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_DecodePadMV_PVOP OMXCATBAR(VCM4P2_DecodePadMV_PVOP, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_DecodeVLCZigzag_Inter OMXCATBAR(VCM4P2_DecodeVLCZigzag_Inter, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_DecodeVLCZigzag_IntraACVLC OMXCATBAR(VCM4P2_DecodeVLCZigzag_IntraACVLC, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_DecodeVLCZigzag_IntraDCVLC OMXCATBAR(VCM4P2_DecodeVLCZigzag_IntraDCVLC, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_EncodeMV OMXCATBAR(VCM4P2_EncodeMV, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_EncodeVLCZigzag_Inter OMXCATBAR(VCM4P2_EncodeVLCZigzag_Inter, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_EncodeVLCZigzag_IntraACVLC OMXCATBAR(VCM4P2_EncodeVLCZigzag_IntraACVLC, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_EncodeVLCZigzag_IntraDCVLC OMXCATBAR(VCM4P2_EncodeVLCZigzag_IntraDCVLC, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_FindMVpred OMXCATBAR(VCM4P2_FindMVpred, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_IDCT8x8blk OMXCATBAR(VCM4P2_IDCT8x8blk, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_MCReconBlock OMXCATBAR(VCM4P2_MCReconBlock, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_MEGetBufSize OMXCATBAR(VCM4P2_MEGetBufSize, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_MEInit OMXCATBAR(VCM4P2_MEInit, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_MotionEstimationMB OMXCATBAR(VCM4P2_MotionEstimationMB, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_PredictReconCoefIntra OMXCATBAR(VCM4P2_PredictReconCoefIntra, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_QuantInter_I OMXCATBAR(VCM4P2_QuantInter_I, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_QuantIntra_I OMXCATBAR(VCM4P2_QuantIntra_I, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_QuantInvInter_I OMXCATBAR(VCM4P2_QuantInvInter_I, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_QuantInvIntra_I OMXCATBAR(VCM4P2_QuantInvIntra_I, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_TransRecBlockCoef_inter OMXCATBAR(VCM4P2_TransRecBlockCoef_inter, OMXVCM4P2_SUFFIX) +#define omxVCM4P2_TransRecBlockCoef_intra OMXCATBAR(VCM4P2_TransRecBlockCoef_intra, OMXVCM4P2_SUFFIX) + + +#endif /* _armOMX_h_ */ diff --git a/src/video/h264_decoder/omxdl/armVC.h b/src/video/h264_decoder/omxdl/armVC.h new file mode 100644 index 000000000..06b4f331c --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVC.h @@ -0,0 +1,1158 @@ +/** + * + * File Name: armVC.h + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * File: armVideo.h + * Brief: Declares API's/Basic Data types used across the OpenMAX Video domain + * + */ + + +#ifndef _armVideo_H_ +#define _armVideo_H_ + +#include "omxVC.h" +#include "armCOMM_Bitstream.h" + +/** + * ARM specific state structure to hold Motion Estimation information. + */ + +struct m4p2_MESpec +{ + OMXVCM4P2MEParams MEParams; + OMXVCM4P2MEMode MEMode; +}; + +struct m4p10_MESpec +{ + OMXVCM4P10MEParams MEParams; + OMXVCM4P10MEMode MEMode; +}; + +typedef struct m4p2_MESpec ARMVCM4P2_MESpec; +typedef struct m4p10_MESpec ARMVCM4P10_MESpec; + +/** + * Function: armVCM4P2_CompareMV + * + * Description: + * Performs comparision of motion vectors and SAD's to decide the + * best MV and SAD + * + * Remarks: + * + * Parameters: + * [in] mvX x coordinate of the candidate motion vector + * [in] mvY y coordinate of the candidate motion vector + * [in] candSAD Candidate SAD + * [in] bestMVX x coordinate of the best motion vector + * [in] bestMVY y coordinate of the best motion vector + * [in] bestSAD best SAD + * + * Return Value: + * OMX_INT -- 1 to indicate that the current sad is the best + * 0 to indicate that it is NOT the best SAD + */ +__attribute__((weak)) +OMX_INT armVCM4P2_CompareMV ( + OMX_S16 mvX, + OMX_S16 mvY, + OMX_INT candSAD, + OMX_S16 bestMVX, + OMX_S16 bestMVY, + OMX_INT bestSAD); + +/** + * Function: armVCM4P2_ACDCPredict + * + * Description: + * Performs adaptive DC/AC coefficient prediction for an intra block. Prior + * to the function call, prediction direction (predDir) should be selected + * as specified in subclause 7.4.3.1 of ISO/IEC 14496-2. + * + * Remarks: + * + * Parameters: + * [in] pSrcDst pointer to the coefficient buffer which contains + * the quantized coefficient residuals (PQF) of the + * current block + * [in] pPredBufRow pointer to the coefficient row buffer + * [in] pPredBufCol pointer to the coefficient column buffer + * [in] curQP quantization parameter of the current block. curQP + * may equal to predQP especially when the current + * block and the predictor block are in the same + * macroblock. + * [in] predQP quantization parameter of the predictor block + * [in] predDir indicates the prediction direction which takes one + * of the following values: + * OMX_VIDEO_HORIZONTAL predict horizontally + * OMX_VIDEO_VERTICAL predict vertically + * [in] ACPredFlag a flag indicating if AC prediction should be + * performed. It is equal to ac_pred_flag in the bit + * stream syntax of MPEG-4 + * [in] videoComp video component type (luminance, chrominance or + * alpha) of the current block + * [in] flag This flag defines the if one wants to use this functions to + * calculate PQF (set 1, prediction) or QF (set 0, reconstruction) + * [out] pPreACPredict pointer to the predicted coefficients buffer. + * Filled ONLY if it is not NULL + * [out] pSrcDst pointer to the coefficient buffer which contains + * the quantized coefficients (QF) of the current + * block + * [out] pPredBufRow pointer to the updated coefficient row buffer + * [out] pPredBufCol pointer to the updated coefficient column buffer + * [out] pSumErr pointer to the updated sum of the difference + * between predicted and unpredicted coefficients + * If this is NULL, do not update + * + * Return Value: + * Standard OMXResult result. See enumeration for possible result codes. + * + */ + __attribute__((weak)) +OMXResult armVCM4P2_ACDCPredict( + OMX_S16 * pSrcDst, + OMX_S16 * pPreACPredict, + OMX_S16 * pPredBufRow, + OMX_S16 * pPredBufCol, + OMX_INT curQP, + OMX_INT predQP, + OMX_INT predDir, + OMX_INT ACPredFlag, + OMXVCM4P2VideoComponent videoComp, + OMX_U8 flag, + OMX_INT *pSumErr +); + +/** + * Function: armVCM4P2_SetPredDir + * + * Description: + * Performs detecting the prediction direction + * + * Remarks: + * + * Parameters: + * [in] blockIndex block index indicating the component type and + * position as defined in subclause 6.1.3.8, of ISO/IEC + * 14496-2. Furthermore, indexes 6 to 9 indicate the + * alpha blocks spatially corresponding to luminance + * blocks 0 to 3 in the same macroblock. + * [in] pCoefBufRow pointer to the coefficient row buffer + * [in] pQpBuf pointer to the quantization parameter buffer + * [out] predQP quantization parameter of the predictor block + * [out] predDir indicates the prediction direction which takes one + * of the following values: + * OMX_VIDEO_HORIZONTAL predict horizontally + * OMX_VIDEO_VERTICAL predict vertically + * + * Return Value: + * Standard OMXResult result. See enumeration for possible result codes. + * + */ + __attribute__((weak)) +OMXResult armVCM4P2_SetPredDir( + OMX_INT blockIndex, + OMX_S16 *pCoefBufRow, + OMX_S16 *pCoefBufCol, + OMX_INT *predDir, + OMX_INT *predQP, + const OMX_U8 *pQpBuf +); + +/** + * Function: armVCM4P2_EncodeVLCZigzag_Intra + * + * Description: + * Performs zigzag scanning and VLC encoding for one intra block. + * + * Remarks: + * + * Parameters: + * [in] ppBitStream pointer to the pointer to the current byte in + * the bit stream + * [in] pBitOffset pointer to the bit position in the byte pointed + * by *ppBitStream. Valid within 0 to 7. + * [in] pQDctBlkCoef pointer to the quantized DCT coefficient + * [in] predDir AC prediction direction, which is used to decide + * the zigzag scan pattern. This takes one of the + * following values: + * OMX_VIDEO_NONE AC prediction not used. + * Performs classical zigzag + * scan. + * OMX_VIDEO_HORIZONTAL Horizontal prediction. + * Performs alternate-vertical + * zigzag scan. + * OMX_VIDEO_VERTICAL Vertical prediction. + * Performs alternate-horizontal + * zigzag scan. + * [in] pattern block pattern which is used to decide whether + * this block is encoded + * [in] start start indicates whether the encoding begins with 0th element + * or 1st. + * [out] ppBitStream *ppBitStream is updated after the block is encoded, + * so that it points to the current byte in the bit + * stream buffer. + * [out] pBitOffset *pBitOffset is updated so that it points to the + * current bit position in the byte pointed by + * *ppBitStream. + * + * Return Value: + * Standard OMXResult result. See enumeration for possible result codes. + * + */ + __attribute__((weak)) +OMXResult armVCM4P2_EncodeVLCZigzag_Intra( + OMX_U8 **ppBitStream, + OMX_INT *pBitOffset, + const OMX_S16 *pQDctBlkCoef, + OMX_U8 predDir, + OMX_U8 pattern, + OMX_INT shortVideoHeader, + OMX_U8 start +); + +/** + * Function: armVCM4P2_DecodeVLCZigzag_Intra + * + * Description: + * Performs VLC decoding and inverse zigzag scan for one intra coded block. + * + * Remarks: + * + * Parameters: + * [in] ppBitStream pointer to the pointer to the current byte in + * the bitstream buffer + * [in] pBitOffset pointer to the bit position in the byte pointed + * to by *ppBitStream. *pBitOffset is valid within + * [0-7]. + * [in] predDir AC prediction direction which is used to decide + * the zigzag scan pattern. It takes one of the + * following values: + * OMX_VIDEO_NONE AC prediction not used; + * perform classical zigzag scan; + * OMX_VIDEO_HORIZONTAL Horizontal prediction; + * perform alternate-vertical + * zigzag scan; + * OMX_VIDEO_VERTICAL Vertical prediction; + * thus perform + * alternate-horizontal + * zigzag scan. + * [in] videoComp video component type (luminance, chrominance or + * alpha) of the current block + * [in] shortVideoHeader binary flag indicating presence of short_video_header; escape modes 0-3 are used if shortVideoHeader==0, + * and escape mode 4 is used when shortVideoHeader==1. + * [in] start start indicates whether the encoding begins with 0th element + * or 1st. + * [out] ppBitStream *ppBitStream is updated after the block is + * decoded, so that it points to the current byte + * in the bit stream buffer + * [out] pBitOffset *pBitOffset is updated so that it points to the + * current bit position in the byte pointed by + * *ppBitStream + * [out] pDst pointer to the coefficient buffer of current + * block. Should be 32-bit aligned + * + * Return Value: + * Standard OMXResult result. See enumeration for possible result codes. + * + */ + __attribute__((weak)) +OMXResult armVCM4P2_DecodeVLCZigzag_Intra( + const OMX_U8 ** ppBitStream, + OMX_INT * pBitOffset, + OMX_S16 * pDst, + OMX_U8 predDir, + OMX_INT shortVideoHeader, + OMX_U8 start +); + +/** + * Function: armVCM4P2_FillVLDBuffer + * + * Description: + * Performs filling of the coefficient buffer according to the run, level + * and sign, also updates the index + * + * Parameters: + * [in] storeRun Stored Run value (count of zeros) + * [in] storeLevel Stored Level value (non-zero value) + * [in] sign Flag indicating the sign of level + * [in] last status of the last flag + * [in] pIndex pointer to coefficient index in 8x8 matrix + * [out] pIndex pointer to updated coefficient index in 8x8 + * matrix + * [in] pZigzagTable pointer to the zigzag tables + * [out] pDst pointer to the coefficient buffer of current + * block. Should be 32-bit aligned + * Return Value: + * Standard OMXResult result. See enumeration for possible result codes. + * + */ + __attribute__((weak)) +OMXResult armVCM4P2_FillVLDBuffer( + OMX_U32 storeRun, + OMX_S16 * pDst, + OMX_S16 storeLevel, + OMX_U8 sign, + OMX_U8 last, + OMX_U8 * index, + const OMX_U8 * pZigzagTable +); + +/** + * Function: armVCM4P2_GetVLCBits + * + * Description: + * Performs escape mode decision based on the run, run+, level, level+ and + * last combinations. + * + * Remarks: + * + * Parameters: + * [in] ppBitStream pointer to the pointer to the current byte in + * the bit stream + * [in] pBitOffset pointer to the bit position in the byte pointed + * by *ppBitStream. Valid within 0 to 7 + * [in] shortVideoHeader binary flag indicating presence of short_video_header; escape modes 0-3 are used if shortVideoHeader==0, + * and escape mode 4 is used when shortVideoHeader==1. + * [in] start start indicates whether the encoding begins with + * 0th element or 1st. + * [in/out] pLast pointer to last status flag + * [in] runBeginSingleLevelEntriesL0 The run value from which level + * will be equal to 1: last == 0 + * [in] IndexBeginSingleLevelEntriesL0 Array index in the VLC table + * pointing to the + * runBeginSingleLevelEntriesL0 + * [in] runBeginSingleLevelEntriesL1 The run value from which level + * will be equal to 1: last == 1 + * [in] IndexBeginSingleLevelEntriesL1 Array index in the VLC table + * pointing to the + * runBeginSingleLevelEntriesL0 + * [in] pRunIndexTableL0 Run Index table defined in + * armVCM4P2_Huff_Tables_VLC.c for last == 0 + * [in] pVlcTableL0 VLC table for last == 0 + * [in] pRunIndexTableL1 Run Index table defined in + * armVCM4P2_Huff_Tables_VLC.c for last == 1 + * [in] pVlcTableL1 VLC table for last == 1 + * [in] pLMAXTableL0 Level MAX table defined in + * armVCM4P2_Huff_Tables_VLC.c for last == 0 + * [in] pLMAXTableL1 Level MAX table defined in + * armVCM4P2_Huff_Tables_VLC.c for last == 1 + * [in] pRMAXTableL0 Run MAX table defined in + * armVCM4P2_Huff_Tables_VLC.c for last == 0 + * [in] pRMAXTableL1 Run MAX table defined in + * armVCM4P2_Huff_Tables_VLC.c for last == 1 + * [out]pDst pointer to the coefficient buffer of current + * block. Should be 32-bit aligned + * + * Return Value: + * Standard OMXResult result. See enumeration for possible result codes. + * + */ + __attribute__((weak)) +OMXResult armVCM4P2_GetVLCBits ( + const OMX_U8 **ppBitStream, + OMX_INT * pBitOffset, + OMX_S16 * pDst, + OMX_INT shortVideoHeader, + OMX_U8 start, + OMX_U8 * pLast, + OMX_U8 runBeginSingleLevelEntriesL0, + OMX_U8 maxIndexForMultipleEntriesL0, + OMX_U8 maxRunForMultipleEntriesL1, + OMX_U8 maxIndexForMultipleEntriesL1, + const OMX_U8 * pRunIndexTableL0, + const ARM_VLC32 *pVlcTableL0, + const OMX_U8 * pRunIndexTableL1, + const ARM_VLC32 *pVlcTableL1, + const OMX_U8 * pLMAXTableL0, + const OMX_U8 * pLMAXTableL1, + const OMX_U8 * pRMAXTableL0, + const OMX_U8 * pRMAXTableL1, + const OMX_U8 * pZigzagTable +); + +/** + * Function: armVCM4P2_PutVLCBits + * + * Description: + * Checks the type of Escape Mode and put encoded bits for + * quantized DCT coefficients. + * + * Remarks: + * + * Parameters: + * [in] ppBitStream pointer to the pointer to the current byte in + * the bit stream + * [in] pBitOffset pointer to the bit position in the byte pointed + * by *ppBitStream. Valid within 0 to 7 + * [in] shortVideoHeader binary flag indicating presence of short_video_header; escape modes 0-3 are used if shortVideoHeader==0, + * and escape mode 4 is used when shortVideoHeader==1. + * [in] start start indicates whether the encoding begins with + * 0th element or 1st. + * [in] maxStoreRunL0 Max store possible (considering last and inter/intra) + * for last = 0 + * [in] maxStoreRunL1 Max store possible (considering last and inter/intra) + * for last = 1 + * [in] maxRunForMultipleEntriesL0 + * The run value after which level + * will be equal to 1: + * (considering last and inter/intra status) for last = 0 + * [in] maxRunForMultipleEntriesL1 + * The run value after which level + * will be equal to 1: + * (considering last and inter/intra status) for last = 1 + * [in] pRunIndexTableL0 Run Index table defined in + * armVCM4P2_Huff_Tables_VLC.c for last == 0 + * [in] pVlcTableL0 VLC table for last == 0 + * [in] pRunIndexTableL1 Run Index table defined in + * armVCM4P2_Huff_Tables_VLC.c for last == 1 + * [in] pVlcTableL1 VLC table for last == 1 + * [in] pLMAXTableL0 Level MAX table defined in + * armVCM4P2_Huff_Tables_VLC.c for last == 0 + * [in] pLMAXTableL1 Level MAX table defined in + * armVCM4P2_Huff_Tables_VLC.c for last == 1 + * [in] pRMAXTableL0 Run MAX table defined in + * armVCM4P2_Huff_Tables_VLC.c for last == 0 + * [in] pRMAXTableL1 Run MAX table defined in + * armVCM4P2_Huff_Tables_VLC.c for last == 1 + * [out] pQDctBlkCoef pointer to the quantized DCT coefficient + * [out] ppBitStream *ppBitStream is updated after the block is encoded + * so that it points to the current byte in the bit + * stream buffer. + * [out] pBitOffset *pBitOffset is updated so that it points to the + * current bit position in the byte pointed by + * *ppBitStream. + * + * Return Value: + * Standard OMXResult result. See enumeration for possible result codes. + * + */ + + __attribute__((weak)) +OMXResult armVCM4P2_PutVLCBits ( + OMX_U8 **ppBitStream, + OMX_INT * pBitOffset, + const OMX_S16 *pQDctBlkCoef, + OMX_INT shortVideoHeader, + OMX_U8 start, + OMX_U8 maxStoreRunL0, + OMX_U8 maxStoreRunL1, + OMX_U8 maxRunForMultipleEntriesL0, + OMX_U8 maxRunForMultipleEntriesL1, + const OMX_U8 * pRunIndexTableL0, + const ARM_VLC32 *pVlcTableL0, + const OMX_U8 * pRunIndexTableL1, + const ARM_VLC32 *pVlcTableL1, + const OMX_U8 * pLMAXTableL0, + const OMX_U8 * pLMAXTableL1, + const OMX_U8 * pRMAXTableL0, + const OMX_U8 * pRMAXTableL1, + const OMX_U8 * pZigzagTable +); +/** + * Function: armVCM4P2_FillVLCBuffer + * + * Description: + * Performs calculating the VLC bits depending on the escape type and insert + * the same in the bitstream + * + * Remarks: + * + * Parameters: + * [in] ppBitStream pointer to the pointer to the current byte in + * the bit stream + * [in] pBitOffset pointer to the bit position in the byte pointed + * by *ppBitStream. Valid within 0 to 7 + * [in] run Run value (count of zeros) to be encoded + * [in] level Level value (non-zero value) to be encoded + * [in] runPlus Calculated as runPlus = run - (RMAX + 1) + * [in] levelPlus Calculated as + * levelPlus = sign(level)*[abs(level) - LMAX] + * [in] fMode Flag indicating the escape modes + * [in] last status of the last flag + * [in] maxRunForMultipleEntries + * The run value after which level will be equal to 1: + * (considering last and inter/intra status) + * [in] pRunIndexTable Run Index table defined in + * armVCM4P2_Huff_tables_VLC.h + * [in] pVlcTable VLC table defined in armVCM4P2_Huff_tables_VLC.h + * [out] ppBitStream *ppBitStream is updated after the block is encoded + * so that it points to the current byte in the bit + * stream buffer. + * [out] pBitOffset *pBitOffset is updated so that it points to the + * current bit position in the byte pointed by + * *ppBitStream. + * + * Return Value: + * Standard OMXResult result. See enumeration for possible result codes. + * + */ + __attribute__((weak)) +OMXResult armVCM4P2_FillVLCBuffer ( + OMX_U8 **ppBitStream, + OMX_INT * pBitOffset, + OMX_U32 run, + OMX_S16 level, + OMX_U32 runPlus, + OMX_S16 levelPlus, + OMX_U8 fMode, + OMX_U8 last, + OMX_U8 maxRunForMultipleEntries, + const OMX_U8 *pRunIndexTable, + const ARM_VLC32 *pVlcTable +); + +/** + * Function: armVCM4P2_CheckVLCEscapeMode + * + * Description: + * Performs escape mode decision based on the run, run+, level, level+ and + * last combinations. + * + * Remarks: + * + * Parameters: + * [in] run Run value (count of zeros) to be encoded + * [in] level Level value (non-zero value) to be encoded + * [in] runPlus Calculated as runPlus = run - (RMAX + 1) + * [in] levelPlus Calculated as + * levelPlus = sign(level)*[abs(level) - LMAX] + * [in] maxStoreRun Max store possible (considering last and inter/intra) + * [in] maxRunForMultipleEntries + * The run value after which level + * will be equal to 1: + * (considering last and inter/intra status) + * [in] shortVideoHeader binary flag indicating presence of short_video_header; escape modes 0-3 are used if shortVideoHeader==0, + * and escape mode 4 is used when shortVideoHeader==1. + * [in] pRunIndexTable Run Index table defined in + * armVCM4P2_Huff_Tables_VLC.c + * (considering last and inter/intra status) + * + * + * Return Value: + * Returns an Escape mode which can take values from 0 to 3 + * 0 --> no escape mode, 1 --> escape type 1, + * 1 --> escape type 2, 3 --> escape type 3, check section 7.4.1.3 + * in the MPEG ISO standard. + * + */ + __attribute__((weak)) +OMX_U8 armVCM4P2_CheckVLCEscapeMode( + OMX_U32 run, + OMX_U32 runPlus, + OMX_S16 level, + OMX_S16 levelPlus, + OMX_U8 maxStoreRun, + OMX_U8 maxRunForMultipleEntries, + OMX_INT shortVideoHeader, + const OMX_U8 *pRunIndexTable +); + + +/** + * Function: armVCM4P2_BlockMatch_Integer + * + * Description: + * Performs a 16x16 block search; estimates motion vector and associated minimum SAD. + * Both the input and output motion vectors are represented using half-pixel units, and + * therefore a shift left or right by 1 bit may be required, respectively, to match the + * input or output MVs with other functions that either generate output MVs or expect + * input MVs represented using integer pixel units. + * + * Remarks: + * + * Parameters: + * [in] pSrcRefBuf pointer to the reference Y plane; points to the reference MB that + * corresponds to the location of the current macroblock in the current + * plane. + * [in] refWidth width of the reference plane + * [in] pRefRect pointer to the valid rectangular in reference plane. Relative to image origin. + * It's not limited to the image boundary, but depended on the padding. For example, + * if you pad 4 pixels outside the image border, then the value for left border + * can be -4 + * [in] pSrcCurrBuf pointer to the current macroblock extracted from original plane (linear array, + * 256 entries); must be aligned on an 8-byte boundary. + * [in] pCurrPointPos position of the current macroblock in the current plane + * [in] pSrcPreMV pointer to predicted motion vector; NULL indicates no predicted MV + * [in] pSrcPreSAD pointer to SAD associated with the predicted MV (referenced by pSrcPreMV) + * [in] searchRange search range for 16X16 integer block,the units of it is full pixel,the search range + * is the same in all directions.It is in inclusive of the boundary and specified in + * terms of integer pixel units. + * [in] pMESpec vendor-specific motion estimation specification structure; must have been allocated + * and then initialized using omxVCM4P2_MEInit prior to calling the block matching + * function. + * [in] BlockSize MacroBlock Size i.e either 16x16 or 8x8. + * [out] pDstMV pointer to estimated MV + * [out] pDstSAD pointer to minimum SAD + * + * Return Value: + * OMX_Sts_NoErr �C no error. + * OMX_Sts_BadArgErr �C bad arguments + * + */ + __attribute__((weak)) +OMXResult armVCM4P2_BlockMatch_Integer( + const OMX_U8 *pSrcRefBuf, + OMX_INT refWidth, + const OMXRect *pRefRect, + const OMX_U8 *pSrcCurrBuf, + const OMXVCM4P2Coordinate *pCurrPointPos, + const OMXVCMotionVector *pSrcPreMV, + const OMX_INT *pSrcPreSAD, + void *pMESpec, + OMXVCMotionVector *pDstMV, + OMX_INT *pDstSAD, + OMX_U8 BlockSize +); + +/** + * Function: armVCM4P2_BlockMatch_Half + * + * Description: + * Performs a 16x16 block match with half-pixel resolution. Returns the estimated + * motion vector and associated minimum SAD. This function estimates the half-pixel + * motion vector by interpolating the integer resolution motion vector referenced + * by the input parameter pSrcDstMV, i.e., the initial integer MV is generated + * externally. The input parameters pSrcRefBuf and pSearchPointRefPos should be + * shifted by the winning MV of 16x16 integer search prior to calling BlockMatch_Half_16x16. + * The function BlockMatch_Integer_16x16 may be used for integer motion estimation. + * + * Remarks: + * + * Parameters: + * [in] pSrcRefBuf pointer to the reference Y plane; points to the reference MB + * that corresponds to the location of the current macroblock in + * the current plane. + * [in] refWidth width of the reference plane + * [in] pRefRect reference plane valid region rectangle + * [in] pSrcCurrBuf pointer to the current macroblock extracted from original plane + * (linear array, 256 entries); must be aligned on an 8-byte boundary. + * [in] pSearchPointRefPos position of the starting point for half pixel search (specified + * in terms of integer pixel units) in the reference plane. + * [in] rndVal rounding control bit for half pixel motion estimation; + * 0=rounding control disabled; 1=rounding control enabled + * [in] pSrcDstMV pointer to the initial MV estimate; typically generated during a prior + * 16X16 integer search and its unit is half pixel. + * [in] BlockSize MacroBlock Size i.e either 16x16 or 8x8. + * [out]pSrcDstMV pointer to estimated MV + * [out]pDstSAD pointer to minimum SAD + * + * Return Value: + * OMX_Sts_NoErr �C no error + * OMX_Sts_BadArgErr �C bad arguments + * + */ + __attribute__((weak)) +OMXResult armVCM4P2_BlockMatch_Half( + const OMX_U8 *pSrcRefBuf, + OMX_INT refWidth, + const OMXRect *pRefRect, + const OMX_U8 *pSrcCurrBuf, + const OMXVCM4P2Coordinate *pSearchPointRefPos, + OMX_INT rndVal, + OMXVCMotionVector *pSrcDstMV, + OMX_INT *pDstSAD, + OMX_U8 BlockSize +); +/** + * Function: armVCM4P2_PadMV + * + * Description: + * Performs motion vector padding for a macroblock. + * + * Remarks: + * + * Parameters: + * [in] pSrcDstMV pointer to motion vector buffer of the current + * macroblock + * [in] pTransp pointer to transparent status buffer of the + * current macroblock + * [out] pSrcDstMV pointer to motion vector buffer in which the + * motion vectors have been padded + * Return Value: + * Standard OMXResult result. See enumeration for possible result codes. + * + */ + __attribute__((weak)) +OMXResult armVCM4P2_PadMV( + OMXVCMotionVector * pSrcDstMV, + OMX_U8 * pTransp +); + +/* + * H.264 Specific Declarations + */ +/* Defines */ +#define ARM_M4P10_Q_OFFSET (15) + + +/* Dequant tables */ + +extern const OMX_U8 armVCM4P10_PosToVCol4x4[16]; +extern const OMX_U8 armVCM4P10_PosToVCol2x2[4]; +extern const OMX_U8 armVCM4P10_VMatrix[6][3]; +extern const OMX_U32 armVCM4P10_MFMatrix[6][3]; + + +/* + * Description: + * This function perform the work required by the OpenMAX + * DecodeCoeffsToPair function and DecodeChromaDCCoeffsToPair. + * Since most of the code is common we share it here. + * + * Parameters: + * [in] ppBitStream Double pointer to current byte in bit stream buffer + * [in] pOffset Pointer to current bit position in the byte pointed + * to by *ppBitStream + * [in] sMaxNumCoeff Maximum number of non-zero coefficients in current + * block (4,15 or 16) + * [in] nTable Table number (0 to 4) according to the five columns + * of Table 9-5 in the H.264 spec + * [out] ppBitStream *ppBitStream is updated after each block is decoded + * [out] pOffset *pOffset is updated after each block is decoded + * [out] pNumCoeff Pointer to the number of nonzero coefficients in + * this block + * [out] ppPosCoefbuf Double pointer to destination residual + * coefficient-position pair buffer + * Return Value: + * Standard omxError result. See enumeration for possible result codes. + + */ + __attribute__((weak)) +OMXResult armVCM4P10_DecodeCoeffsToPair( + const OMX_U8** ppBitStream, + OMX_S32* pOffset, + OMX_U8* pNumCoeff, + OMX_U8**ppPosCoefbuf, + OMX_INT nTable, + OMX_INT sMaxNumCoeff + ); + +/* + * Description: + * Perform DC style intra prediction, averaging upper and left block + * + * Parameters: + * [in] pSrcLeft Pointer to the buffer of 16 left coefficients: + * p[x, y] (x = -1, y = 0..3) + * [in] pSrcAbove Pointer to the buffer of 16 above coefficients: + * p[x,y] (x = 0..3, y = -1) + * [in] leftStep Step of left coefficient buffer + * [in] dstStep Step of the destination buffer + * [in] availability Neighboring 16x16 MB availability flag + * [out] pDst Pointer to the destination buffer + * + * Return Value: + * None + */ + __attribute__((weak)) +void armVCM4P10_PredictIntraDC4x4( + const OMX_U8* pSrcLeft, + const OMX_U8 *pSrcAbove, + OMX_U8* pDst, + OMX_INT leftStep, + OMX_INT dstStep, + OMX_S32 availability +); + +/* + * Description + * Unpack a 4x4 block of coefficient-residual pair values + * + * Parameters: + * [in] ppSrc Double pointer to residual coefficient-position pair + * buffer output by CALVC decoding + * [out] ppSrc *ppSrc is updated to the start of next non empty block + * [out] pDst Pointer to unpacked 4x4 block + */ + __attribute__((weak)) +void armVCM4P10_UnpackBlock4x4( + const OMX_U8 **ppSrc, + OMX_S16* pDst +); + +/* + * Description + * Unpack a 2x2 block of coefficient-residual pair values + * + * Parameters: + * [in] ppSrc Double pointer to residual coefficient-position pair + * buffer output by CALVC decoding + * [out] ppSrc *ppSrc is updated to the start of next non empty block + * [out] pDst Pointer to unpacked 4x4 block + */ + __attribute__((weak)) +void armVCM4P10_UnpackBlock2x2( + const OMX_U8 **ppSrc, + OMX_S16* pDst +); + +/* + * Description + * Deblock one boundary pixel + * + * Parameters: + * [in] pQ0 Pointer to pixel q0 + * [in] Step Step between pixels q0 and q1 + * [in] tC0 Edge threshold value + * [in] alpha alpha threshold value + * [in] beta beta threshold value + * [in] bS deblocking strength + * [in] ChromaFlag True for chroma blocks + * [out] pQ0 Deblocked pixels + * + */ + __attribute__((weak)) +void armVCM4P10_DeBlockPixel( + OMX_U8 *pQ0, /* pointer to the pixel q0 */ + int Step, /* step between pixels q0 and q1 */ + int tC0, /* edge threshold value */ + int alpha, /* alpha */ + int beta, /* beta */ + int bS, /* deblocking strength */ + int ChromaFlag +); + +/** + * Function: armVCM4P10_InterpolateHalfHor_Luma + * + * Description: + * This function performs interpolation for horizontal 1/2-pel positions + * + * Remarks: + * + * [in] pSrc Pointer to top-left corner of block used to interpolate + in the reconstructed frame plane + * [in] iSrcStep Step of the source buffer. + * [in] iDstStep Step of the destination(interpolation) buffer. + * [in] iWidth Width of the current block + * [in] iHeight Height of the current block + * [out] pDst Pointer to the interpolation buffer of the 1/2-pel + * + * Return Value: + * Standard OMXResult value. + * + */ + __attribute__((weak)) +OMXResult armVCM4P10_InterpolateHalfHor_Luma( + const OMX_U8* pSrc, + OMX_U32 iSrcStep, + OMX_U8* pDst, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight +); + +/** + * Function: armVCM4P10_InterpolateHalfVer_Luma + * + * Description: + * This function performs interpolation for vertical 1/2-pel positions + * around a full-pel position. + * + * Remarks: + * + * [in] pSrc Pointer to top-left corner of block used to interpolate + * in the reconstructed frame plane + * [in] iSrcStep Step of the source buffer. + * [in] iDstStep Step of the destination(interpolation) buffer. + * [in] iWidth Width of the current block + * [in] iHeight Height of the current block + * [out] pDst Pointer to the interpolation buffer of the 1/2-pel + * + * Return Value: + * Standard OMXResult value. + * + */ + __attribute__((weak)) +OMXResult armVCM4P10_InterpolateHalfVer_Luma( + const OMX_U8* pSrc, + OMX_U32 iSrcStep, + OMX_U8* pDst, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight +); + +/** + * Function: armVCM4P10_InterpolateHalfDiag_Luma + * + * Description: + * This function performs interpolation for (1/2, 1/2) positions + * around a full-pel position. + * + * Remarks: + * + * [in] pSrc Pointer to top-left corner of block used to interpolate + * in the reconstructed frame plane + * [in] iSrcStep Step of the source buffer. + * [in] iDstStep Step of the destination(interpolation) buffer. + * [in] iWidth Width of the current block + * [in] iHeight Height of the current block + * [out] pDst Pointer to the interpolation buffer of the (1/2,1/2)-pel + * + * Return Value: + * Standard OMXResult value. + * + */ + __attribute__((weak)) +OMXResult armVCM4P10_InterpolateHalfDiag_Luma( + const OMX_U8* pSrc, + OMX_U32 iSrcStep, + OMX_U8* pDst, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight +); + +/* + * Description: + * Transform Residual 4x4 Coefficients + * + * Parameters: + * [in] pSrc Source 4x4 block + * [out] pDst Destination 4x4 block + * + */ + __attribute__((weak)) +void armVCM4P10_TransformResidual4x4(OMX_S16* pDst, OMX_S16 *pSrc); + +/* + * Description: + * Forward Transform Residual 4x4 Coefficients + * + * Parameters: + * [in] pSrc Source 4x4 block + * [out] pDst Destination 4x4 block + * + */ + __attribute__((weak)) +void armVCM4P10_FwdTransformResidual4x4(OMX_S16* pDst, OMX_S16 *pSrc); + +__attribute__((weak)) +OMX_INT armVCM4P10_CompareMotionCostToMV ( + OMX_S16 mvX, + OMX_S16 mvY, + OMXVCMotionVector diffMV, + OMX_INT candSAD, + OMXVCMotionVector *bestMV, + OMX_U32 nLamda, + OMX_S32 *pBestCost); + +/** + * Function: armVCCOMM_SAD + * + * Description: + * This function calculate the SAD for NxM blocks. + * + * Remarks: + * + * [in] pSrcOrg Pointer to the original block + * [in] iStepOrg Step of the original block buffer + * [in] pSrcRef Pointer to the reference block + * [in] iStepRef Step of the reference block buffer + * [in] iHeight Height of the block + * [in] iWidth Width of the block + * [out] pDstSAD Pointer of result SAD + * + * Return Value: + * Standard OMXResult value. + * + */ +__attribute__((weak)) +OMXResult armVCCOMM_SAD( + const OMX_U8* pSrcOrg, + OMX_U32 iStepOrg, + const OMX_U8* pSrcRef, + OMX_U32 iStepRef, + OMX_S32* pDstSAD, + OMX_U32 iHeight, + OMX_U32 iWidth); + +/** + * Function: armVCCOMM_Average + * + * Description: + * This function calculates the average of two blocks and stores the result. + * + * Remarks: + * + * [in] pPred0 Pointer to the top-left corner of reference block 0 + * [in] pPred1 Pointer to the top-left corner of reference block 1 + * [in] iPredStep0 Step of reference block 0 + * [in] iPredStep1 Step of reference block 1 + * [in] iDstStep Step of the destination buffer + * [in] iWidth Width of the blocks + * [in] iHeight Height of the blocks + * [out] pDstPred Pointer to the destination buffer + * + * Return Value: + * Standard OMXResult value. + * + */ + __attribute__((weak)) + OMXResult armVCCOMM_Average ( + const OMX_U8* pPred0, + const OMX_U8* pPred1, + OMX_U32 iPredStep0, + OMX_U32 iPredStep1, + OMX_U8* pDstPred, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight +); + +/** + * Function: armVCM4P10_SADQuar + * + * Description: + * This function calculates the SAD between one block (pSrc) and the + * average of the other two (pSrcRef0 and pSrcRef1) + * + * Remarks: + * + * [in] pSrc Pointer to the original block + * [in] pSrcRef0 Pointer to reference block 0 + * [in] pSrcRef1 Pointer to reference block 1 + * [in] iSrcStep Step of the original block buffer + * [in] iRefStep0 Step of reference block 0 + * [in] iRefStep1 Step of reference block 1 + * [in] iHeight Height of the block + * [in] iWidth Width of the block + * [out] pDstSAD Pointer of result SAD + * + * Return Value: + * Standard OMXResult value. + * + */ + __attribute__((weak)) +OMXResult armVCM4P10_SADQuar( + const OMX_U8* pSrc, + const OMX_U8* pSrcRef0, + const OMX_U8* pSrcRef1, + OMX_U32 iSrcStep, + OMX_U32 iRefStep0, + OMX_U32 iRefStep1, + OMX_U32* pDstSAD, + OMX_U32 iHeight, + OMX_U32 iWidth +); + +/** + * Function: armVCM4P10_Interpolate_Chroma + * + * Description: + * This function performs interpolation for chroma components. + * + * Remarks: + * + * [in] pSrc Pointer to top-left corner of block used to + * interpolate in the reconstructed frame plane + * [in] iSrcStep Step of the source buffer. + * [in] iDstStep Step of the destination(interpolation) buffer. + * [in] iWidth Width of the current block + * [in] iHeight Height of the current block + * [in] dx Fractional part of horizontal motion vector + * component in 1/8 pixel unit (0~7) + * [in] dy Fractional part of vertical motion vector + * component in 1/8 pixel unit (0~7) + * [out] pDst Pointer to the interpolation buffer + * + * Return Value: + * Standard OMXResult value. + * + */ + __attribute__((weak)) + OMXResult armVCM4P10_Interpolate_Chroma( + OMX_U8 *pSrc, + OMX_U32 iSrcStep, + OMX_U8 *pDst, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight, + OMX_U32 dx, + OMX_U32 dy +); + +/** + * Function: armVCM4P10_Interpolate_Luma + * + * Description: + * This function performs interpolation for luma components. + * + * Remarks: + * + * [in] pSrc Pointer to top-left corner of block used to + * interpolate in the reconstructed frame plane + * [in] iSrcStep Step of the source buffer. + * [in] iDstStep Step of the destination(interpolation) buffer. + * [in] iWidth Width of the current block + * [in] iHeight Height of the current block + * [in] dx Fractional part of horizontal motion vector + * component in 1/4 pixel unit (0~3) + * [in] dy Fractional part of vertical motion vector + * component in 1/4 pixel unit (0~3) + * [out] pDst Pointer to the interpolation buffer + * + * Return Value: + * Standard OMXResult value. + * + */ + __attribute__((weak)) + OMXResult armVCM4P10_Interpolate_Luma( + const OMX_U8 *pSrc, + OMX_U32 iSrcStep, + OMX_U8 *pDst, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight, + OMX_U32 dx, + OMX_U32 dy +); + +/** + * Function: omxVCH264_DequantTransformACFromPair_U8_S16_C1_DLx + * + * Description: + * Reconstruct the 4x4 residual block from coefficient-position pair buffer, + * perform dequantisation and integer inverse transformation for 4x4 block of + * residuals and update the pair buffer pointer to next non-empty block. + * + * Remarks: + * + * Parameters: + * [in] ppSrc Double pointer to residual coefficient-position + * pair buffer output by CALVC decoding + * [in] pDC Pointer to the DC coefficient of this block, NULL + * if it doesn't exist + * [in] QP Quantization parameter + * [in] AC Flag indicating if at least one non-zero coefficient exists + * [out] pDst pointer to the reconstructed 4x4 block data + * + * Return Value: + * Standard omxError result. See enumeration for possible result codes. + * + */ + __attribute__((weak)) +OMXResult armVCM4P10_DequantTransformACFromPair_U8_S16_C1_DLx( + OMX_U8 **ppSrc, + OMX_S16 *pDst, + OMX_INT QP, + OMX_S16* pDC, + int AC +); + +#endif /*_armVideo_H_*/ + +/*End of File*/ + diff --git a/src/video/h264_decoder/omxdl/armVCCOMM_Average.c b/src/video/h264_decoder/omxdl/armVCCOMM_Average.c new file mode 100644 index 000000000..1e5107786 --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCCOMM_Average.c @@ -0,0 +1,78 @@ +/** + * + * File Name: armVCCOMM_Average.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * Description: + * This function will calculate Average of two blocks if size iWidth X iHeight + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +/** + * Function: armVCCOMM_Average + * + * Description: + * This function calculates the average of two blocks and stores the result. + * + * Remarks: + * + * [in] pPred0 Pointer to the top-left corner of reference block 0 + * [in] pPred1 Pointer to the top-left corner of reference block 1 + * [in] iPredStep0 Step of reference block 0 + * [in] iPredStep1 Step of reference block 1 + * [in] iDstStep Step of the destination buffer + * [in] iWidth Width of the blocks + * [in] iHeight Height of the blocks + * [out] pDstPred Pointer to the destination buffer + * + * Return Value: + * Standard OMXResult value. + * + */ + OMXResult armVCCOMM_Average ( + const OMX_U8* pPred0, + const OMX_U8* pPred1, + OMX_U32 iPredStep0, + OMX_U32 iPredStep1, + OMX_U8* pDstPred, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight +) +{ + OMX_U32 x, y; + + /* check for argument error */ + armRetArgErrIf(pPred0 == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pPred1 == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pDstPred == NULL, OMX_Sts_BadArgErr) + + for (y = 0; y < iHeight; y++) + { + for (x = 0; x < iWidth; x++) + { + pDstPred [y * iDstStep + x] = + (OMX_U8)(((OMX_U32)pPred0 [y * iPredStep0 + x] + + pPred1 [y * iPredStep1 + x] + 1) >> 1); + } + } + + return OMX_Sts_NoErr; +} + +/***************************************************************************** + * END OF FILE + *****************************************************************************/ + diff --git a/src/video/h264_decoder/omxdl/armVCCOMM_SAD.c b/src/video/h264_decoder/omxdl/armVCCOMM_SAD.c new file mode 100644 index 000000000..d41ac9a05 --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCCOMM_SAD.c @@ -0,0 +1,75 @@ +/** + * + * File Name: armVCCOMM_SAD.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * Description: + * This function will calculate SAD for NxM blocks + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" + +/** + * Function: armVCCOMM_SAD + * + * Description: + * This function calculate the SAD for NxM blocks. + * + * Remarks: + * + * [in] pSrcOrg Pointer to the original block + * [in] iStepOrg Step of the original block buffer + * [in] pSrcRef Pointer to the reference block + * [in] iStepRef Step of the reference block buffer + * [in] iHeight Height of the block + * [in] iWidth Width of the block + * [out] pDstSAD Pointer of result SAD + * + * Return Value: + * Standard OMXResult value. + * + */ +OMXResult armVCCOMM_SAD( + const OMX_U8* pSrcOrg, + OMX_U32 iStepOrg, + const OMX_U8* pSrcRef, + OMX_U32 iStepRef, + OMX_S32* pDstSAD, + OMX_U32 iHeight, + OMX_U32 iWidth +) +{ + OMX_INT x, y; + + /* check for argument error */ + armRetArgErrIf(pSrcOrg == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pSrcRef == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pDstSAD == NULL, OMX_Sts_BadArgErr) + + *pDstSAD = 0; + for (y = 0; y < iHeight; y++) + { + for (x = 0; x < iWidth; x++) + { + *pDstSAD += armAbs(pSrcOrg [(y * iStepOrg) + x] - + pSrcRef [(y * iStepRef) + x]); + } + } + + return OMX_Sts_NoErr; +} + +/***************************************************************************** + * END OF FILE + *****************************************************************************/ + diff --git a/src/video/h264_decoder/omxdl/armVCM4P10_CAVLCTables.c b/src/video/h264_decoder/omxdl/armVCM4P10_CAVLCTables.c new file mode 100644 index 000000000..f4e36ad6c --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCM4P10_CAVLCTables.c @@ -0,0 +1,703 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: armVCM4P10_CAVLCTables.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * CAVLC tables for H.264 + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM_Bitstream.h" +#include "armVC.h" +#include "armVCM4P10_CAVLCTables.h" + +/* Tables mapping a code to TrailingOnes and TotalCoeff */ + +const OMX_U8 armVCM4P10_CAVLCTrailingOnes[62] = { + 0, + 0, 1, + 0, 1, 2, + 0, 1, 2, 3, + 0, 1, 2, 3, + 0, 1, 2, 3, + 0, 1, 2, 3, + 0, 1, 2, 3, + 0, 1, 2, 3, + 0, 1, 2, 3, + 0, 1, 2, 3, + 0, 1, 2, 3, + 0, 1, 2, 3, + 0, 1, 2, 3, + 0, 1, 2, 3, + 0, 1, 2, 3, + 0, 1, 2, 3 +}; + +const OMX_U8 armVCM4P10_CAVLCTotalCoeff[62] = { + 0, + 1, 1, + 2, 2, 2, + 3, 3, 3, 3, + 4, 4, 4, 4, + 5, 5, 5, 5, + 6, 6, 6, 6, + 7, 7, 7, 7, + 8, 8, 8, 8, + 9, 9, 9, 9, + 10, 10, 10, 10, + 11, 11, 11, 11, + 12, 12, 12, 12, + 13, 13, 13, 13, + 14, 14, 14, 14, + 15, 15, 15, 15, + 16, 16, 16, 16 +}; + +static const ARM_VLC32 armVCM4P10_CAVLCCoeffToken0[63] = { + { 1, 0x0001 }, + { 6, 0x0005 }, + { 2, 0x0001 }, + { 8, 0x0007 }, + { 6, 0x0004 }, + { 3, 0x0001 }, + { 9, 0x0007 }, + { 8, 0x0006 }, + { 7, 0x0005 }, + { 5, 0x0003 }, + { 10, 0x0007 }, + { 9, 0x0006 }, + { 8, 0x0005 }, + { 6, 0x0003 }, + { 11, 0x0007 }, + { 10, 0x0006 }, + { 9, 0x0005 }, + { 7, 0x0004 }, + { 13, 0x000f }, + { 11, 0x0006 }, + { 10, 0x0005 }, + { 8, 0x0004 }, + { 13, 0x000b }, + { 13, 0x000e }, + { 11, 0x0005 }, + { 9, 0x0004 }, + { 13, 0x0008 }, + { 13, 0x000a }, + { 13, 0x000d }, + { 10, 0x0004 }, + { 14, 0x000f }, + { 14, 0x000e }, + { 13, 0x0009 }, + { 11, 0x0004 }, + { 14, 0x000b }, + { 14, 0x000a }, + { 14, 0x000d }, + { 13, 0x000c }, + { 15, 0x000f }, + { 15, 0x000e }, + { 14, 0x0009 }, + { 14, 0x000c }, + { 15, 0x000b }, + { 15, 0x000a }, + { 15, 0x000d }, + { 14, 0x0008 }, + { 16, 0x000f }, + { 15, 0x0001 }, + { 15, 0x0009 }, + { 15, 0x000c }, + { 16, 0x000b }, + { 16, 0x000e }, + { 16, 0x000d }, + { 15, 0x0008 }, + { 16, 0x0007 }, + { 16, 0x000a }, + { 16, 0x0009 }, + { 16, 0x000c }, + { 16, 0x0004 }, + { 16, 0x0006 }, + { 16, 0x0005 }, + { 16, 0x0008 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCCoeffToken1[63] = { + { 2, 0x0003 }, + { 6, 0x000b }, + { 2, 0x0002 }, + { 6, 0x0007 }, + { 5, 0x0007 }, + { 3, 0x0003 }, + { 7, 0x0007 }, + { 6, 0x000a }, + { 6, 0x0009 }, + { 4, 0x0005 }, + { 8, 0x0007 }, + { 6, 0x0006 }, + { 6, 0x0005 }, + { 4, 0x0004 }, + { 8, 0x0004 }, + { 7, 0x0006 }, + { 7, 0x0005 }, + { 5, 0x0006 }, + { 9, 0x0007 }, + { 8, 0x0006 }, + { 8, 0x0005 }, + { 6, 0x0008 }, + { 11, 0x000f }, + { 9, 0x0006 }, + { 9, 0x0005 }, + { 6, 0x0004 }, + { 11, 0x000b }, + { 11, 0x000e }, + { 11, 0x000d }, + { 7, 0x0004 }, + { 12, 0x000f }, + { 11, 0x000a }, + { 11, 0x0009 }, + { 9, 0x0004 }, + { 12, 0x000b }, + { 12, 0x000e }, + { 12, 0x000d }, + { 11, 0x000c }, + { 12, 0x0008 }, + { 12, 0x000a }, + { 12, 0x0009 }, + { 11, 0x0008 }, + { 13, 0x000f }, + { 13, 0x000e }, + { 13, 0x000d }, + { 12, 0x000c }, + { 13, 0x000b }, + { 13, 0x000a }, + { 13, 0x0009 }, + { 13, 0x000c }, + { 13, 0x0007 }, + { 14, 0x000b }, + { 13, 0x0006 }, + { 13, 0x0008 }, + { 14, 0x0009 }, + { 14, 0x0008 }, + { 14, 0x000a }, + { 13, 0x0001 }, + { 14, 0x0007 }, + { 14, 0x0006 }, + { 14, 0x0005 }, + { 14, 0x0004 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCCoeffToken2[63] = { + { 4, 0x000f }, + { 6, 0x000f }, + { 4, 0x000e }, + { 6, 0x000b }, + { 5, 0x000f }, + { 4, 0x000d }, + { 6, 0x0008 }, + { 5, 0x000c }, + { 5, 0x000e }, + { 4, 0x000c }, + { 7, 0x000f }, + { 5, 0x000a }, + { 5, 0x000b }, + { 4, 0x000b }, + { 7, 0x000b }, + { 5, 0x0008 }, + { 5, 0x0009 }, + { 4, 0x000a }, + { 7, 0x0009 }, + { 6, 0x000e }, + { 6, 0x000d }, + { 4, 0x0009 }, + { 7, 0x0008 }, + { 6, 0x000a }, + { 6, 0x0009 }, + { 4, 0x0008 }, + { 8, 0x000f }, + { 7, 0x000e }, + { 7, 0x000d }, + { 5, 0x000d }, + { 8, 0x000b }, + { 8, 0x000e }, + { 7, 0x000a }, + { 6, 0x000c }, + { 9, 0x000f }, + { 8, 0x000a }, + { 8, 0x000d }, + { 7, 0x000c }, + { 9, 0x000b }, + { 9, 0x000e }, + { 8, 0x0009 }, + { 8, 0x000c }, + { 9, 0x0008 }, + { 9, 0x000a }, + { 9, 0x000d }, + { 8, 0x0008 }, + { 10, 0x000d }, + { 9, 0x0007 }, + { 9, 0x0009 }, + { 9, 0x000c }, + { 10, 0x0009 }, + { 10, 0x000c }, + { 10, 0x000b }, + { 10, 0x000a }, + { 10, 0x0005 }, + { 10, 0x0008 }, + { 10, 0x0007 }, + { 10, 0x0006 }, + { 10, 0x0001 }, + { 10, 0x0004 }, + { 10, 0x0003 }, + { 10, 0x0002 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCCoeffToken3[63] = { + { 6, 0x0003 }, + { 6, 0x0000 }, + { 6, 0x0001 }, + { 6, 0x0004 }, + { 6, 0x0005 }, + { 6, 0x0006 }, + { 6, 0x0008 }, + { 6, 0x0009 }, + { 6, 0x000a }, + { 6, 0x000b }, + { 6, 0x000c }, + { 6, 0x000d }, + { 6, 0x000e }, + { 6, 0x000f }, + { 6, 0x0010 }, + { 6, 0x0011 }, + { 6, 0x0012 }, + { 6, 0x0013 }, + { 6, 0x0014 }, + { 6, 0x0015 }, + { 6, 0x0016 }, + { 6, 0x0017 }, + { 6, 0x0018 }, + { 6, 0x0019 }, + { 6, 0x001a }, + { 6, 0x001b }, + { 6, 0x001c }, + { 6, 0x001d }, + { 6, 0x001e }, + { 6, 0x001f }, + { 6, 0x0020 }, + { 6, 0x0021 }, + { 6, 0x0022 }, + { 6, 0x0023 }, + { 6, 0x0024 }, + { 6, 0x0025 }, + { 6, 0x0026 }, + { 6, 0x0027 }, + { 6, 0x0028 }, + { 6, 0x0029 }, + { 6, 0x002a }, + { 6, 0x002b }, + { 6, 0x002c }, + { 6, 0x002d }, + { 6, 0x002e }, + { 6, 0x002f }, + { 6, 0x0030 }, + { 6, 0x0031 }, + { 6, 0x0032 }, + { 6, 0x0033 }, + { 6, 0x0034 }, + { 6, 0x0035 }, + { 6, 0x0036 }, + { 6, 0x0037 }, + { 6, 0x0038 }, + { 6, 0x0039 }, + { 6, 0x003a }, + { 6, 0x003b }, + { 6, 0x003c }, + { 6, 0x003d }, + { 6, 0x003e }, + { 6, 0x003f }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCCoeffToken4[15] = { + { 2, 0x0001 }, + { 6, 0x0007 }, + { 1, 0x0001 }, + { 6, 0x0004 }, + { 6, 0x0006 }, + { 3, 0x0001 }, + { 6, 0x0003 }, + { 7, 0x0003 }, + { 7, 0x0002 }, + { 6, 0x0005 }, + { 6, 0x0002 }, + { 8, 0x0003 }, + { 8, 0x0002 }, + { 7, 0x0000 }, + { 0, 0x0000 } +}; + + +const ARM_VLC32 *armVCM4P10_CAVLCCoeffTokenTables[5] = { + armVCM4P10_CAVLCCoeffToken0, + armVCM4P10_CAVLCCoeffToken1, + armVCM4P10_CAVLCCoeffToken2, + armVCM4P10_CAVLCCoeffToken3, + armVCM4P10_CAVLCCoeffToken4 +}; + +/* Table for level_prefix */ + +const ARM_VLC32 armVCM4P10_CAVLCLevelPrefix[17] = { + { 1, 1}, + { 2, 1}, + { 3, 1}, + { 4, 1}, + { 5, 1}, + { 6, 1}, + { 7, 1}, + { 8, 1}, + { 9, 1}, + { 10, 1}, + { 11, 1}, + { 12, 1}, + { 13, 1}, + { 14, 1}, + { 15, 1}, + { 16, 1}, + { 0, 0} +}; + +/* Tables for total_zeros */ + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros1[17] = { + { 1, 0x0001 }, + { 3, 0x0003 }, + { 3, 0x0002 }, + { 4, 0x0003 }, + { 4, 0x0002 }, + { 5, 0x0003 }, + { 5, 0x0002 }, + { 6, 0x0003 }, + { 6, 0x0002 }, + { 7, 0x0003 }, + { 7, 0x0002 }, + { 8, 0x0003 }, + { 8, 0x0002 }, + { 9, 0x0003 }, + { 9, 0x0002 }, + { 9, 0x0001 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros2[16] = { + { 3, 0x0007 }, + { 3, 0x0006 }, + { 3, 0x0005 }, + { 3, 0x0004 }, + { 3, 0x0003 }, + { 4, 0x0005 }, + { 4, 0x0004 }, + { 4, 0x0003 }, + { 4, 0x0002 }, + { 5, 0x0003 }, + { 5, 0x0002 }, + { 6, 0x0003 }, + { 6, 0x0002 }, + { 6, 0x0001 }, + { 6, 0x0000 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros3[15] = { + { 4, 0x0005 }, + { 3, 0x0007 }, + { 3, 0x0006 }, + { 3, 0x0005 }, + { 4, 0x0004 }, + { 4, 0x0003 }, + { 3, 0x0004 }, + { 3, 0x0003 }, + { 4, 0x0002 }, + { 5, 0x0003 }, + { 5, 0x0002 }, + { 6, 0x0001 }, + { 5, 0x0001 }, + { 6, 0x0000 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros4[14] = { + { 5, 0x0003 }, + { 3, 0x0007 }, + { 4, 0x0005 }, + { 4, 0x0004 }, + { 3, 0x0006 }, + { 3, 0x0005 }, + { 3, 0x0004 }, + { 4, 0x0003 }, + { 3, 0x0003 }, + { 4, 0x0002 }, + { 5, 0x0002 }, + { 5, 0x0001 }, + { 5, 0x0000 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros5[13] = { + { 4, 0x0005 }, + { 4, 0x0004 }, + { 4, 0x0003 }, + { 3, 0x0007 }, + { 3, 0x0006 }, + { 3, 0x0005 }, + { 3, 0x0004 }, + { 3, 0x0003 }, + { 4, 0x0002 }, + { 5, 0x0001 }, + { 4, 0x0001 }, + { 5, 0x0000 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros6[12] = { + { 6, 0x0001 }, + { 5, 0x0001 }, + { 3, 0x0007 }, + { 3, 0x0006 }, + { 3, 0x0005 }, + { 3, 0x0004 }, + { 3, 0x0003 }, + { 3, 0x0002 }, + { 4, 0x0001 }, + { 3, 0x0001 }, + { 6, 0x0000 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros7[11] = { + { 6, 0x0001 }, + { 5, 0x0001 }, + { 3, 0x0005 }, + { 3, 0x0004 }, + { 3, 0x0003 }, + { 2, 0x0003 }, + { 3, 0x0002 }, + { 4, 0x0001 }, + { 3, 0x0001 }, + { 6, 0x0000 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros8[10] = { + { 6, 0x0001 }, + { 4, 0x0001 }, + { 5, 0x0001 }, + { 3, 0x0003 }, + { 2, 0x0003 }, + { 2, 0x0002 }, + { 3, 0x0002 }, + { 3, 0x0001 }, + { 6, 0x0000 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros9[9] = { + { 6, 0x0001 }, + { 6, 0x0000 }, + { 4, 0x0001 }, + { 2, 0x0003 }, + { 2, 0x0002 }, + { 3, 0x0001 }, + { 2, 0x0001 }, + { 5, 0x0001 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros10[8] = { + { 5, 0x0001 }, + { 5, 0x0000 }, + { 3, 0x0001 }, + { 2, 0x0003 }, + { 2, 0x0002 }, + { 2, 0x0001 }, + { 4, 0x0001 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros11[7] = { + { 4, 0x0000 }, + { 4, 0x0001 }, + { 3, 0x0001 }, + { 3, 0x0002 }, + { 1, 0x0001 }, + { 3, 0x0003 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros12[6] = { + { 4, 0x0000 }, + { 4, 0x0001 }, + { 2, 0x0001 }, + { 1, 0x0001 }, + { 3, 0x0001 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros13[5] = { + { 3, 0x0000 }, + { 3, 0x0001 }, + { 1, 0x0001 }, + { 2, 0x0001 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros14[4] = { + { 2, 0x0000 }, + { 2, 0x0001 }, + { 1, 0x0001 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros15[3] = { + { 1, 0x0000 }, + { 1, 0x0001 }, + { 0, 0x0000 } +}; + +const ARM_VLC32 *armVCM4P10_CAVLCTotalZeroTables[15] = { + armVCM4P10_CAVLCTotalZeros1, + armVCM4P10_CAVLCTotalZeros2, + armVCM4P10_CAVLCTotalZeros3, + armVCM4P10_CAVLCTotalZeros4, + armVCM4P10_CAVLCTotalZeros5, + armVCM4P10_CAVLCTotalZeros6, + armVCM4P10_CAVLCTotalZeros7, + armVCM4P10_CAVLCTotalZeros8, + armVCM4P10_CAVLCTotalZeros9, + armVCM4P10_CAVLCTotalZeros10, + armVCM4P10_CAVLCTotalZeros11, + armVCM4P10_CAVLCTotalZeros12, + armVCM4P10_CAVLCTotalZeros13, + armVCM4P10_CAVLCTotalZeros14, + armVCM4P10_CAVLCTotalZeros15 +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros2x2_1[5] = { + { 1, 1 }, + { 2, 1 }, + { 3, 1 }, + { 3, 0 }, + { 0, 0 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros2x2_2[4] = { + { 1, 1 }, + { 2, 1 }, + { 2, 0 }, + { 0, 0 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCTotalZeros2x2_3[3] = { + { 1, 1 }, + { 1, 0 }, + { 0, 0 } +}; + +const ARM_VLC32 *armVCM4P10_CAVLCTotalZeros2x2Tables[3] = { + armVCM4P10_CAVLCTotalZeros2x2_1, + armVCM4P10_CAVLCTotalZeros2x2_2, + armVCM4P10_CAVLCTotalZeros2x2_3 +}; + + +/* Tables for run_before */ + +static const ARM_VLC32 armVCM4P10_CAVLCRunBefore1[3] = { + { 1, 0x0001 }, + { 1, 0x0000 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCRunBefore2[4] = { + { 1, 0x0001 }, + { 2, 0x0001 }, + { 2, 0x0000 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCRunBefore3[5] = { + { 2, 0x0003 }, + { 2, 0x0002 }, + { 2, 0x0001 }, + { 2, 0x0000 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCRunBefore4[6] = { + { 2, 0x0003 }, + { 2, 0x0002 }, + { 2, 0x0001 }, + { 3, 0x0001 }, + { 3, 0x0000 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCRunBefore5[7] = { + { 2, 0x0003 }, + { 2, 0x0002 }, + { 3, 0x0003 }, + { 3, 0x0002 }, + { 3, 0x0001 }, + { 3, 0x0000 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCRunBefore6[8] = { + { 2, 0x0003 }, + { 3, 0x0000 }, + { 3, 0x0001 }, + { 3, 0x0003 }, + { 3, 0x0002 }, + { 3, 0x0005 }, + { 3, 0x0004 }, + { 0, 0x0000 } +}; + +static const ARM_VLC32 armVCM4P10_CAVLCRunBefore7[16] = { + { 3, 0x0007 }, + { 3, 0x0006 }, + { 3, 0x0005 }, + { 3, 0x0004 }, + { 3, 0x0003 }, + { 3, 0x0002 }, + { 3, 0x0001 }, + { 4, 0x0001 }, + { 5, 0x0001 }, + { 6, 0x0001 }, + { 7, 0x0001 }, + { 8, 0x0001 }, + { 9, 0x0001 }, + { 10, 0x0001 }, + { 11, 0x0001 }, + { 0, 0x0000 } +}; + +const ARM_VLC32 *armVCM4P10_CAVLCRunBeforeTables[7] = { + armVCM4P10_CAVLCRunBefore1, + armVCM4P10_CAVLCRunBefore2, + armVCM4P10_CAVLCRunBefore3, + armVCM4P10_CAVLCRunBefore4, + armVCM4P10_CAVLCRunBefore5, + armVCM4P10_CAVLCRunBefore6, + armVCM4P10_CAVLCRunBefore7 +}; diff --git a/src/video/h264_decoder/omxdl/armVCM4P10_CAVLCTables.h b/src/video/h264_decoder/omxdl/armVCM4P10_CAVLCTables.h new file mode 100644 index 000000000..8d18a8f4f --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCM4P10_CAVLCTables.h @@ -0,0 +1,34 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: armVCM4P10_CAVLCTables.h + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * ---------------------------------------------------------------- + * File: armVCM4P10_CAVLCTables.h + * ---------------------------------------------------------------- + * + * Header file for ARM implementation of OpenMAX VCM4P10 + * + */ + +#ifndef ARMVCM4P10_CAVLCTABLES_H +#define ARMVCM4P10_CAVLCTABLES_H + +/* CAVLC tables */ + +extern const OMX_U8 armVCM4P10_CAVLCTrailingOnes[62]; +extern const OMX_U8 armVCM4P10_CAVLCTotalCoeff[62]; +extern const ARM_VLC32 *armVCM4P10_CAVLCCoeffTokenTables[5]; +extern const ARM_VLC32 armVCM4P10_CAVLCLevelPrefix[17]; +extern const ARM_VLC32 *armVCM4P10_CAVLCTotalZeroTables[15]; +extern const ARM_VLC32 *armVCM4P10_CAVLCTotalZeros2x2Tables[3]; +extern const ARM_VLC32 *armVCM4P10_CAVLCRunBeforeTables[7]; + +#endif diff --git a/src/video/h264_decoder/omxdl/armVCM4P10_DeBlockPixel.c b/src/video/h264_decoder/omxdl/armVCM4P10_DeBlockPixel.c new file mode 100644 index 000000000..f4fb1d9fa --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCM4P10_DeBlockPixel.c @@ -0,0 +1,151 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: armVCM4P10_DeBlockPixel.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 luma deblock module + * + */ + +#ifdef DEBUG_ARMVCM4P10_DEBLOCKPIXEL +#undef DEBUG_ON +#define DEBUG_ON +#endif /* DEBUG_ARMVCM4P10_DEBLOCKPIXEL */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +/* + * Description + * Deblock one boundary pixel + * + * Parameters: + * [in] pQ0 Pointer to pixel q0 + * [in] Step Step between pixels q0 and q1 + * [in] tC0 Edge threshold value + * [in] alpha alpha threshold value + * [in] beta beta threshold value + * [in] bS deblocking strength + * [in] ChromaFlag True for chroma blocks + * [out] pQ0 Deblocked pixels + * + */ + +void armVCM4P10_DeBlockPixel( + OMX_U8 *pQ0, /* pointer to the pixel q0 */ + int Step, /* step between pixels q0 and q1 */ + int tC0, /* edge threshold value */ + int alpha, /* alpha */ + int beta, /* beta */ + int bS, /* deblocking strength */ + int ChromaFlag +) +{ + int p3, p2, p1, p0, q0, q1, q2, q3; + int ap, aq, delta; + + if (bS==0) + { + return; + } + + p3 = pQ0[-4*Step]; + p2 = pQ0[-3*Step]; + p1 = pQ0[-2*Step]; + p0 = pQ0[-1*Step]; + q0 = pQ0[ 0*Step]; + q1 = pQ0[ 1*Step]; + q2 = pQ0[ 2*Step]; + q3 = pQ0[ 3*Step]; + + if (armAbs(p0-q0)>=alpha || armAbs(p1-p0)>=beta || armAbs(q1-q0)>=beta) + { + DEBUG_PRINTF_10("DeBlockPixel: %02x %02x %02x %02x | %02x %02x %02x %02x alpha=%d beta=%d\n", + p3, p2, p1, p0, q0, q1, q2, q3, alpha, beta); + return; + } + + ap = armAbs(p2 - p0); + aq = armAbs(q2 - q0); + + if (bS < 4) + { + int tC = tC0; + + if (ChromaFlag) + { + tC++; + } + else + { + if (ap < beta) + { + tC++; + } + if (aq < beta) + { + tC++; + } + } + + delta = (((q0-p0)<<2) + (p1-q1) + 4) >> 3; + delta = armClip(-tC, tC, delta); + + pQ0[-1*Step] = (OMX_U8)armClip(0, 255, p0 + delta); + pQ0[ 0*Step] = (OMX_U8)armClip(0, 255, q0 - delta); + + if (ChromaFlag==0 && ap>1) - (p1<<1))>>1; + delta = armClip(-tC0, tC0, delta); + pQ0[-2*Step] = (OMX_U8)(p1 + delta); + } + + if (ChromaFlag==0 && aq>1) - (q1<<1))>>1; + delta = armClip(-tC0, tC0, delta); + pQ0[ 1*Step] = (OMX_U8)(q1 + delta); + } + } + else /* bS==4 */ + { + if (ChromaFlag==0 && ap>2)+2)) + { + pQ0[-1*Step] = (OMX_U8)((p2 + 2*p1 + 2*p0 + 2*q0 + q1 + 4)>>3); + pQ0[-2*Step] = (OMX_U8)((p2 + p1 + p0 + q0 + 2)>>2); + pQ0[-3*Step] = (OMX_U8)((2*p3 + 3*p2 + p1 + p0 + q0 + 4)>>3); + } + else + { + pQ0[-1*Step] = (OMX_U8)((2*p1 + p0 + q1 + 2)>>2); + } + + if (ChromaFlag==0 && aq>2)+2)) + { + pQ0[ 0*Step] = (OMX_U8)((q2 + 2*q1 + 2*q0 + 2*p0 + p1 + 4)>>3); + pQ0[ 1*Step] = (OMX_U8)((q2 + q1 + p0 + q0 + 2)>>2); + pQ0[ 2*Step] = (OMX_U8)((2*q3 + 3*q2 + q1 + q0 + p0 + 4)>>3); + } + else + { + pQ0[ 0*Step] = (OMX_U8)((2*q1 + q0 + p1 + 2)>>2); + } + } + + DEBUG_PRINTF_13("DeBlockPixel: %02x %02x %02x %02x | %02x %02x %02x %02x bS=%d -> %02x %02x %02x %02x\n", + p3, p2, p1, p0, q0, q1, q2, q3, bS, + pQ0[-2*Step], pQ0[-1*Step],pQ0[0*Step],pQ0[1*Step]); + +} diff --git a/src/video/h264_decoder/omxdl/armVCM4P10_DecodeCoeffsToPair.c b/src/video/h264_decoder/omxdl/armVCM4P10_DecodeCoeffsToPair.c new file mode 100644 index 000000000..80c43443c --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCM4P10_DecodeCoeffsToPair.c @@ -0,0 +1,286 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: armVCM4P10_DecodeCoeffsToPair.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 decode coefficients module + * + */ + +#ifdef DEBUG_ARMVCM4P10_DECODECOEFFSTOPAIR +#undef DEBUG_ON +#define DEBUG_ON +#endif + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armCOMM_Bitstream.h" +#include "armVCM4P10_CAVLCTables.h" + +OMX_U8 DEBUG_armVCM4P10_DecodeCoeffsToPair_LastTotalZeros; + +/* 4x4 DeZigZag table */ + +static const OMX_U8 armVCM4P10_ZigZag[16] = +{ + 0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15 +}; + +/* + * Description: + * This function perform the work required by the OpenMAX + * DecodeCoeffsToPair function and DecodeChromaDCCoeffsToPair. + * Since most of the code is common we share it here. + * + * Parameters: + * [in] ppBitStream Double pointer to current byte in bit stream buffer + * [in] pOffset Pointer to current bit position in the byte pointed + * to by *ppBitStream + * [in] sMaxNumCoeff Maximum number of non-zero coefficients in current + * block (4,15 or 16) + * [in] nTable Table number (0 to 4) according to the five columns + * of Table 9-5 in the H.264 spec + * [out] ppBitStream *ppBitStream is updated after each block is decoded + * [out] pOffset *pOffset is updated after each block is decoded + * [out] pNumCoeff Pointer to the number of nonzero coefficients in + * this block + * [out] ppPosCoefbuf Double pointer to destination residual + * coefficient-position pair buffer + * Return Value: + * Standard omxError result. See enumeration for possible result codes. + + */ + +OMXResult armVCM4P10_DecodeCoeffsToPair( + const OMX_U8** ppBitStream, + OMX_S32* pOffset, + OMX_U8* pNumCoeff, + OMX_U8 **ppPosCoefbuf, + OMX_INT nTable, + OMX_INT sMaxNumCoeff + ) +{ + int CoeffToken, TotalCoeff, TrailingOnes; + int Level, LevelCode, LevelPrefix, LevelSuffix, LevelSuffixSize; + int SuffixLength, Run, ZerosLeft,CoeffNum; + int i, Flags; + OMX_U8 *pPosCoefbuf = *ppPosCoefbuf; + OMX_S16 pLevel[16]; + OMX_U8 pRun[16]; + + CoeffToken = armUnPackVLC32(ppBitStream, pOffset, armVCM4P10_CAVLCCoeffTokenTables[nTable]); + armRetDataErrIf(CoeffToken == ARM_NO_CODEBOOK_INDEX, OMX_Sts_Err); + + TrailingOnes = armVCM4P10_CAVLCTrailingOnes[CoeffToken]; + TotalCoeff = armVCM4P10_CAVLCTotalCoeff[CoeffToken]; + *pNumCoeff = (OMX_U8)TotalCoeff; + + DEBUG_PRINTF_2("TotalCoeff = %d, TrailingOnes = %d\n", TotalCoeff, TrailingOnes); + + if (TotalCoeff == 0) + { + /* Nothing to do */ + return OMX_Sts_NoErr; + } + + /* Decode trailing ones */ + for (i=TotalCoeff-1; i>=TotalCoeff-TrailingOnes; i--) + { + if (armGetBits(ppBitStream, pOffset, 1)) + { + Level = -1; + } + else + { + Level = +1; + } + pLevel[i] = (OMX_S16)Level; + + DEBUG_PRINTF_2("Level[%d] = %d\n", i, pLevel[i]); + } + + /* Decode (non zero) level values */ + SuffixLength = 0; + if (TotalCoeff>10 && TrailingOnes<3) + { + SuffixLength=1; + } + for ( ; i>=0; i--) + { + LevelPrefix = armUnPackVLC32(ppBitStream, pOffset, armVCM4P10_CAVLCLevelPrefix); + armRetDataErrIf(LevelPrefix == ARM_NO_CODEBOOK_INDEX, OMX_Sts_Err); + + LevelSuffixSize = SuffixLength; + if (LevelPrefix==14 && SuffixLength==0) + { + LevelSuffixSize = 4; + } + if (LevelPrefix==15) + { + LevelSuffixSize = 12; + } + + LevelSuffix = 0; + if (LevelSuffixSize > 0) + { + LevelSuffix = armGetBits(ppBitStream, pOffset, LevelSuffixSize); + } + + LevelCode = (LevelPrefix << SuffixLength) + LevelSuffix; + + + if (LevelPrefix==15 && SuffixLength==0) + { + LevelCode += 15; + } + + /* LevelCode = 2*(magnitude-1) + sign */ + + if (i==TotalCoeff-1-TrailingOnes && TrailingOnes<3) + { + /* Level magnitude can't be 1 */ + LevelCode += 2; + } + if (LevelCode & 1) + { + /* 2a+1 maps to -a-1 */ + Level = (-LevelCode-1)>>1; + } + else + { + /* 2a+0 maps to +a+1 */ + Level = (LevelCode+2)>>1; + } + pLevel[i] = (OMX_S16)Level; + + DEBUG_PRINTF_2("Level[%d] = %d\n", i, pLevel[i]); + + if (SuffixLength==0) + { + SuffixLength=1; + } + if ( ((LevelCode>>1)+1)>(3<<(SuffixLength-1)) && SuffixLength<6 ) + { + SuffixLength++; + } + } + + /* Decode run values */ + ZerosLeft = 0; + if (TotalCoeff < sMaxNumCoeff) + { + /* Decode TotalZeros VLC */ + if (sMaxNumCoeff==4) + { + ZerosLeft = armUnPackVLC32(ppBitStream, pOffset, armVCM4P10_CAVLCTotalZeros2x2Tables[TotalCoeff-1]); + armRetDataErrIf(ZerosLeft ==ARM_NO_CODEBOOK_INDEX , OMX_Sts_Err); + } + else + { + ZerosLeft = armUnPackVLC32(ppBitStream, pOffset, armVCM4P10_CAVLCTotalZeroTables[TotalCoeff-1]); + armRetDataErrIf(ZerosLeft ==ARM_NO_CODEBOOK_INDEX , OMX_Sts_Err); + } + } + + DEBUG_PRINTF_1("TotalZeros = %d\n", ZerosLeft); + DEBUG_armVCM4P10_DecodeCoeffsToPair_LastTotalZeros = ZerosLeft; + + CoeffNum=ZerosLeft+TotalCoeff-1; + + for (i=TotalCoeff-1; i>0; i--) + { + Run = 0; + if (ZerosLeft > 0) + { + int Table = ZerosLeft; + if (Table > 6) + { + Table = 7; + } + Run = armUnPackVLC32(ppBitStream, pOffset, armVCM4P10_CAVLCRunBeforeTables[Table-1]); + armRetDataErrIf(Run == ARM_NO_CODEBOOK_INDEX, OMX_Sts_Err); + } + pRun[i] = (OMX_U8)Run; + + DEBUG_PRINTF_2("Run[%d] = %d\n", i, pRun[i]); + + ZerosLeft -= Run; + // **************************************************** + // rasky: added to detect error condition that goes unnoticed + armRetDataErrIf(ZerosLeft < 0, OMX_Sts_Err); + // **************************************************** + } + pRun[0] = (OMX_U8)ZerosLeft; + + DEBUG_PRINTF_1("Run[0] = %d\n", pRun[i]); + // for (int i=TotalCoeff;i<16;i++) { + // pRun[i] = 255; + // } + // printf("REF: Run1: %d,%d,%d,%d,%d,%d,%d,%d\n", pRun[0], pRun[1], pRun[2], pRun[3], pRun[4], pRun[5], pRun[6], pRun[7]); + // printf("REF: Run2: %d,%d,%d,%d,%d,%d,%d,%d\n", pRun[8], pRun[9], pRun[10], pRun[11], pRun[12], pRun[13], pRun[14], pRun[15]); + // printf("ZEROLEFT: %d\n", ZerosLeft); + + /* Fill in coefficients */ + + if (sMaxNumCoeff==15) + { + CoeffNum++; /* Skip the DC position */ + } + + /*for (i=0;i=0; i--) + { + /*CoeffNum += pRun[i]+1;*/ + Level = pLevel[i]; + + DEBUG_PRINTF_2("Coef[%d] = %d\n", CoeffNum, Level); + + Flags = CoeffNum; + + // **************************************************** + // rasky: added to avoid buffer overflow + armRetDataErrIf(Flags < 0 || Flags > 15, OMX_Sts_Err); + // **************************************************** + + CoeffNum -= (pRun[i]+1); + if (sMaxNumCoeff>4) + { + /* Perform 4x4 DeZigZag */ + Flags = armVCM4P10_ZigZag[Flags]; + } + if (i==0) + { + /* End of block flag */ + Flags += 0x20; + } + if (Level<-128 || Level>127) + { + /* Overflow flag */ + Flags += 0x10; + } + + *pPosCoefbuf++ = (OMX_U8)(Flags); + *pPosCoefbuf++ = (OMX_U8)(Level & 0xFF); + if (Flags & 0x10) + { + *pPosCoefbuf++ = (OMX_U8)(Level>>8); + } + } + + *ppPosCoefbuf = pPosCoefbuf; + + return OMX_Sts_NoErr; +} + diff --git a/src/video/h264_decoder/omxdl/armVCM4P10_DequantTables.c b/src/video/h264_decoder/omxdl/armVCM4P10_DequantTables.c new file mode 100644 index 000000000..d9c2541e5 --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCM4P10_DequantTables.c @@ -0,0 +1,45 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: armVCM4P10_DequantTables.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 inverse quantize tables + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" +#include "armVC.h" + + +const OMX_U8 armVCM4P10_PosToVCol4x4[16] = +{ + 0, 2, 0, 2, + 2, 1, 2, 1, + 0, 2, 0, 2, + 2, 1, 2, 1 +}; + +const OMX_U8 armVCM4P10_PosToVCol2x2[4] = +{ + 0, 2, + 2, 1 +}; + +const OMX_U8 armVCM4P10_VMatrix[6][3] = +{ + { 10, 16, 13 }, + { 11, 18, 14 }, + { 13, 20, 16 }, + { 14, 23, 18 }, + { 16, 25, 20 }, + { 18, 29, 23 } +}; diff --git a/src/video/h264_decoder/omxdl/armVCM4P10_InterpolateHalfDiag_Luma.c b/src/video/h264_decoder/omxdl/armVCM4P10_InterpolateHalfDiag_Luma.c new file mode 100644 index 000000000..8732f4fb6 --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCM4P10_InterpolateHalfDiag_Luma.c @@ -0,0 +1,106 @@ +/** + * + * File Name: armVCM4P10_InterpolateHalfDiag_Luma.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * Description: + * This functions will help to calculate Half Pel luma interpolation + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + + +/** + * Function: armVCM4P10_InterpolateHalfDiag_Luma + * + * Description: + * This function performs interpolation for (1/2, 1/2) positions + * around a full-pel position. + * + * Remarks: + * + * [in] pSrc Pointer to top-left corner of block used to interpolate + * in the reconstructed frame plane + * [in] iSrcStep Step of the source buffer. + * [in] iDstStep Step of the destination(interpolation) buffer. + * [in] iWidth Width of the current block + * [in] iHeight Height of the current block + * [out] pDst Pointer to the interpolation buffer of the (1/2,1/2)-pel + * + * Return Value: + * Standard OMXResult value. + * + */ + +OMXResult armVCM4P10_InterpolateHalfDiag_Luma( + const OMX_U8* pSrc, + OMX_U32 iSrcStep, + OMX_U8* pDst, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight +) +{ + OMX_S32 HalfCoeff, pos; + OMX_S16 Buf [21 * 16]; /* 21 rows by 16 pixels per row */ + OMX_U32 y, x; + + /* check for argument error */ + armRetArgErrIf(pSrc == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pDst == NULL, OMX_Sts_BadArgErr) + + /* + * Intermediate values will be 1/2 pel at Horizontal direction + * Starting at (0.5, -2) at top extending to (0.5, height + 3) at bottom + * Buf contains a 2D array of size (iWidth)X(iHeight + 5) + */ + for (y = 0; y < iHeight + 5; y++) + { + for (x = 0; x < iWidth; x++) + { + pos = (y-2) * iSrcStep + x; + HalfCoeff = + pSrc [pos - 2] - + 5 * pSrc [pos - 1] + + 20 * pSrc [pos] + + 20 * pSrc [pos + 1] - + 5 * pSrc [pos + 2] + + pSrc [pos + 3]; + Buf [y * iWidth + x] = (OMX_S16)HalfCoeff; + } /* x */ + } /* y */ + + /* Vertical interpolate */ + for (y = 0; y < iHeight; y++) + { + for (x = 0; x < iWidth; x++) + { + pos = y * iWidth + x; + HalfCoeff = + Buf [pos] - + 5 * Buf [pos + 1 * iWidth] + + 20 * Buf [pos + 2 * iWidth] + + 20 * Buf [pos + 3 * iWidth] - + 5 * Buf [pos + 4 * iWidth] + + Buf [pos + 5 * iWidth]; + + HalfCoeff = (HalfCoeff + 512) >> 10; + HalfCoeff = armClip(0, 255, HalfCoeff); + + pDst [y * iDstStep + x] = (OMX_U8) HalfCoeff; + } + } + + return OMX_Sts_NoErr; +} diff --git a/src/video/h264_decoder/omxdl/armVCM4P10_InterpolateHalfHor_Luma.c b/src/video/h264_decoder/omxdl/armVCM4P10_InterpolateHalfHor_Luma.c new file mode 100644 index 000000000..89c00798a --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCM4P10_InterpolateHalfHor_Luma.c @@ -0,0 +1,82 @@ +/** + * + * File Name: armVCM4P10_InterpolateHalfHor_Luma.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * Description: + * This functions will help to calculate Half Pel luma interpolation + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +/** + * Function: armVCM4P10_InterpolateHalfHor_Luma + * + * Description: + * This function performs interpolation for horizontal 1/2-pel positions + * + * Remarks: + * + * [in] pSrc Pointer to top-left corner of block used to interpolate + * in the reconstructed frame plane + * [in] iSrcStep Step of the source buffer. + * [in] iDstStep Step of the destination(interpolation) buffer. + * [in] iWidth Width of the current block + * [in] iHeight Height of the current block + * [out] pDst Pointer to the interpolation buffer of the 1/2-pel + * + * Return Value: + * Standard OMXResult value. + * + */ + +OMXResult armVCM4P10_InterpolateHalfHor_Luma( + const OMX_U8* pSrc, + OMX_U32 iSrcStep, + OMX_U8* pDst, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight +) +{ + OMX_INT x, y; + OMX_S32 HalfCoeff, pos; + + /* check for argument error */ + armRetArgErrIf(pSrc == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pDst == NULL, OMX_Sts_BadArgErr) + + for (y = 0; y < iHeight; y++) + { + for (x = 0; x < iWidth; x++) + { + pos = y * iSrcStep + x; + HalfCoeff = + pSrc [pos - 2] - + 5 * pSrc [pos - 1] + + 20 * pSrc [pos] + + 20 * pSrc [pos + 1] - + 5 * pSrc [pos + 2] + + pSrc [pos + 3]; + + HalfCoeff = (HalfCoeff + 16) >> 5; + HalfCoeff = armClip(0, 255, HalfCoeff); + + pDst [y * iDstStep + x] = HalfCoeff; + } /* x */ + } /* y */ + + return OMX_Sts_NoErr; +} + diff --git a/src/video/h264_decoder/omxdl/armVCM4P10_InterpolateHalfVer_Luma.c b/src/video/h264_decoder/omxdl/armVCM4P10_InterpolateHalfVer_Luma.c new file mode 100644 index 000000000..a73aa308a --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCM4P10_InterpolateHalfVer_Luma.c @@ -0,0 +1,84 @@ +/** + * + * File Name: armVCM4P10_InterpolateHalfVer_Luma.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * Description: + * This functions will help to calculate Half Pel luma interpolation + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +/** + * Function: armVCM4P10_InterpolateHalfVer_Luma + * + * Description: + * This function performs interpolation for vertical 1/2-pel positions + * around a full-pel position. + * + * Remarks: + * + * [in] pSrc Pointer to top-left corner of block used to interpolate + * in the reconstructed frame plane + * [in] iSrcStep Step of the source buffer. + * [in] iDstStep Step of the destination(interpolation) buffer. + * [in] iWidth Width of the current block + * [in] iHeight Height of the current block + * [out] pDst Pointer to the interpolation buffer of the 1/2-pel + * + * Return Value: + * Standard OMXResult value. + * + */ + +OMXResult armVCM4P10_InterpolateHalfVer_Luma( + const OMX_U8* pSrc, + OMX_U32 iSrcStep, + OMX_U8* pDst, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight +) +{ + OMX_S32 HalfCoeff, pos; + OMX_INT y, x; + + /* check for argument error */ + armRetArgErrIf(pSrc == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pDst == NULL, OMX_Sts_BadArgErr) + + + for (y = 0; y < iHeight; y++) + { + for (x = 0; x < iWidth; x++) + { + pos = y * iSrcStep + x; + HalfCoeff = + pSrc [pos - (OMX_S32)(2 * iSrcStep)] - + 5 * pSrc [pos - (OMX_S32)(1 * iSrcStep)] + + 20 * pSrc [pos] + + 20 * pSrc [pos + 1 * iSrcStep] - + 5 * pSrc [pos + 2 * iSrcStep] + + pSrc [pos + 3 * iSrcStep]; + + HalfCoeff = (HalfCoeff + 16) >> 5; + HalfCoeff = armClip(0, 255, HalfCoeff); + + pDst [y * iDstStep + x] = (OMX_U8) HalfCoeff; + } + } + + return OMX_Sts_NoErr; +} + diff --git a/src/video/h264_decoder/omxdl/armVCM4P10_Interpolate_Chroma.c b/src/video/h264_decoder/omxdl/armVCM4P10_Interpolate_Chroma.c new file mode 100644 index 000000000..7cc10e849 --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCM4P10_Interpolate_Chroma.c @@ -0,0 +1,121 @@ +/** + * + * File Name: armVCM4P10_Interpolate_Chroma.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * Description: + * This function will calculate interpolation for chroma components + * + */ + +#include "omxtypes.h" +#include "armOMX.h" + +#include "armCOMM.h" + +#ifdef H264BSD_N64 +#include "../../rsph264_internal.h" +#endif + +/** + * Function: armVCM4P10_Interpolate_Chroma + * + * Description: + * This function performs interpolation for chroma components. + * + * Remarks: + * + * [in] pSrc Pointer to top-left corner of block used to + * interpolate in the reconstructed frame plane + * [in] iSrcStep Step of the source buffer. + * [in] iDstStep Step of the destination(interpolation) buffer. + * [in] iWidth Width of the current block + * [in] iHeight Height of the current block + * [in] dx Fractional part of horizontal motion vector + * component in 1/8 pixel unit (0~7) + * [in] dy Fractional part of vertical motion vector + * component in 1/8 pixel unit (0~7) + * [out] pDst Pointer to the interpolation buffer + * + * Return Value: + * Standard OMXResult value. + * + */ + OMXResult armVCM4P10_Interpolate_Chroma( + OMX_U8 *pSrc, + OMX_U32 iSrcStep, + OMX_U8 *pDst, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight, + OMX_U32 dx, + OMX_U32 dy +) +{ +#if H264BSD_N64 + rsph264_queue_interpolate_chroma(RSPH264_CACHE_SKIP_SOURCE|RSPH264_CACHE_SKIP_DEST, + pSrc, iSrcStep, + pDst, iDstStep, + iWidth, iHeight, + dx, dy); + // rsph264_sync(); +#else + OMX_U32 EightMinusdx = 8 - dx; + OMX_U32 EightMinusdy = 8 - dy; + OMX_U32 ACoeff, BCoeff, CCoeff, DCoeff; + OMX_U32 x, y; + + /* check for argument error */ + armRetArgErrIf(pSrc == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pDst == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(dx > 7, OMX_Sts_BadArgErr) + armRetArgErrIf(dy > 7, OMX_Sts_BadArgErr) + armRetArgErrIf(iSrcStep == 0, OMX_Sts_BadArgErr) + armRetArgErrIf(iDstStep == 0, OMX_Sts_BadArgErr) + armRetArgErrIf(iWidth == 0, OMX_Sts_BadArgErr) + armRetArgErrIf(iHeight == 0, OMX_Sts_BadArgErr) + + /* if fractionl mv is not (0, 0) */ + if (dx != 0 || dy != 0) + { + ACoeff = EightMinusdx * EightMinusdy; + BCoeff = dx * EightMinusdy; + CCoeff = EightMinusdx * dy; + DCoeff = dx * dy; + + for (y = 0; y < iHeight; y++) + { + for (x = 0; x < iWidth; x++) + { + pDst [y * iDstStep + x] = ( + ACoeff * pSrc [y * iSrcStep + x] + + BCoeff * pSrc [y * iSrcStep + x + 1] + + CCoeff * pSrc [(y + 1) * iSrcStep + x] + + DCoeff * pSrc [(y + 1) * iSrcStep + x + 1] + + 32) >> 6; + } + } + } + else + { + for (y = 0; y < iHeight; y++) + { + for (x = 0; x < iWidth; x++) + { + pDst [y * iDstStep + x] = pSrc [y * iSrcStep + x]; + } + } + } +#endif + return OMX_Sts_NoErr; +} + +/***************************************************************************** + * END OF FILE + *****************************************************************************/ + diff --git a/src/video/h264_decoder/omxdl/armVCM4P10_Interpolate_Luma.c b/src/video/h264_decoder/omxdl/armVCM4P10_Interpolate_Luma.c new file mode 100644 index 000000000..9c6acfd60 --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCM4P10_Interpolate_Luma.c @@ -0,0 +1,210 @@ +/** + * + * File Name: armVCM4P10_Interpolate_Luma.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * Description: + * This function will calculate interpolation for luma components + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +#ifdef H264BSD_N64 +#include "../../rsph264_internal.h" +#endif + +/** + * Function: armM4P10_Copy + * + * Description: + * This function performs copy a block of data from source to destination + * + * Remarks: + * + * [in] pSrc Pointer to top-left corner of block + * [in] iSrcStep Step of the source buffer. + * [in] iDstStep Step of the destination buffer. + * [in] iWidth Width of the current block + * [in] iHeight Height of the current block + * [out] pDst Pointer to the interpolation buffer + * + * Return Value: + * Standard OMXResult value. + * + */ +static OMXResult armM4P10_Copy( + const OMX_U8* pSrc, + OMX_U32 iSrcStep, + OMX_U8* pDst, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight +) +{ + OMX_U32 x, y; + + for (y = 0; y < iHeight; y++) + { + for (x = 0; x < iWidth; x++) + { + pDst [y * iDstStep + x] = pSrc [y * iSrcStep + x]; + } + } + + return OMX_Sts_NoErr; +} + +/** + * Function: armVCM4P10_Interpolate_Luma + * + * Description: + * This function performs interpolation for luma components. + * + * Remarks: + * + * [in] pSrc Pointer to top-left corner of block used to + * interpolate in the reconstructed frame plane + * [in] iSrcStep Step of the source buffer. + * [in] iDstStep Step of the destination(interpolation) buffer. + * [in] iWidth Width of the current block + * [in] iHeight Height of the current block + * [in] dx Fractional part of horizontal motion vector + * component in 1/4 pixel unit (0~3) + * [in] dy Fractional part of vertical motion vector + * component in 1/4 pixel unit (0~3) + * [out] pDst Pointer to the interpolation buffer + * + * Return Value: + * Standard OMXResult value. + * + */ + + OMXResult armVCM4P10_Interpolate_Luma( + const OMX_U8 *pSrc, + OMX_U32 iSrcStep, + OMX_U8 *pDst, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight, + OMX_U32 dx, + OMX_U32 dy +) +{ +#if H264BSD_N64 + (void)armM4P10_Copy; + + rsph264_queue_interpolate_luma(RSPH264_CACHE_SKIP_SOURCE|RSPH264_CACHE_SKIP_DEST, + pSrc, iSrcStep, + pDst, iDstStep, + iWidth, iHeight, + dx, dy); + // rsph264_sync(); +#else + OMX_U8 pBuf1 [16*16]; + const OMX_U8 *pSrcHalfHor = pSrc; + const OMX_U8 *pSrcHalfVer = pSrc; + + /* check for argument error */ + armRetArgErrIf(pSrc == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pDst == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(dx > 3, OMX_Sts_BadArgErr) + armRetArgErrIf(dy > 3, OMX_Sts_BadArgErr) + + /* Work out positions for half pixel interpolation */ + if (dx == 3) + { + pSrcHalfVer += 1; + } + if (dy == 3) + { + pSrcHalfHor += iSrcStep; + } + + /* Switch on type of pixel + * Pixels are named 'a' to 's' as in the H.264 standard + */ + if (dx == 0 && dy == 0) + { + /* G */ + armM4P10_Copy(pSrc, iSrcStep, pDst, iDstStep, iWidth, iHeight); + } + else if (dy == 0) + { + /* a, b, c */ + armVCM4P10_InterpolateHalfHor_Luma + (pSrcHalfHor, iSrcStep, pDst, iDstStep, iWidth, iHeight); + + if (dx == 1 || dx == 3) + { + armVCCOMM_Average + (pDst, pSrcHalfVer, iDstStep, iSrcStep, pDst, iDstStep, iWidth, iHeight); + } + } + else if (dx == 0) + { + /* d, h, n */ + armVCM4P10_InterpolateHalfVer_Luma + (pSrcHalfVer, iSrcStep, pDst, iDstStep, iWidth, iHeight); + + if (dy == 1 || dy == 3) + { + armVCCOMM_Average + (pDst, pSrcHalfHor, iDstStep, iSrcStep, pDst, iDstStep, iWidth, iHeight); + } + } + else if (dx == 2 || dy == 2) + { + /* j */ + armVCM4P10_InterpolateHalfDiag_Luma + (pSrc, iSrcStep, pDst, iDstStep, iWidth, iHeight); + + if (dx == 1 || dx == 3) + { + /* i, k */ + armVCM4P10_InterpolateHalfVer_Luma + (pSrcHalfVer, iSrcStep, pBuf1, iWidth, iWidth, iHeight); + + armVCCOMM_Average + (pDst, pBuf1, iDstStep, iWidth, pDst, iDstStep, iWidth, iHeight); + } + if (dy == 1 || dy == 3) + { + /* f,q */ + armVCM4P10_InterpolateHalfHor_Luma + (pSrcHalfHor, iSrcStep, pBuf1, iWidth, iWidth, iHeight); + + armVCCOMM_Average + (pDst, pBuf1, iDstStep, iWidth, pDst, iDstStep, iWidth, iHeight); + } + } + else /* dx=1,3 and dy=1,3 */ + { + /* e, g, p, r */ + armVCM4P10_InterpolateHalfHor_Luma + (pSrcHalfHor, iSrcStep, pBuf1, iWidth, iWidth, iHeight); + + armVCM4P10_InterpolateHalfVer_Luma + (pSrcHalfVer, iSrcStep, pDst, iDstStep, iWidth, iHeight); + + armVCCOMM_Average + (pBuf1, pDst, iWidth, iDstStep, pDst, iDstStep, iWidth, iHeight); + } +#endif + + return OMX_Sts_NoErr; +} + +/***************************************************************************** + * END OF FILE + *****************************************************************************/ diff --git a/src/video/h264_decoder/omxdl/armVCM4P10_PredictIntraDC4x4.c b/src/video/h264_decoder/omxdl/armVCM4P10_PredictIntraDC4x4.c new file mode 100644 index 000000000..b713073d1 --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCM4P10_PredictIntraDC4x4.c @@ -0,0 +1,88 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: armVCM4P10_PredictIntraDC4x4.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 4x4 intra prediction module + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +/* + * Description: + * Perform DC style intra prediction, averaging upper and left block + * + * Parameters: + * [in] pSrcLeft Pointer to the buffer of 16 left coefficients: + * p[x, y] (x = -1, y = 0..3) + * [in] pSrcAbove Pointer to the buffer of 16 above coefficients: + * p[x,y] (x = 0..3, y = -1) + * [in] leftStep Step of left coefficient buffer + * [in] dstStep Step of the destination buffer + * [in] availability Neighboring 16x16 MB availability flag + * [out] pDst Pointer to the destination buffer + * + * Return Value: + * None + */ + +void armVCM4P10_PredictIntraDC4x4( + const OMX_U8* pSrcLeft, + const OMX_U8 *pSrcAbove, + OMX_U8* pDst, + OMX_INT leftStep, + OMX_INT dstStep, + OMX_S32 availability +) +{ + int x, y, Sum=0, Count = 0; + + if (availability & OMX_VC_LEFT) + { + for (y=0; y<4; y++) + { + Sum += pSrcLeft[y*leftStep]; + } + Count++; + } + if (availability & OMX_VC_UPPER) + { + for (x=0; x<4; x++) + { + Sum += pSrcAbove[x]; + } + Count++; + } + if (Count==0) + { + Sum = 128; + } + else if (Count==1) + { + Sum = (Sum + 2) >> 2; + } + else /* Count = 2 */ + { + Sum = (Sum + 4) >> 3; + } + for (y=0; y<4; y++) + { + for (x=0; x<4; x++) + { + pDst[y*dstStep+x] = (OMX_U8)Sum; + } + } +} diff --git a/src/video/h264_decoder/omxdl/armVCM4P10_QuantTables.c b/src/video/h264_decoder/omxdl/armVCM4P10_QuantTables.c new file mode 100644 index 000000000..f0b5bb0c9 --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCM4P10_QuantTables.c @@ -0,0 +1,31 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: armVCM4P10_QuantTables.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 inverse quantize tables + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armVC.h" + +const OMX_U32 armVCM4P10_MFMatrix[6][3] = +{ + {13107, 5243, 8066}, + {11916, 4660, 7490}, + {10082, 4194, 6554}, + { 9362, 3647, 5825}, + { 8192, 3355, 5243}, + { 7282, 2893, 4559} +}; diff --git a/src/video/h264_decoder/omxdl/armVCM4P10_TransformResidual4x4.c b/src/video/h264_decoder/omxdl/armVCM4P10_TransformResidual4x4.c new file mode 100644 index 000000000..b1c53057f --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCM4P10_TransformResidual4x4.c @@ -0,0 +1,122 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: armVCM4P10_TransformResidual4x4.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 transform module + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +#ifdef H264BSD_TEST +#include +#include +#endif + +/* + * Description: + * Transform Residual 4x4 Coefficients + * + * Parameters: + * [in] pSrc Source 4x4 block + * [out] pDst Destination 4x4 block + * + */ + +void armVCM4P10_TransformResidual4x4(OMX_S16* pDst, OMX_S16 *pSrc) +{ + int i; + + // printf("\nStart:\n"); + // for (i=0;i<4;i++) { + // printf("%04x %04x %04x %04x\n", (uint16_t)pSrc[i*4+0], (uint16_t)pSrc[i*4+1], (uint16_t)pSrc[i*4+2], (uint16_t)pSrc[i*4+3]); + // } + + /* Transform rows */ + for (i=0; i<16; i+=4) + { + int d0 = pSrc[i+0]; + int d1 = pSrc[i+1]; + int d2 = pSrc[i+2]; + int d3 = pSrc[i+3]; + int e0 = d0 + d2; + int e1 = d0 - d2; + int e2 = (d1>>1) - d3; + int e3 = d1 + (d3>>1); + int f0 = e0 + e3; + int f1 = e1 + e2; + int f2 = e1 - e2; + int f3 = e0 - e3; + #ifdef H264BSD_TEST + // RASKY: detect overflow here... this should never happen + // on non-broken bitstreams, but it's useful to make sure + // our exhaustive tests are sound. + assert(f0 == (OMX_S16)f0); + assert(f1 == (OMX_S16)f1); + assert(f2 == (OMX_S16)f2); + assert(f3 == (OMX_S16)f3); + #endif + pDst[i+0] = (OMX_S16)f0; + pDst[i+1] = (OMX_S16)f1; + pDst[i+2] = (OMX_S16)f2; + pDst[i+3] = (OMX_S16)f3; + } + + // printf("After rows:\n"); + // for (i=0;i<4;i++) { + // printf("%04x %04x %04x %04x\n", (uint16_t)pDst[i*4+0], (uint16_t)pDst[i*4+1], (uint16_t)pDst[i*4+2], (uint16_t)pDst[i*4+3]); + // } + + /* Transform columns */ + for (i=0; i<4; i++) + { + int f0 = pDst[i+0]; + int f1 = pDst[i+4]; + int f2 = pDst[i+8]; + int f3 = pDst[i+12]; + int g0 = f0 + f2; + int g1 = f0 - f2; + int g2 = (f1>>1) - f3; + int g3 = f1 + (f3>>1); + int h0 = g0 + g3; + int h1 = g1 + g2; + int h2 = g1 - g2; + int h3 = g0 - g3; + int j0 = (h0 + 32)>>6; + int j1 = (h1 + 32)>>6; + int j2 = (h2 + 32)>>6; + int j3 = (h3 + 32)>>6; + #ifdef H264BSD_TEST + // RASKY: detect overflow here... this should never happen + // on non-broken bitstreams, but it's useful to make sure + // our exhaustive tests are sound. + assert(j0 == (OMX_S16)j0); + assert(j1 == (OMX_S16)j1); + assert(j2 == (OMX_S16)j2); + assert(j3 == (OMX_S16)j3); + #endif + pDst[i+0] = (OMX_S16)j0; + pDst[i+4] = (OMX_S16)j1; + pDst[i+8] = (OMX_S16)j2; + pDst[i+12] = (OMX_S16)j3; + } + + // printf("After cols:\n"); + // for (i=0;i<4;i++) { + // printf("%04x %04x %04x %04x\n", (uint16_t)pDst[i*4+0], (uint16_t)pDst[i*4+1], (uint16_t)pDst[i*4+2], (uint16_t)pDst[i*4+3]); + // } +} + diff --git a/src/video/h264_decoder/omxdl/armVCM4P10_UnpackBlock2x2.c b/src/video/h264_decoder/omxdl/armVCM4P10_UnpackBlock2x2.c new file mode 100644 index 000000000..dda49f67d --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCM4P10_UnpackBlock2x2.c @@ -0,0 +1,78 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: armVCM4P10_UnpackBlock2x2.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 inverse quantize and transform helper module + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armVC.h" + +/* + * Description + * Unpack a 2x2 block of coefficient-residual pair values + * + * Parameters: + * [in] ppSrc Double pointer to residual coefficient-position pair + * buffer output by CALVC decoding + * [out] ppSrc *ppSrc is updated to the start of next non empty block + * [out] pDst Pointer to unpacked 4x4 block + */ + +void armVCM4P10_UnpackBlock2x2( + const OMX_U8 **ppSrc, + OMX_S16* pDst +) +{ + const OMX_U8 *pSrc = *ppSrc; + int i; + int Flag, Value; + + for (i=0; i<4; i++) + { + pDst[i] = 0; + } + + do + { + Flag = *pSrc++; + if (Flag & 0x10) + { + /* 16 bit */ + Value = *pSrc++; + Value = Value | ((*pSrc++)<<8); + if (Value & 0x8000) + { + Value -= 0x10000; + } + } + else + { + /* 8 bit */ + Value = *pSrc++; + if (Value & 0x80) + { + Value -= 0x100; + } + } + i = Flag & 15; + pDst[i] = (OMX_S16)Value; + } + while ((Flag & 0x20)==0); + + *ppSrc = pSrc; +} + +/* End of file */ diff --git a/src/video/h264_decoder/omxdl/armVCM4P10_UnpackBlock4x4.c b/src/video/h264_decoder/omxdl/armVCM4P10_UnpackBlock4x4.c new file mode 100644 index 000000000..3c0dcbd1b --- /dev/null +++ b/src/video/h264_decoder/omxdl/armVCM4P10_UnpackBlock4x4.c @@ -0,0 +1,78 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: armVCM4P10_UnpackBlock4x4.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 inverse quantize and transform helper module + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armVC.h" + +/* + * Description + * Unpack a 4x4 block of coefficient-residual pair values + * + * Parameters: + * [in] ppSrc Double pointer to residual coefficient-position pair + * buffer output by CALVC decoding + * [out] ppSrc *ppSrc is updated to the start of next non empty block + * [out] pDst Pointer to unpacked 4x4 block + */ + +void armVCM4P10_UnpackBlock4x4( + const OMX_U8 **ppSrc, + OMX_S16* pDst +) +{ + const OMX_U8 *pSrc = *ppSrc; + int i; + int Flag, Value; + + for (i=0; i<16; i++) + { + pDst[i] = 0; + } + + do + { + Flag = *pSrc++; + if (Flag & 0x10) + { + /* 16 bit */ + Value = *pSrc++; + Value = Value | ((*pSrc++)<<8); + if (Value & 0x8000) + { + Value -= 0x10000; + } + } + else + { + /* 8 bit */ + Value = *pSrc++; + if (Value & 0x80) + { + Value -= 0x100; + } + } + i = Flag & 15; + pDst[i] = (OMX_S16)Value; + } + while ((Flag & 0x20)==0); + + *ppSrc = pSrc; +} + +/* End of file */ diff --git a/src/video/h264_decoder/omxdl/omxVC.h b/src/video/h264_decoder/omxdl/omxVC.h new file mode 100644 index 000000000..dca991e9b --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVC.h @@ -0,0 +1,4501 @@ +/** + * File: omxVC.h + * Brief: OpenMAX DL v1.0.2 - Video Coding library + * + * Copyright � 2005-2008 The Khronos Group Inc. All Rights Reserved. + * + * These materials are protected by copyright laws and contain material + * proprietary to the Khronos Group, Inc. You may use these materials + * for implementing Khronos specifications, without altering or removing + * any trademark, copyright or other notice from the specification. + * + * Khronos Group makes no, and expressly disclaims any, representations + * or warranties, express or implied, regarding these materials, including, + * without limitation, any implied warranties of merchantability or fitness + * for a particular purpose or non-infringement of any intellectual property. + * Khronos Group makes no, and expressly disclaims any, warranties, express + * or implied, regarding the correctness, accuracy, completeness, timeliness, + * and reliability of these materials. + * + * Under no circumstances will the Khronos Group, or any of its Promoters, + * Contributors or Members or their respective partners, officers, directors, + * employees, agents or representatives be liable for any damages, whether + * direct, indirect, special or consequential damages for lost revenues, + * lost profits, or otherwise, arising from or in connection with these + * materials. + * + * Khronos and OpenMAX are trademarks of the Khronos Group Inc. + * + */ + +/* *****************************************************************************************/ + +#ifndef _OMXVC_H_ +#define _OMXVC_H_ + +#include "omxtypes.h" + +#ifdef __cplusplus +extern "C" { +#endif + + +/* 6.1.1.1 Motion Vectors */ +/* In omxVC, motion vectors are represented as follows: */ + +typedef struct { + OMX_S16 dx; + OMX_S16 dy; +} OMXVCMotionVector; + + + +/** + * Function: omxVCCOMM_Average_8x (6.1.3.1.1) + * + * Description: + * This function calculates the average of two 8x4, 8x8, or 8x16 blocks. The + * result is rounded according to (a+b+1)/2. The block average function can + * be used in conjunction with half-pixel interpolation to obtain quarter + * pixel motion estimates, as described in [ISO14496-10], subclause 8.4.2.2.1. + * + * Input Arguments: + * + * pPred0 - Pointer to the top-left corner of reference block 0 + * pPred1 - Pointer to the top-left corner of reference block 1 + * iPredStep0 - Step of reference block 0 + * iPredStep1 - Step of reference block 1 + * iDstStep - Step of the destination buffer. + * iHeight - Height of the blocks + * + * Output Arguments: + * + * pDstPred - Pointer to the destination buffer. 8-byte aligned. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned under any of the following + * conditions: + * - one or more of the following pointers is NULL: pPred0, pPred1, or + * pDstPred. + * - pDstPred is not aligned on an 8-byte boundary. + * - iPredStep0 <= 0 or iPredStep0 is not a multiple of 8. + * - iPredStep1 <= 0 or iPredStep1 is not a multiple of 8. + * - iDstStep <= 0 or iDstStep is not a multiple of 8. + * - iHeight is not 4, 8, or 16. + * + */ +__attribute__((weak)) + OMXResult omxVCCOMM_Average_8x ( + const OMX_U8 *pPred0, + const OMX_U8 *pPred1, + OMX_U32 iPredStep0, + OMX_U32 iPredStep1, + OMX_U8 *pDstPred, + OMX_U32 iDstStep, + OMX_U32 iHeight +); + + + +/** + * Function: omxVCCOMM_Average_16x (6.1.3.1.2) + * + * Description: + * This function calculates the average of two 16x16 or 16x8 blocks. The + * result is rounded according to (a+b+1)/2. The block average function can + * be used in conjunction with half-pixel interpolation to obtain quarter + * pixel motion estimates, as described in [ISO14496-10], subclause 8.4.2.2.1. + * + * Input Arguments: + * + * pPred0 - Pointer to the top-left corner of reference block 0 + * pPred1 - Pointer to the top-left corner of reference block 1 + * iPredStep0 - Step of reference block 0 + * iPredStep1 - Step of reference block 1 + * iDstStep - Step of the destination buffer + * iHeight - Height of the blocks + * + * Output Arguments: + * + * pDstPred - Pointer to the destination buffer. 16-byte aligned. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned under any of the following + * conditions: + * - one or more of the following pointers is NULL: pPred0, pPred1, or + * pDstPred. + * - pDstPred is not aligned on a 16-byte boundary. + * - iPredStep0 <= 0 or iPredStep0 is not a multiple of 16. + * - iPredStep1 <= 0 or iPredStep1 is not a multiple of 16. + * - iDstStep <= 0 or iDstStep is not a multiple of 16. + * - iHeight is not 8 or 16. + * + */ + __attribute__((weak)) + OMXResult omxVCCOMM_Average_16x ( + const OMX_U8 *pPred0, + const OMX_U8 *pPred1, + OMX_U32 iPredStep0, + OMX_U32 iPredStep1, + OMX_U8 *pDstPred, + OMX_U32 iDstStep, + OMX_U32 iHeight +); + + + +/** + * Function: omxVCCOMM_ExpandFrame_I (6.1.3.2.1) + * + * Description: + * This function expands a reconstructed frame in-place. The unexpanded + * source frame should be stored in a plane buffer with sufficient space + * pre-allocated for edge expansion, and the input frame should be located in + * the plane buffer center. This function executes the pixel expansion by + * replicating source frame edge pixel intensities in the empty pixel + * locations (expansion region) between the source frame edge and the plane + * buffer edge. The width/height of the expansion regions on the + * horizontal/vertical edges is controlled by the parameter iExpandPels. + * + * Input Arguments: + * + * pSrcDstPlane - pointer to the top-left corner of the frame to be + * expanded; must be aligned on an 8-byte boundary. + * iFrameWidth - frame width; must be a multiple of 8. + * iFrameHeight -frame height; must be a multiple of 8. + * iExpandPels - number of pixels to be expanded in the horizontal and + * vertical directions; must be a multiple of 8. + * iPlaneStep - distance, in bytes, between the start of consecutive lines + * in the plane buffer; must be larger than or equal to + * (iFrameWidth + 2 * iExpandPels). + * + * Output Arguments: + * + * pSrcDstPlane -Pointer to the top-left corner of the frame (NOT the + * top-left corner of the plane); must be aligned on an 8-byte + * boundary. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned under any of the following + * conditions: + * - pSrcDstPlane is NULL. + * - pSrcDstPlane is not aligned on an 8-byte boundary. + * - one of the following parameters is either equal to zero or is a + * non-multiple of 8: iFrameHeight, iFrameWidth, iPlaneStep, or + * iExpandPels. + * - iPlaneStep < (iFrameWidth + 2 * iExpandPels). + * + */ + __attribute__((weak)) +OMXResult omxVCCOMM_ExpandFrame_I ( + OMX_U8 *pSrcDstPlane, + OMX_U32 iFrameWidth, + OMX_U32 iFrameHeight, + OMX_U32 iExpandPels, + OMX_U32 iPlaneStep +); + + + +/** + * Function: omxVCCOMM_Copy8x8 (6.1.3.3.1) + * + * Description: + * Copies the reference 8x8 block to the current block. + * + * Input Arguments: + * + * pSrc - pointer to the reference block in the source frame; must be + * aligned on an 8-byte boundary. + * step - distance between the starts of consecutive lines in the reference + * frame, in bytes; must be a multiple of 8 and must be larger than + * or equal to 8. + * + * Output Arguments: + * + * pDst - pointer to the destination block; must be aligned on an 8-byte + * boundary. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned under any of the following + * conditions: + * - one or more of the following pointers is NULL: pSrc, pDst + * - one or more of the following pointers is not aligned on an 8-byte + * boundary: pSrc, pDst + * - step <8 or step is not a multiple of 8. + * + */ + __attribute__((weak)) + OMXResult omxVCCOMM_Copy8x8 ( + const OMX_U8 *pSrc, + OMX_U8 *pDst, + OMX_INT step +); + + + +/** + * Function: omxVCCOMM_Copy16x16 (6.1.3.3.2) + * + * Description: + * Copies the reference 16x16 macroblock to the current macroblock. + * + * Input Arguments: + * + * pSrc - pointer to the reference macroblock in the source frame; must be + * aligned on a 16-byte boundary. + * step - distance between the starts of consecutive lines in the reference + * frame, in bytes; must be a multiple of 16 and must be larger + * than or equal to 16. + * + * Output Arguments: + * + * pDst - pointer to the destination macroblock; must be aligned on a + * 16-byte boundary. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned under any of the following + * conditions: + * - one or more of the following pointers is NULL: pSrc, pDst + * - one or more of the following pointers is not aligned on a 16-byte + * boundary: pSrc, pDst + * - step <16 or step is not a multiple of 16. + * + */ + __attribute__((weak)) +OMXResult omxVCCOMM_Copy16x16 ( + const OMX_U8 *pSrc, + OMX_U8 *pDst, + OMX_INT step +); + + + +/** + * Function: omxVCCOMM_ComputeTextureErrorBlock_SAD (6.1.4.1.1) + * + * Description: + * Computes texture error of the block; also returns SAD. + * + * Input Arguments: + * + * pSrc - pointer to the source plane; must be aligned on an 8-byte + * boundary. + * srcStep - step of the source plane + * pSrcRef - pointer to the reference buffer, an 8x8 block; must be aligned + * on an 8-byte boundary. + * + * Output Arguments: + * + * pDst - pointer to the destination buffer, an 8x8 block; must be aligned + * on an 8-byte boundary. + * pDstSAD - pointer to the Sum of Absolute Differences (SAD) value + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments + * - At least one of the following + * pointers is NULL: pSrc, pSrcRef, pDst and pDstSAD. + * - pSrc is not 8-byte aligned. + * - SrcStep <= 0 or srcStep is not a multiple of 8. + * - pSrcRef is not 8-byte aligned. + * - pDst is not 8-byte aligned. + * + */ + __attribute__((weak)) +OMXResult omxVCCOMM_ComputeTextureErrorBlock_SAD ( + const OMX_U8 *pSrc, + OMX_INT srcStep, + const OMX_U8 *pSrcRef, + OMX_S16 *pDst, + OMX_INT *pDstSAD +); + + + +/** + * Function: omxVCCOMM_ComputeTextureErrorBlock (6.1.4.1.2) + * + * Description: + * Computes the texture error of the block. + * + * Input Arguments: + * + * pSrc - pointer to the source plane. This should be aligned on an 8-byte + * boundary. + * srcStep - step of the source plane + * pSrcRef - pointer to the reference buffer, an 8x8 block. This should be + * aligned on an 8-byte boundary. + * + * Output Arguments: + * + * pDst - pointer to the destination buffer, an 8x8 block. This should be + * aligned on an 8-byte boundary. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments: + * - At least one of the following pointers is NULL: + * pSrc, pSrcRef, pDst. + * - pSrc is not 8-byte aligned. + * - SrcStep <= 0 or srcStep is not a multiple of 8. + * - pSrcRef is not 8-byte aligned. + * - pDst is not 8-byte aligned + * + */ + __attribute__((weak)) +OMXResult omxVCCOMM_ComputeTextureErrorBlock ( + const OMX_U8 *pSrc, + OMX_INT srcStep, + const OMX_U8 *pSrcRef, + OMX_S16 *pDst +); + + + +/** + * Function: omxVCCOMM_LimitMVToRect (6.1.4.1.3) + * + * Description: + * Limits the motion vector associated with the current block/macroblock to + * prevent the motion compensated block/macroblock from moving outside a + * bounding rectangle as shown in Figure 6-1. + * + * Input Arguments: + * + * pSrcMV - pointer to the motion vector associated with the current block + * or macroblock + * pRectVOPRef - pointer to the bounding rectangle + * Xcoord, Ycoord - coordinates of the current block or macroblock + * size - size of the current block or macroblock; must be equal to 8 or + * 16. + * + * Output Arguments: + * + * pDstMV - pointer to the limited motion vector + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments. Returned if one or more of the + * following conditions is true: + * - at least one of the following pointers is NULL: + * pSrcMV, pDstMV, or pRectVOPRef. + * - size is not equal to either 8 or 16. + * - the width or height of the bounding rectangle is less than + * twice the block size. + */ + __attribute__((weak)) +OMXResult omxVCCOMM_LimitMVToRect ( + const OMXVCMotionVector *pSrcMV, + OMXVCMotionVector *pDstMV, + const OMXRect *pRectVOPRef, + OMX_INT Xcoord, + OMX_INT Ycoord, + OMX_INT size +); + + + +/** + * Function: omxVCCOMM_SAD_16x (6.1.4.1.4) + * + * Description: + * This function calculates the SAD for 16x16 and 16x8 blocks. + * + * Input Arguments: + * + * pSrcOrg - Pointer to the original block; must be aligned on a 16-byte + * boundary. + * iStepOrg - Step of the original block buffer + * pSrcRef - Pointer to the reference block + * iStepRef - Step of the reference block buffer + * iHeight - Height of the block + * + * Output Arguments: + * + * pDstSAD - Pointer of result SAD + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments. Returned if one or more of the + * following conditions is true: + * - at least one of the following pointers is NULL: + * pSrcOrg, pDstSAD, or pSrcRef + * - pSrcOrg is not 16-byte aligned. + * - iStepOrg <= 0 or iStepOrg is not a multiple of 16 + * - iStepRef <= 0 or iStepRef is not a multiple of 16 + * - iHeight is not 8 or 16 + * + */ + __attribute__((weak)) +OMXResult omxVCCOMM_SAD_16x ( + const OMX_U8 *pSrcOrg, + OMX_U32 iStepOrg, + const OMX_U8 *pSrcRef, + OMX_U32 iStepRef, + OMX_S32 *pDstSAD, + OMX_U32 iHeight +); + + + +/** + * Function: omxVCCOMM_SAD_8x (6.1.4.1.5) + * + * Description: + * This function calculates the SAD for 8x16, 8x8, 8x4 blocks. + * + * Input Arguments: + * + * pSrcOrg - Pointer to the original block; must be aligned on a 8-byte + * boundary. + * iStepOrg - Step of the original block buffer + * pSrcRef - Pointer to the reference block + * iStepRef - Step of the reference block buffer + * iHeight - Height of the block + * + * Output Arguments: + * + * pDstSAD -Pointer of result SAD + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments. Returned if one or more of the + * following conditions is true: + * - at least one of the following pointers is NULL: + * pSrcOrg, pDstSAD, or pSrcRef + * - pSrcOrg is not 8-byte aligned. + * - iStepOrg <= 0 or iStepOrg is not a multiple of 8 + * - iStepRef <= 0 or iStepRef is not a multiple of 8 + * - iHeight is not 4, 8 or 16 + * + */ + __attribute__((weak)) +OMXResult omxVCCOMM_SAD_8x ( + const OMX_U8 *pSrcOrg, + OMX_U32 iStepOrg, + const OMX_U8 *pSrcRef, + OMX_U32 iStepRef, + OMX_S32*pDstSAD, + OMX_U32 iHeight +); + + + +/* 6.2.1.1 Direction */ +/* The direction enumerator is used with functions that perform AC/DC prediction and zig-zag scan. */ + +enum { + OMX_VC_NONE = 0, + OMX_VC_HORIZONTAL = 1, + OMX_VC_VERTICAL = 2 +}; + + + +/* 6.2.1.2 Bilinear Interpolation */ +/* The bilinear interpolation enumerator is used with motion estimation, motion compensation, and reconstruction functions. */ + +enum { + OMX_VC_INTEGER_PIXEL = 0, /* case a */ + OMX_VC_HALF_PIXEL_X = 1, /* case b */ + OMX_VC_HALF_PIXEL_Y = 2, /* case c */ + OMX_VC_HALF_PIXEL_XY = 3 /* case d */ +}; + + + +/* 6.2.1.3 Neighboring Macroblock Availability */ +/* Neighboring macroblock availability is indicated using the following flags: */ + +enum { + OMX_VC_UPPER = 1, /** above macroblock is available */ + OMX_VC_LEFT = 2, /** left macroblock is available */ + OMX_VC_CENTER = 4, + OMX_VC_RIGHT = 8, + OMX_VC_LOWER = 16, + OMX_VC_UPPER_LEFT = 32, /** above-left macroblock is available */ + OMX_VC_UPPER_RIGHT = 64, /** above-right macroblock is available */ + OMX_VC_LOWER_LEFT = 128, + OMX_VC_LOWER_RIGHT = 256 +}; + + + +/* 6.2.1.4 Video Components */ +/* A data type that enumerates video components is defined as follows: */ + +typedef enum { + OMX_VC_LUMINANCE, /** Luminance component */ + OMX_VC_CHROMINANCE /** chrominance component */ +} OMXVCM4P2VideoComponent; + + + +/* 6.2.1.5 MacroblockTypes */ +/* A data type that enumerates macroblock types is defined as follows: */ + +typedef enum { + OMX_VC_INTER = 0, /** P picture or P-VOP */ + OMX_VC_INTER_Q = 1, /** P picture or P-VOP */ + OMX_VC_INTER4V = 2, /** P picture or P-VOP */ + OMX_VC_INTRA = 3, /** I and P picture, I- and P-VOP */ + OMX_VC_INTRA_Q = 4, /** I and P picture, I- and P-VOP */ + OMX_VC_INTER4V_Q = 5 /** P picture or P-VOP (H.263)*/ +} OMXVCM4P2MacroblockType; + + + +/* 6.2.1.6 Coordinates */ +/* Coordinates are represented as follows: */ + +typedef struct { + OMX_INT x; + OMX_INT y; +} OMXVCM4P2Coordinate; + + + +/* 6.2.1.7 Motion Estimation Algorithms */ +/* A data type that enumerates motion estimation search methods is defined as follows: */ + +typedef enum { + OMX_VC_M4P2_FAST_SEARCH = 0, /** Fast motion search */ + OMX_VC_M4P2_FULL_SEARCH = 1 /** Full motion search */ +} OMXVCM4P2MEMode; + + + +/* 6.2.1.8 Motion Estimation Parameters */ +/* A data structure containing control parameters for + * motion estimation functions is defined as follows: + */ + +typedef struct { + OMX_INT searchEnable8x8; /** enables 8x8 search */ + OMX_INT halfPelSearchEnable; /** enables half-pel resolution */ + OMX_INT searchRange; /** search range */ + OMX_INT rndVal; /** rounding control; 0-disabled, 1-enabled*/ +} OMXVCM4P2MEParams; + + + +/* 6.2.1.9 Macroblock Information */ +/* A data structure containing macroblock parameters for + * motion estimation functions is defined as follows: + */ + +typedef struct { + OMX_S32 sliceId; /* slice number */ + OMXVCM4P2MacroblockType mbType; /* MB type: OMX_VC_INTRA, OMX_VC_INTER, or OMX_VC_INTER4 */ + OMX_S32 qp; /* quantization parameter*/ + OMX_U32 cbpy; /* CBP Luma */ + OMX_U32 cbpc; /* CBP Chroma */ + OMXVCMotionVector pMV0[2][2]; /* motion vector, represented using 1/2-pel units, + * pMV0[blocky][blockx] (blocky = 0~1, blockx =0~1) + */ + OMXVCMotionVector pMVPred[2][2]; /* motion vector prediction, represented using 1/2-pel units, + * pMVPred[blocky][blockx] (blocky = 0~1, blockx = 0~1) + */ + OMX_U8 pPredDir[2][2]; /* AC prediction direction: + * OMX_VC_NONE, OMX_VC_VERTICAL, OMX_VC_HORIZONTAL + */ +} OMXVCM4P2MBInfo, *OMXVCM4P2MBInfoPtr; + + + +/** + * Function: omxVCM4P2_FindMVpred (6.2.3.1.1) + * + * Description: + * Predicts a motion vector for the current block using the procedure + * specified in [ISO14496-2], subclause 7.6.5. The resulting predicted MV is + * returned in pDstMVPred. If the parameter pDstMVPredME if is not NULL then + * the set of three MV candidates used for prediction is also returned, + * otherwise pDstMVPredMEis NULL upon return. + * + * Input Arguments: + * + * pSrcMVCurMB - pointer to the MV buffer associated with the current Y + * macroblock; a value of NULL indicates unavailability. + * pSrcCandMV1 - pointer to the MV buffer containing the 4 MVs associated + * with the MB located to the left of the current MB; set to NULL + * if there is no MB to the left. + * pSrcCandMV2 - pointer to the MV buffer containing the 4 MVs associated + * with the MB located above the current MB; set to NULL if there + * is no MB located above the current MB. + * pSrcCandMV3 - pointer to the MV buffer containing the 4 MVs associated + * with the MB located to the right and above the current MB; set + * to NULL if there is no MB located to the above-right. + * iBlk - the index of block in the current macroblock + * pDstMVPredME - MV candidate return buffer; if set to NULL then + * prediction candidate MVs are not returned and pDstMVPredME will + * be NULL upon function return; if pDstMVPredME is non-NULL then it + * must point to a buffer containing sufficient space for three + * return MVs. + * + * Output Arguments: + * + * pDstMVPred - pointer to the predicted motion vector + * pDstMVPredME - if non-NULL upon input then pDstMVPredME points upon + * return to a buffer containing the three motion vector candidates + * used for prediction as specified in [ISO14496-2], subclause + * 7.6.5, otherwise if NULL upon input then pDstMVPredME is NULL + * upon output. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned under any of the following + * conditions: + * - the pointer pDstMVPred is NULL + * - the parameter iBlk does not fall into the range 0 <= iBlk<=3 + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_FindMVpred ( + const OMXVCMotionVector *pSrcMVCurMB, + const OMXVCMotionVector *pSrcCandMV1, + const OMXVCMotionVector *pSrcCandMV2, + const OMXVCMotionVector *pSrcCandMV3, + OMXVCMotionVector *pDstMVPred, + OMXVCMotionVector *pDstMVPredME, + OMX_INT iBlk +); + + + +/** + * Function: omxVCM4P2_IDCT8x8blk (6.2.3.2.1) + * + * Description: + * Computes a 2D inverse DCT for a single 8x8 block, as defined in + * [ISO14496-2]. + * + * Input Arguments: + * + * pSrc - pointer to the start of the linearly arranged IDCT input buffer; + * must be aligned on a 16-byte boundary. According to + * [ISO14496-2], the input coefficient values should lie within the + * range [-2048, 2047]. + * + * Output Arguments: + * + * pDst - pointer to the start of the linearly arranged IDCT output buffer; + * must be aligned on a 16-byte boundary. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments: + * - pSrc or pDst is NULL. + * - pSrc or pDst is not 16-byte aligned. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_IDCT8x8blk ( + const OMX_S16 *pSrc, + OMX_S16 *pDst +); + + + +/** + * Function: omxVCM4P2_MEGetBufSize (6.2.4.1.1) + * + * Description: + * Computes the size, in bytes, of the vendor-specific specification + * structure for the following motion estimation functions: + * BlockMatch_Integer_8x8, BlockMatch_Integer_16x16, and MotionEstimationMB. + * + * Input Arguments: + * + * MEmode - motion estimation mode; available modes are defined by the + * enumerated type OMXVCM4P2MEMode + * pMEParams - motion estimation parameters + * + * Output Arguments: + * + * pSize - pointer to the number of bytes required for the specification + * structure + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - one or more of the following is true: + * - an invalid value was specified for the parameter MEmode + * - a negative or zero value was specified for the + * parameter pMEParams->searchRange + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_MEGetBufSize ( + OMXVCM4P2MEMode MEmode, + const OMXVCM4P2MEParams *pMEParams, + OMX_U32 *pSize +); + + + +/** + * Function: omxVCM4P2_MEInit (6.2.4.1.2) + * + * Description: + * Initializes the vendor-specific specification structure required for the + * following motion estimation functions: BlockMatch_Integer_8x8, + * BlockMatch_Integer_16x16, and MotionEstimationMB. Memory for the + * specification structure *pMESpec must be allocated prior to calling the + * function, and should be aligned on a 4-byte boundary. Following + * initialization by this function, the vendor-specific structure *pMESpec + * should contain an implementation-specific representation of all motion + * estimation parameters received via the structure pMEParams, for example + * rndVal, searchRange, etc. The number of bytes required for the + * specification structure can be determined using the function + * omxVCM4P2_MEGetBufSize. + * + * Input Arguments: + * + * MEmode - motion estimation mode; available modes are defined by the + * enumerated type OMXVCM4P2MEMode + * pMEParams - motion estimation parameters + * pMESpec - pointer to the uninitialized ME specification structure + * + * Output Arguments: + * + * pMESpec - pointer to the initialized ME specification structure + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - one or more of the following is true: + * - an invalid value was specified for the parameter MEmode + * - a negative or zero value was specified for the + * parameter pMEParams->searchRange + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_MEInit ( + OMXVCM4P2MEMode MEmode, + const OMXVCM4P2MEParams*pMEParams, + void *pMESpec +); + + + +/** + * Function: omxVCM4P2_BlockMatch_Integer_16x16 (6.2.4.2.1) + * + * Description: + * Performs a 16x16 block search; estimates motion vector and associated + * minimum SAD. Both the input and output motion vectors are represented using + * half-pixel units, and therefore a shift left or right by 1 bit may be + * required, respectively, to match the input or output MVs with other + * functions that either generate output MVs or expect input MVs represented + * using integer pixel units. + * + * Input Arguments: + * + * pSrcRefBuf - pointer to the reference Y plane; points to the reference + * MB that corresponds to the location of the current macroblock in + * the current plane. + * refWidth - width of the reference plane + * pRefRect - pointer to the valid reference plane rectangle; coordinates + * are specified relative to the image origin. Rectangle + * boundaries may extend beyond image boundaries if the image has + * been padded. For example, if padding extends 4 pixels beyond + * frame border, then the value for the left border could be set to + * -4. + * pSrcCurrBuf - pointer to the current block in the current macroblock + * buffer extracted from the original plane (linear array, 256 + * entries); must be aligned on a 16-byte boundary. The number of + * bytes between lines (step) is 16. + * pCurrPointPos - position of the current macroblock in the current plane + * pSrcPreMV - pointer to predicted motion vector; NULL indicates no + * predicted MV + * pSrcPreSAD - pointer to SAD associated with the predicted MV (referenced + * by pSrcPreMV); may be set to NULL if unavailable. + * pMESpec - vendor-specific motion estimation specification structure; + * must have been allocated and then initialized using + * omxVCM4P2_MEInit prior to calling the block matching function. + * + * Output Arguments: + * + * pDstMV - pointer to estimated MV + * pDstSAD - pointer to minimum SAD + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments. Returned if one of the following + * conditions is true: + * - at least one of the following pointers is NULL: pSrcRefBuf, + * pRefRect, pSrcCurrBuff, pCurrPointPos, pDstMV, pDstSAD or + * pMESpec, or + * - pSrcCurrBuf is not 16-byte aligned + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_BlockMatch_Integer_16x16 ( + const OMX_U8 *pSrcRefBuf, + OMX_INT refWidth, + const OMXRect *pRefRect, + const OMX_U8 *pSrcCurrBuf, + const OMXVCM4P2Coordinate *pCurrPointPos, + const OMXVCMotionVector*pSrcPreMV, + const OMX_INT *pSrcPreSAD, + void *pMESpec, + OMXVCMotionVector*pDstMV, + OMX_INT *pDstSAD +); + + + +/** + * Function: omxVCM4P2_BlockMatch_Integer_8x8 (6.2.4.2.2) + * + * Description: + * Performs an 8x8 block search; estimates motion vector and associated + * minimum SAD. Both the input and output motion vectors are represented + * using half-pixel units, and therefore a shift left or right by 1 bit may be + * required, respectively, to match the input or output MVs with other + * functions that either generate output MVs or expect input MVs represented + * using integer pixel units. + * + * Input Arguments: + * + * pSrcRefBuf - pointer to the reference Y plane; points to the reference + * block that corresponds to the location of the current 8x8 block + * in the current plane. + * refWidth - width of the reference plane + * pRefRect - pointer to the valid reference plane rectangle; coordinates + * are specified relative to the image origin. Rectangle + * boundaries may extend beyond image boundaries if the image has + * been padded. + * pSrcCurrBuf - pointer to the current block in the current macroblock + * buffer extracted from the original plane (linear array, 128 + * entries); must be aligned on an 8-byte boundary. The number of + * bytes between lines (step) is 16 bytes. + * pCurrPointPos - position of the current block in the current plane + * pSrcPreMV - pointer to predicted motion vector; NULL indicates no + * predicted MV + * pSrcPreSAD - pointer to SAD associated with the predicted MV (referenced + * by pSrcPreMV); may be set to NULL if unavailable. + * pMESpec - vendor-specific motion estimation specification structure; + * must have been allocated and then initialized using + * omxVCM4P2_MEInit prior to calling the block matching function. + * + * Output Arguments: + * + * pDstMV - pointer to estimated MV + * pDstSAD - pointer to minimum SAD + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments. Returned if one of the following + * conditions is true: + * - at least one of the following pointers is NULL: pSrcRefBuf, + * pRefRect, pSrcCurrBuff, pCurrPointPos, pDstMV, pDstSAD or + * pMESpec, or + * - pSrcCurrBuf is not 8-byte aligned + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_BlockMatch_Integer_8x8 ( + const OMX_U8 *pSrcRefBuf, + OMX_INT refWidth, + const OMXRect *pRefRect, + const OMX_U8 *pSrcCurrBuf, + const OMXVCM4P2Coordinate *pCurrPointPos, + const OMXVCMotionVector *pSrcPreMV, + const OMX_INT *pSrcPreSAD, + void *pMESpec, + OMXVCMotionVector *pDstMV, + OMX_INT *pDstSAD +); + + + +/** + * Function: omxVCM4P2_BlockMatch_Half_16x16 (6.2.4.2.3) + * + * Description: + * Performs a 16x16 block match with half-pixel resolution. Returns the + * estimated motion vector and associated minimum SAD. This function + * estimates the half-pixel motion vector by interpolating the integer + * resolution motion vector referenced by the input parameter pSrcDstMV, i.e., + * the initial integer MV is generated externally. The input parameters + * pSrcRefBuf and pSearchPointRefPos should be shifted by the winning MV of + * 16x16 integer search prior to calling BlockMatch_Half_16x16. The function + * BlockMatch_Integer_16x16 may be used for integer motion estimation. + * + * Input Arguments: + * + * pSrcRefBuf - pointer to the reference Y plane; points to the reference + * macroblock that corresponds to the location of the current + * macroblock in the current plane. + * refWidth - width of the reference plane + * pRefRect - reference plane valid region rectangle + * pSrcCurrBuf - pointer to the current block in the current macroblock + * buffer extracted from the original plane (linear array, 256 + * entries); must be aligned on a 16-byte boundary. The number of + * bytes between lines (step) is 16. + * pSearchPointRefPos - position of the starting point for half pixel + * search (specified in terms of integer pixel units) in the + * reference plane, i.e., the reference position pointed to by the + * predicted motion vector. + * rndVal - rounding control parameter: 0 - disabled; 1 - enabled. + * pSrcDstMV - pointer to the initial MV estimate; typically generated + * during a prior 16X16 integer search; specified in terms of + * half-pixel units. + * + * Output Arguments: + * + * pSrcDstMV - pointer to estimated MV + * pDstSAD - pointer to minimum SAD + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments. Returned if one of the following + * conditions is true: + * - at least one of the following pointers is NULL: pSrcRefBuf, + * pRefRect, pSrcCurrBuff, pSearchPointRefPos, pSrcDstMV. + * - pSrcCurrBuf is not 16-byte aligned, or + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_BlockMatch_Half_16x16 ( + const OMX_U8 *pSrcRefBuf, + OMX_INT refWidth, + const OMXRect *pRefRect, + const OMX_U8 *pSrcCurrBuf, + const OMXVCM4P2Coordinate *pSearchPointRefPos, + OMX_INT rndVal, + OMXVCMotionVector *pSrcDstMV, + OMX_INT *pDstSAD +); + + + +/** + * Function: omxVCM4P2_BlockMatch_Half_8x8 (6.2.4.2.4) + * + * Description: + * Performs an 8x8 block match with half-pixel resolution. Returns the + * estimated motion vector and associated minimum SAD. This function + * estimates the half-pixel motion vector by interpolating the integer + * resolution motion vector referenced by the input parameter pSrcDstMV, i.e., + * the initial integer MV is generated externally. The input parameters + * pSrcRefBuf and pSearchPointRefPos should be shifted by the winning MV of + * 8x8 integer search prior to calling BlockMatch_Half_8x8. The function + * BlockMatch_Integer_8x8 may be used for integer motion estimation. + * + * Input Arguments: + * + * pSrcRefBuf - pointer to the reference Y plane; points to the reference + * block that corresponds to the location of the current 8x8 block + * in the current plane. + * refWidth - width of the reference plane + * pRefRect - reference plane valid region rectangle + * pSrcCurrBuf - pointer to the current block in the current macroblock + * buffer extracted from the original plane (linear array, 128 + * entries); must be aligned on a 8-byte boundary. The number of + * bytes between lines (step) is 16. + * pSearchPointRefPos - position of the starting point for half pixel + * search (specified in terms of integer pixel units) in the + * reference plane. + * rndVal - rounding control parameter: 0 - disabled; 1 - enabled. + * pSrcDstMV - pointer to the initial MV estimate; typically generated + * during a prior 8x8 integer search, specified in terms of + * half-pixel units. + * + * Output Arguments: + * + * pSrcDstMV - pointer to estimated MV + * pDstSAD - pointer to minimum SAD + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments. Returned if one of the following + * conditions is true: + * - at least one of the following pointers is NULL: + * pSrcRefBuf, pRefRect, pSrcCurrBuff, pSearchPointRefPos, pSrcDstMV + * - pSrcCurrBuf is not 8-byte aligned + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_BlockMatch_Half_8x8 ( + const OMX_U8 *pSrcRefBuf, + OMX_INT refWidth, + const OMXRect *pRefRect, + const OMX_U8 *pSrcCurrBuf, + const OMXVCM4P2Coordinate *pSearchPointRefPos, + OMX_INT rndVal, + OMXVCMotionVector *pSrcDstMV, + OMX_INT *pDstSAD +); + + + +/** + * Function: omxVCM4P2_MotionEstimationMB (6.2.4.3.1) + * + * Description: + * Performs motion search for a 16x16 macroblock. Selects best motion search + * strategy from among inter-1MV, inter-4MV, and intra modes. Supports + * integer and half pixel resolution. + * + * Input Arguments: + * + * pSrcCurrBuf - pointer to the top-left corner of the current MB in the + * original picture plane; must be aligned on a 16-byte boundary. + * The function does not expect source data outside the region + * bounded by the MB to be available; for example it is not + * necessary for the caller to guarantee the availability of + * pSrcCurrBuf[-SrcCurrStep], i.e., the row of pixels above the MB + * to be processed. + * srcCurrStep - width of the original picture plane, in terms of full + * pixels; must be a multiple of 16. + * pSrcRefBuf - pointer to the reference Y plane; points to the reference + * plane location corresponding to the location of the current + * macroblock in the current plane; must be aligned on a 16-byte + * boundary. + * srcRefStep - width of the reference picture plane, in terms of full + * pixels; must be a multiple of 16. + * pRefRect - reference plane valid region rectangle, specified relative to + * the image origin + * pCurrPointPos - position of the current macroblock in the current plane + * pMESpec - pointer to the vendor-specific motion estimation specification + * structure; must be allocated and then initialized using + * omxVCM4P2_MEInit prior to calling this function. + * pMBInfo - array, of dimension four, containing pointers to information + * associated with four nearby MBs: + * - pMBInfo[0] - pointer to left MB information + * - pMBInfo[1] - pointer to top MB information + * - pMBInfo[2] - pointer to top-left MB information + * - pMBInfo[3] - pointer to top-right MB information + * Any pointer in the array may be set equal to NULL if the + * corresponding MB doesn't exist. For each MB, the following structure + * members are used: + * - mbType - macroblock type, either OMX_VC_INTRA, OMX_VC_INTER, or + * OMX_VC_INTER4V + * - pMV0[2][2] - estimated motion vectors; represented + * in 1/2 pixel units + * - sliceID - number of the slice to which the MB belongs + * pSrcDstMBCurr - pointer to information structure for the current MB. + * The following entries should be set prior to calling the + * function: sliceID - the number of the slice the to which the + * current MB belongs. The structure elements cbpy and cbpc are + * ignored. + * + * Output Arguments: + * + * pSrcDstMBCurr - pointer to updated information structure for the current + * MB after MB-level motion estimation has been completed. The + * following structure members are updated by the ME function: + * - mbType - macroblock type: OMX_VC_INTRA, OMX_VC_INTER, or + * OMX_VC_INTER4V. + * - pMV0[2][2] - estimated motion vectors; represented in + * terms of 1/2 pel units. + * - pMVPred[2][2] - predicted motion vectors; represented + * in terms of 1/2 pel units. + * The structure members cbpy and cbpc are not updated by the function. + * pDstSAD - pointer to the minimum SAD for INTER1V, or sum of minimum SADs + * for INTER4V + * pDstBlockSAD - pointer to an array of SAD values for each of the four + * 8x8 luma blocks in the MB. The block SADs are in scan order for + * each MB. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments. Returned if one or more of the + * following conditions is true: + * - at least one of the following pointers is NULL: pSrcCurrBuf, + * pSrcRefBuf, pRefRect, pCurrPointPos, pMBInter, pMBIntra, + * pSrcDstMBCurr, or pDstSAD. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_MotionEstimationMB ( + const OMX_U8 *pSrcCurrBuf, + OMX_S32 srcCurrStep, + const OMX_U8 *pSrcRefBuf, + OMX_S32 srcRefStep, + const OMXRect*pRefRect, + const OMXVCM4P2Coordinate *pCurrPointPos, + void *pMESpec, + const OMXVCM4P2MBInfoPtr *pMBInfo, + OMXVCM4P2MBInfo *pSrcDstMBCurr, + OMX_U16 *pDstSAD, + OMX_U16 *pDstBlockSAD +); + + + +/** + * Function: omxVCM4P2_DCT8x8blk (6.2.4.4.1) + * + * Description: + * Computes a 2D forward DCT for a single 8x8 block, as defined in + * [ISO14496-2]. + * + * Input Arguments: + * + * pSrc - pointer to the start of the linearly arranged input buffer; must + * be aligned on a 16-byte boundary. Input values (pixel + * intensities) are valid in the range [-255,255]. + * + * Output Arguments: + * + * pDst - pointer to the start of the linearly arranged output buffer; must + * be aligned on a 16-byte boundary. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments, returned if: + * - pSrc or pDst is NULL. + * - pSrc or pDst is not 16-byte aligned. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_DCT8x8blk ( + const OMX_S16 *pSrc, + OMX_S16 *pDst +); + + + +/** + * Function: omxVCM4P2_QuantIntra_I (6.2.4.4.2) + * + * Description: + * Performs quantization on intra block coefficients. This function supports + * bits_per_pixel == 8. + * + * Input Arguments: + * + * pSrcDst - pointer to the input intra block coefficients; must be aligned + * on a 16-byte boundary. + * QP - quantization parameter (quantizer_scale). + * blockIndex - block index indicating the component type and position, + * valid in the range 0 to 5, as defined in [ISO14496-2], subclause + * 6.1.3.8. + * shortVideoHeader - binary flag indicating presence of + * short_video_header; shortVideoHeader==1 selects linear intra DC + * mode, and shortVideoHeader==0 selects non linear intra DC mode. + * + * Output Arguments: + * + * pSrcDst - pointer to the output (quantized) interblock coefficients. + * When shortVideoHeader==1, AC coefficients are saturated on the + * interval [-127, 127], and DC coefficients are saturated on the + * interval [1, 254]. When shortVideoHeader==0, AC coefficients + * are saturated on the interval [-2047, 2047]. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments: + * - pSrcDst is NULL. + * - blockIndex < 0 or blockIndex >= 10 + * - QP <= 0 or QP >= 32. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_QuantIntra_I ( + OMX_S16 *pSrcDst, + OMX_U8 QP, + OMX_INT blockIndex, + OMX_INT shortVideoHeader +); + + + +/** + * Function: omxVCM4P2_QuantInter_I (6.2.4.4.3) + * + * Description: + * Performs quantization on an inter coefficient block; supports + * bits_per_pixel == 8. + * + * Input Arguments: + * + * pSrcDst - pointer to the input inter block coefficients; must be aligned + * on a 16-byte boundary. + * QP - quantization parameter (quantizer_scale) + * shortVideoHeader - binary flag indicating presence of short_video_header; + * shortVideoHeader==1 selects linear intra DC mode, and + * shortVideoHeader==0 selects non linear intra DC mode. + * + * Output Arguments: + * + * pSrcDst - pointer to the output (quantized) interblock coefficients. + * When shortVideoHeader==1, AC coefficients are saturated on the + * interval [-127, 127], and DC coefficients are saturated on the + * interval [1, 254]. When shortVideoHeader==0, AC coefficients + * are saturated on the interval [-2047, 2047]. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments: + * - pSrcDst is NULL. + * - QP <= 0 or QP >= 32. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_QuantInter_I ( + OMX_S16 *pSrcDst, + OMX_U8 QP, + OMX_INT shortVideoHeader +); + + + +/** + * Function: omxVCM4P2_TransRecBlockCoef_intra (6.2.4.4.4) + * + * Description: + * Quantizes the DCT coefficients, implements intra block AC/DC coefficient + * prediction, and reconstructs the current intra block texture for prediction + * on the next frame. Quantized row and column coefficients are returned in + * the updated coefficient buffers. + * + * Input Arguments: + * + * pSrc - pointer to the pixels of current intra block; must be aligned on + * an 8-byte boundary. + * pPredBufRow - pointer to the coefficient row buffer containing + * ((num_mb_per_row * 2 + 1) * 8) elements of type OMX_S16. + * Coefficients are organized into blocks of eight as described + * below (Internal Prediction Coefficient Update Procedures). The + * DC coefficient is first, and the remaining buffer locations + * contain the quantized AC coefficients. Each group of eight row + * buffer elements combined with one element eight elements ahead + * contains the coefficient predictors of the neighboring block + * that is spatially above or to the left of the block currently to + * be decoded. A negative-valued DC coefficient indicates that this + * neighboring block is not INTRA-coded or out of bounds, and + * therefore the AC and DC coefficients are invalid. Pointer must + * be aligned on an 8-byte boundary. + * pPredBufCol - pointer to the prediction coefficient column buffer + * containing 16 elements of type OMX_S16. Coefficients are + * organized as described in section 6.2.2.5. Pointer must be + * aligned on an 8-byte boundary. + * pSumErr - pointer to a flag indicating whether or not AC prediction is + * required; AC prediction is enabled if *pSumErr >=0, but the + * value is not used for coefficient prediction, i.e., the sum of + * absolute differences starts from 0 for each call to this + * function. Otherwise AC prediction is disabled if *pSumErr < 0 . + * blockIndex - block index indicating the component type and position, as + * defined in [ISO14496-2], subclause 6.1.3.8. + * curQp - quantization parameter of the macroblock to which the current + * block belongs + * pQpBuf - pointer to a 2-element quantization parameter buffer; pQpBuf[0] + * contains the quantization parameter associated with the 8x8 + * block left of the current block (QPa), and pQpBuf[1] contains + * the quantization parameter associated with the 8x8 block above + * the current block (QPc). In the event that the corresponding + * block is outside of the VOP bound, the Qp value will not affect + * the intra prediction process, as described in [ISO14496-2], + * sub-clause 7.4.3.3, Adaptive AC Coefficient Prediction. + * srcStep - width of the source buffer; must be a multiple of 8. + * dstStep - width of the reconstructed destination buffer; must be a + * multiple of 16. + * shortVideoHeader - binary flag indicating presence of + * short_video_header; shortVideoHeader==1 selects linear intra DC + * mode, and shortVideoHeader==0 selects non linear intra DC mode. + * + * Output Arguments: + * + * pDst - pointer to the quantized DCT coefficient buffer; pDst[0] contains + * the predicted DC coefficient; the remaining entries contain the + * quantized AC coefficients (without prediction). The pointer + * pDstmust be aligned on a 16-byte boundary. + * pRec - pointer to the reconstructed texture; must be aligned on an + * 8-byte boundary. + * pPredBufRow - pointer to the updated coefficient row buffer + * pPredBufCol - pointer to the updated coefficient column buffer + * pPreACPredict - if prediction is enabled, the parameter points to the + * start of the buffer containing the coefficient differences for + * VLC encoding. The entry pPreACPredict[0]indicates prediction + * direction for the current block and takes one of the following + * values: OMX_VC_NONE (prediction disabled), OMX_VC_HORIZONTAL, or + * OMX_VC_VERTICAL. The entries + * pPreACPredict[1]-pPreACPredict[7]contain predicted AC + * coefficients. If prediction is disabled (*pSumErr<0) then the + * contents of this buffer are undefined upon return from the + * function + * pSumErr - pointer to the value of the accumulated AC coefficient errors, + * i.e., sum of the absolute differences between predicted and + * unpredicted AC coefficients + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - Bad arguments: + * - At least one of the following pointers is NULL: pSrc, pDst, pRec, + * pCoefBufRow, pCoefBufCol, pQpBuf, pPreACPredict, pSumErr. + * - blockIndex < 0 or blockIndex >= 10; + * - curQP <= 0 or curQP >= 32. + * - srcStep, or dstStep <= 0 or not a multiple of 8. + * - pDst is not 16-byte aligned: . + * - At least one of the following pointers is not 8-byte aligned: + * pSrc, pRec. + * + * Note: The coefficient buffers must be updated in accordance with the + * update procedures defined in section in 6.2.2. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_TransRecBlockCoef_intra ( + const OMX_U8 *pSrc, + OMX_S16 *pDst, + OMX_U8 *pRec, + OMX_S16 *pPredBufRow, + OMX_S16 *pPredBufCol, + OMX_S16 *pPreACPredict, + OMX_INT *pSumErr, + OMX_INT blockIndex, + OMX_U8 curQp, + const OMX_U8 *pQpBuf, + OMX_INT srcStep, + OMX_INT dstStep, + OMX_INT shortVideoHeader +); + + + +/** + * Function: omxVCM4P2_TransRecBlockCoef_inter (6.2.4.4.5) + * + * Description: + * Implements DCT, and quantizes the DCT coefficients of the inter block + * while reconstructing the texture residual. There is no boundary check for + * the bit stream buffer. + * + * Input Arguments: + * + * pSrc -pointer to the residuals to be encoded; must be aligned on an + * 16-byte boundary. + * QP - quantization parameter. + * shortVideoHeader - binary flag indicating presence of short_video_header; + * shortVideoHeader==1 selects linear intra DC mode, and + * shortVideoHeader==0 selects non linear intra DC mode. + * + * Output Arguments: + * + * pDst - pointer to the quantized DCT coefficients buffer; must be aligned + * on a 16-byte boundary. + * pRec - pointer to the reconstructed texture residuals; must be aligned + * on a 16-byte boundary. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments: + * - At least one of the following pointers is either NULL or + * not 16-byte aligned: + * - pSrc + * - pDst + * - pRec + * - QP <= 0 or QP >= 32. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_TransRecBlockCoef_inter ( + const OMX_S16 *pSrc, + OMX_S16 *pDst, + OMX_S16 *pRec, + OMX_U8 QP, + OMX_INT shortVideoHeader +); + + + +/** + * Function: omxVCM4P2_EncodeVLCZigzag_IntraDCVLC (6.2.4.5.2) + * + * Description: + * Performs zigzag scan and VLC encoding of AC and DC coefficients for one + * intra block. Two versions of the function (DCVLC and ACVLC) are provided + * in order to support the two different methods of processing DC + * coefficients, as described in [ISO14496-2], subclause 7.4.1.4, "Intra DC + * Coefficient Decoding for the Case of Switched VLC Encoding". + * + * Input Arguments: + * + * ppBitStream - double pointer to the current byte in the bitstream + * pBitOffset - pointer to the bit position in the byte pointed by + * *ppBitStream. Valid within 0 to 7. + * pQDctBlkCoef - pointer to the quantized DCT coefficient + * predDir - AC prediction direction, which is used to decide the zigzag + * scan pattern; takes one of the following values: + * - OMX_VC_NONE - AC prediction not used. + * Performs classical zigzag scan. + * - OMX_VC_HORIZONTAL - Horizontal prediction. + * Performs alternate-vertical zigzag scan. + * - OMX_VC_VERTICAL - Vertical prediction. + * Performs alternate-horizontal zigzag scan. + * pattern - block pattern which is used to decide whether this block is + * encoded + * shortVideoHeader - binary flag indicating presence of + * short_video_header; escape modes 0-3 are used if + * shortVideoHeader==0, and escape mode 4 is used when + * shortVideoHeader==1. + * videoComp - video component type (luminance, chrominance) of the current + * block + * + * Output Arguments: + * + * ppBitStream - *ppBitStream is updated after the block is encoded, so + * that it points to the current byte in the bit stream buffer. + * pBitOffset - *pBitOffset is updated so that it points to the current bit + * position in the byte pointed by *ppBitStream. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - Bad arguments: + * - At least one of the following pointers is NULL: ppBitStream, + * *ppBitStream, pBitOffset, pQDctBlkCoef. + * - *pBitOffset < 0, or *pBitOffset >7. + * - PredDir is not one of: OMX_VC_NONE, OMX_VC_HORIZONTAL, or + * OMX_VC_VERTICAL. + * - VideoComp is not one component of enum OMXVCM4P2VideoComponent. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_EncodeVLCZigzag_IntraDCVLC ( + OMX_U8 **ppBitStream, + OMX_INT *pBitOffset, + const OMX_S16 *pQDctBlkCoef, + OMX_U8 predDir, + OMX_U8 pattern, + OMX_INT shortVideoHeader, + OMXVCM4P2VideoComponent videoComp +); + + + +/** + * Function: omxVCM4P2_EncodeVLCZigzag_IntraACVLC (6.2.4.5.2) + * + * Description: + * Performs zigzag scan and VLC encoding of AC and DC coefficients for one + * intra block. Two versions of the function (DCVLC and ACVLC) are provided + * in order to support the two different methods of processing DC + * coefficients, as described in [ISO14496-2], subclause 7.4.1.4, Intra DC + * Coefficient Decoding for the Case of Switched VLC Encoding. + * + * Input Arguments: + * + * ppBitStream - double pointer to the current byte in the bitstream + * pBitOffset - pointer to the bit position in the byte pointed by + * *ppBitStream. Valid within 0 to 7. + * pQDctBlkCoef - pointer to the quantized DCT coefficient + * predDir - AC prediction direction, which is used to decide the zigzag + * scan pattern; takes one of the following values: + * - OMX_VC_NONE - AC prediction not used. + * Performs classical zigzag scan. + * - OMX_VC_HORIZONTAL - Horizontal prediction. + * Performs alternate-vertical zigzag scan. + * - OMX_VC_VERTICAL - Vertical prediction. + * Performs alternate-horizontal zigzag scan. + * pattern - block pattern which is used to decide whether this block is + * encoded + * shortVideoHeader - binary flag indicating presence of + * short_video_header; escape modes 0-3 are used if + * shortVideoHeader==0, and escape mode 4 is used when + * shortVideoHeader==1. + * + * Output Arguments: + * + * ppBitStream - *ppBitStream is updated after the block is encoded, so + * that it points to the current byte in the bit stream buffer. + * pBitOffset - *pBitOffset is updated so that it points to the current bit + * position in the byte pointed by *ppBitStream. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - Bad arguments: + * - At least one of the following pointers is NULL: ppBitStream, + * *ppBitStream, pBitOffset, pQDctBlkCoef. + * - *pBitOffset < 0, or *pBitOffset >7. + * - PredDir is not one of: OMX_VC_NONE, OMX_VC_HORIZONTAL, or + * OMX_VC_VERTICAL. + * - VideoComp is not one component of enum OMXVCM4P2VideoComponent. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_EncodeVLCZigzag_IntraACVLC ( + OMX_U8 **ppBitStream, + OMX_INT *pBitOffset, + const OMX_S16 *pQDctBlkCoef, + OMX_U8 predDir, + OMX_U8 pattern, + OMX_INT shortVideoHeader +); + + + +/** + * Function: omxVCM4P2_EncodeVLCZigzag_Inter (6.2.4.5.3) + * + * Description: + * Performs classical zigzag scanning and VLC encoding for one inter block. + * + * Input Arguments: + * + * ppBitStream - pointer to the pointer to the current byte in the bit + * stream + * pBitOffset - pointer to the bit position in the byte pointed by + * *ppBitStream. Valid within 0 to 7 + * pQDctBlkCoef - pointer to the quantized DCT coefficient + * pattern - block pattern which is used to decide whether this block is + * encoded + * shortVideoHeader - binary flag indicating presence of + * short_video_header; escape modes 0-3 are used if + * shortVideoHeader==0, and escape mode 4 is used when + * shortVideoHeader==1. + * + * Output Arguments: + * + * ppBitStream - *ppBitStream is updated after the block is encoded so that + * it points to the current byte in the bit stream buffer. + * pBitOffset - *pBitOffset is updated so that it points to the current bit + * position in the byte pointed by *ppBitStream. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - Bad arguments + * - At least one of the pointers: is NULL: ppBitStream, *ppBitStream, + * pBitOffset, pQDctBlkCoef + * - *pBitOffset < 0, or *pBitOffset >7. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_EncodeVLCZigzag_Inter ( + OMX_U8 **ppBitStream, + OMX_INT *pBitOffset, + const OMX_S16 *pQDctBlkCoef, + OMX_U8 pattern, + OMX_INT shortVideoHeader +); + + + +/** + * Function: omxVCM4P2_EncodeMV (6.2.4.5.4) + * + * Description: + * Predicts a motion vector for the current macroblock, encodes the + * difference, and writes the output to the stream buffer. The input MVs + * pMVCurMB, pSrcMVLeftMB, pSrcMVUpperMB, and pSrcMVUpperRightMB should lie + * within the ranges associated with the input parameter fcodeForward, as + * described in [ISO14496-2], subclause 7.6.3. This function provides a + * superset of the functionality associated with the function + * omxVCM4P2_FindMVpred. + * + * Input Arguments: + * + * ppBitStream - double pointer to the current byte in the bitstream buffer + * pBitOffset - index of the first free (next available) bit in the stream + * buffer referenced by *ppBitStream, valid in the range 0 to 7. + * pMVCurMB - pointer to the current macroblock motion vector; a value of + * NULL indicates unavailability. + * pSrcMVLeftMB - pointer to the source left macroblock motion vector; a + * value of NULLindicates unavailability. + * pSrcMVUpperMB - pointer to source upper macroblock motion vector; a + * value of NULL indicates unavailability. + * pSrcMVUpperRightMB - pointer to source upper right MB motion vector; a + * value of NULL indicates unavailability. + * fcodeForward - an integer with values from 1 to 7; used in encoding + * motion vectors related to search range, as described in + * [ISO14496-2], subclause 7.6.3. + * MBType - macro block type, valid in the range 0 to 5 + * + * Output Arguments: + * + * ppBitStream - updated pointer to the current byte in the bit stream + * buffer + * pBitOffset - updated index of the next available bit position in stream + * buffer referenced by *ppBitStream + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments + * - At least one of the following pointers is NULL: ppBitStream, + * *ppBitStream, pBitOffset, pMVCurMB + * - *pBitOffset < 0, or *pBitOffset >7. + * - fcodeForward <= 0, or fcodeForward > 7, or MBType < 0. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_EncodeMV ( + OMX_U8 **ppBitStream, + OMX_INT *pBitOffset, + const OMXVCMotionVector *pMVCurMB, + const OMXVCMotionVector*pSrcMVLeftMB, + const OMXVCMotionVector *pSrcMVUpperMB, + const OMXVCMotionVector *pSrcMVUpperRightMB, + OMX_INT fcodeForward, + OMXVCM4P2MacroblockType MBType +); + + + +/** + * Function: omxVCM4P2_DecodePadMV_PVOP (6.2.5.1.1) + * + * Description: + * Decodes and pads the four motion vectors associated with a non-intra P-VOP + * macroblock. For macroblocks of type OMX_VC_INTER4V, the output MV is + * padded as specified in [ISO14496-2], subclause 7.6.1.6. Otherwise, for + * macroblocks of types other than OMX_VC_INTER4V, the decoded MV is copied to + * all four output MV buffer entries. + * + * Input Arguments: + * + * ppBitStream - pointer to the pointer to the current byte in the bit + * stream buffer + * pBitOffset - pointer to the bit position in the byte pointed to by + * *ppBitStream. *pBitOffset is valid within [0-7]. + * pSrcMVLeftMB, pSrcMVUpperMB, and pSrcMVUpperRightMB - pointers to the + * motion vector buffers of the macroblocks specially at the left, + * upper, and upper-right side of the current macroblock, + * respectively; a value of NULL indicates unavailability. Note: + * Any neighborhood macroblock outside the current VOP or video + * packet or outside the current GOB (when short_video_header is + * 1 ) for which gob_header_empty is 0 is treated as + * transparent, according to [ISO14496-2], subclause 7.6.5. + * fcodeForward - a code equal to vop_fcode_forward in MPEG-4 bit stream + * syntax + * MBType - the type of the current macroblock. If MBType is not equal to + * OMX_VC_INTER4V, the destination motion vector buffer is still + * filled with the same decoded vector. + * + * Output Arguments: + * + * ppBitStream - *ppBitStream is updated after the block is decoded, so + * that it points to the current byte in the bit stream buffer + * pBitOffset - *pBitOffset is updated so that it points to the current bit + * position in the byte pointed by *ppBitStream + * pDstMVCurMB - pointer to the motion vector buffer for the current + * macroblock; contains four decoded motion vectors + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments: + * - At least one of the following pointers is NULL: + * ppBitStream, *ppBitStream, pBitOffset, pDstMVCurMB + * - *pBitOffset exceeds [0,7] + * - fcodeForward exceeds (0,7] + * - MBType less than zero + * - motion vector buffer is not 4-byte aligned. + * OMX_Sts_Err - status error + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_DecodePadMV_PVOP ( + const OMX_U8 **ppBitStream, + OMX_INT *pBitOffset, + OMXVCMotionVector *pSrcMVLeftMB, + OMXVCMotionVector*pSrcMVUpperMB, + OMXVCMotionVector *pSrcMVUpperRightMB, + OMXVCMotionVector*pDstMVCurMB, + OMX_INT fcodeForward, + OMXVCM4P2MacroblockType MBType +); + + + +/** + * Function: omxVCM4P2_DecodeVLCZigzag_IntraDCVLC (6.2.5.2.2) + * + * Description: + * Performs VLC decoding and inverse zigzag scan of AC and DC coefficients + * for one intra block. Two versions of the function (DCVLC and ACVLC) are + * provided in order to support the two different methods of processing DC + * coefficients, as described in [ISO14496-2], subclause 7.4.1.4, Intra DC + * Coefficient Decoding for the Case of Switched VLC Encoding. + * + * Input Arguments: + * + * ppBitStream - pointer to the pointer to the current byte in the + * bitstream buffer + * pBitOffset - pointer to the bit position in the current byte referenced + * by *ppBitStream. The parameter *pBitOffset is valid in the + * range [0-7]. + * Bit Position in one byte: |Most Least| + * *pBitOffset |0 1 2 3 4 5 6 7| + * predDir - AC prediction direction; used to select the zigzag scan + * pattern; takes one of the following values: + * - OMX_VC_NONE - AC prediction not used; + * performs classical zigzag scan. + * - OMX_VC_HORIZONTAL - Horizontal prediction; + * performs alternate-vertical zigzag scan; + * - OMX_VC_VERTICAL - Vertical prediction; + * performs alternate-horizontal zigzag scan. + * shortVideoHeader - binary flag indicating presence of + * short_video_header; escape modes 0-3 are used if + * shortVideoHeader==0, and escape mode 4 is used when + * shortVideoHeader==1. + * videoComp - video component type (luminance or chrominance) of the + * current block + * + * Output Arguments: + * + * ppBitStream - *ppBitStream is updated after the block is decoded such + * that it points to the current byte in the bit stream buffer + * pBitOffset - *pBitOffset is updated such that it points to the current + * bit position in the byte pointed by *ppBitStream + * pDst - pointer to the coefficient buffer of current block; must be + * 4-byte aligned. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments, if: + * - At least one of the following pointers is NULL: + * ppBitStream, *ppBitStream, pBitOffset, pDst + * - *pBitOffset exceeds [0,7] + * - preDir exceeds [0,2] + * - pDst is not 4-byte aligned + * OMX_Sts_Err - if: + * - In DecodeVLCZigzag_IntraDCVLC, dc_size > 12 + * - At least one of mark bits equals zero + * - Illegal stream encountered; code cannot be located in VLC table + * - Forbidden code encountered in the VLC FLC table. + * - The number of coefficients is greater than 64 + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_DecodeVLCZigzag_IntraDCVLC ( + const OMX_U8 **ppBitStream, + OMX_INT *pBitOffset, + OMX_S16 *pDst, + OMX_U8 predDir, + OMX_INT shortVideoHeader, + OMXVCM4P2VideoComponent videoComp +); + + + +/** + * Function: omxVCM4P2_DecodeVLCZigzag_IntraACVLC (6.2.5.2.2) + * + * Description: + * Performs VLC decoding and inverse zigzag scan of AC and DC coefficients + * for one intra block. Two versions of the function (DCVLC and ACVLC) are + * provided in order to support the two different methods of processing DC + * coefficients, as described in [ISO14496-2], subclause 7.4.1.4, Intra DC + * Coefficient Decoding for the Case of Switched VLC Encoding. + * + * Input Arguments: + * + * ppBitStream - pointer to the pointer to the current byte in the + * bitstream buffer + * pBitOffset - pointer to the bit position in the current byte referenced + * by *ppBitStream. The parameter *pBitOffset is valid in the + * range [0-7]. Bit Position in one byte: |Most Least| *pBitOffset + * |0 1 2 3 4 5 6 7| + * predDir - AC prediction direction; used to select the zigzag scan + * pattern; takes one of the following values: OMX_VC_NONE - AC + * prediction not used; performs classical zigzag scan. + * OMX_VC_HORIZONTAL - Horizontal prediction; performs + * alternate-vertical zigzag scan; OMX_VC_VERTICAL - Vertical + * prediction; performs alternate-horizontal zigzag scan. + * shortVideoHeader - binary flag indicating presence of + * short_video_header; escape modes 0-3 are used if + * shortVideoHeader==0, and escape mode 4 is used when + * shortVideoHeader==1. + * videoComp - video component type (luminance or chrominance) of the + * current block + * + * Output Arguments: + * + * ppBitStream - *ppBitStream is updated after the block is decoded such + * that it points to the current byte in the bit stream buffer + * pBitOffset - *pBitOffset is updated such that it points to the current + * bit position in the byte pointed by *ppBitStream + * pDst - pointer to the coefficient buffer of current block; must be + * 4-byte aligned. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments At least one of the following + * pointers is NULL: ppBitStream, *ppBitStream, pBitOffset, pDst, + * or At least one of the following conditions is true: + * *pBitOffset exceeds [0,7], preDir exceeds [0,2], or pDst is + * not 4-byte aligned + * OMX_Sts_Err In DecodeVLCZigzag_IntraDCVLC, dc_size > 12 At least one of + * mark bits equals zero Illegal stream encountered; code cannot + * be located in VLC table Forbidden code encountered in the VLC + * FLC table The number of coefficients is greater than 64 + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_DecodeVLCZigzag_IntraACVLC ( + const OMX_U8 **ppBitStream, + OMX_INT *pBitOffset, + OMX_S16 *pDst, + OMX_U8 predDir, + OMX_INT shortVideoHeader +); + + + +/** + * Function: omxVCM4P2_DecodeVLCZigzag_Inter (6.2.5.2.3) + * + * Description: + * Performs VLC decoding and inverse zigzag scan for one inter-coded block. + * + * Input Arguments: + * + * ppBitStream - double pointer to the current byte in the stream buffer + * pBitOffset - pointer to the next available bit in the current stream + * byte referenced by *ppBitStream. The parameter *pBitOffset is + * valid within the range [0-7]. + * shortVideoHeader - binary flag indicating presence of + * short_video_header; escape modes 0-3 are used if + * shortVideoHeader==0, and escape mode 4 is used when + * shortVideoHeader==1. + * + * Output Arguments: + * + * ppBitStream - *ppBitStream is updated after the block is decoded such + * that it points to the current byte in the stream buffer + * pBitOffset - *pBitOffset is updated after decoding such that it points + * to the next available bit in the stream byte referenced by + * *ppBitStream + * pDst - pointer to the coefficient buffer of current block; must be + * 4-byte aligned. + * + * Return Value: + * + * OMX_Sts_BadArgErr - bad arguments: + * - At least one of the following pointers is NULL: + * ppBitStream, *ppBitStream, pBitOffset, pDst + * - pDst is not 4-byte aligned + * - *pBitOffset exceeds [0,7] + * OMX_Sts_Err - status error, if: + * - At least one mark bit is equal to zero + * - Encountered an illegal stream code that cannot be found in the VLC table + * - Encountered an illegal code in the VLC FLC table + * - The number of coefficients is greater than 64 + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_DecodeVLCZigzag_Inter ( + const OMX_U8 **ppBitStream, + OMX_INT *pBitOffset, + OMX_S16 *pDst, + OMX_INT shortVideoHeader +); + + + +/** + * Function: omxVCM4P2_QuantInvIntra_I (6.2.5.3.2) + * + * Description: + * Performs the second inverse quantization mode on an intra/inter coded + * block. Supports bits_per_pixel = 8. The output coefficients are clipped to + * the range [-2048, 2047]. + * + * Input Arguments: + * + * pSrcDst - pointer to the input (quantized) intra/inter block; must be + * aligned on a 16-byte boundary. + * QP - quantization parameter (quantizer_scale) + * videoComp - video component type of the current block. Takes one of the + * following flags: OMX_VC_LUMINANCE, OMX_VC_CHROMINANCE (intra + * version only). + * shortVideoHeader - binary flag indicating presence of short_video_header + * (intra version only). + * + * Output Arguments: + * + * pSrcDst - pointer to the output (dequantized) intra/inter block + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; one or more of the following is + * true: + * - pSrcDst is NULL + * - QP <= 0 or QP >=31 + * - videoComp is neither OMX_VC_LUMINANCE nor OMX_VC_CHROMINANCE. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_QuantInvIntra_I ( + OMX_S16 *pSrcDst, + OMX_INT QP, + OMXVCM4P2VideoComponent videoComp, + OMX_INT shortVideoHeader +); + + + +/** + * Function: omxVCM4P2_QuantInvInter_I (6.2.5.3.2) + * + * Description: + * Performs the second inverse quantization mode on an intra/inter coded + * block. Supports bits_per_pixel = 8. The output coefficients are clipped to + * the range [-2048, 2047]. + * + * Input Arguments: + * + * pSrcDst - pointer to the input (quantized) intra/inter block; must be + * aligned on a 16-byte boundary. + * QP - quantization parameter (quantizer_scale) + * videoComp - video component type of the current block. Takes one of the + * following flags: OMX_VC_LUMINANCE, OMX_VC_CHROMINANCE (intra + * version only). + * shortVideoHeader - binary flag indicating presence of short_video_header + * (intra version only). + * + * Output Arguments: + * + * pSrcDst - pointer to the output (dequantized) intra/inter block + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; one or more of the following is + * true: + * - pSrcDst is NULL + * - QP <= 0 or QP >=31 + * - videoComp is neither OMX_VC_LUMINANCE nor OMX_VC_CHROMINANCE. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_QuantInvInter_I ( + OMX_S16 *pSrcDst, + OMX_INT QP +); + + + +/** + * Function: omxVCM4P2_DecodeBlockCoef_Intra (6.2.5.4.1) + * + * Description: + * Decodes the INTRA block coefficients. Inverse quantization, inversely + * zigzag positioning, and IDCT, with appropriate clipping on each step, are + * performed on the coefficients. The results are then placed in the output + * frame/plane on a pixel basis. Note: This function will be used only when + * at least one non-zero AC coefficient of current block exists in the bit + * stream. The DC only condition will be handled in another function. + * + * + * Input Arguments: + * + * ppBitStream - pointer to the pointer to the current byte in the bit + * stream buffer. There is no boundary check for the bit stream + * buffer. + * pBitOffset - pointer to the bit position in the byte pointed to by + * *ppBitStream. *pBitOffset is valid within [0-7]. + * step - width of the destination plane + * pCoefBufRow - pointer to the coefficient row buffer; must be aligned on + * an 8-byte boundary. + * pCoefBufCol - pointer to the coefficient column buffer; must be aligned + * on an 8-byte boundary. + * curQP - quantization parameter of the macroblock which the current block + * belongs to + * pQPBuf - pointer to the quantization parameter buffer + * blockIndex - block index indicating the component type and position as + * defined in [ISO14496-2], subclause 6.1.3.8, Figure 6-5. + * intraDCVLC - a code determined by intra_dc_vlc_thr and QP. This allows a + * mechanism to switch between two VLC for coding of Intra DC + * coefficients as per [ISO14496-2], Table 6-21. + * ACPredFlag - a flag equal to ac_pred_flag (of luminance) indicating if + * the ac coefficients of the first row or first column are + * differentially coded for intra coded macroblock. + * shortVideoHeader - binary flag indicating presence of + * short_video_header; shortVideoHeader==1 selects linear intra DC + * mode, and shortVideoHeader==0 selects non linear intra DC mode. + * + * Output Arguments: + * + * ppBitStream - *ppBitStream is updated after the block is decoded, so + * that it points to the current byte in the bit stream buffer + * pBitOffset - *pBitOffset is updated so that it points to the current bit + * position in the byte pointed by *ppBitStream + * pDst - pointer to the block in the destination plane; must be aligned on + * an 8-byte boundary. + * pCoefBufRow - pointer to the updated coefficient row buffer. + * pCoefBufCol - pointer to the updated coefficient column buffer Note: + * The coefficient buffers must be updated in accordance with the + * update procedure defined in section 6.2.2. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments, if: + * - At least one of the following pointers is NULL: + * ppBitStream, *ppBitStream, pBitOffset, pCoefBufRow, pCoefBufCol, + * pQPBuf, pDst. + * - *pBitOffset exceeds [0,7] + * - curQP exceeds (1, 31) + * - blockIndex exceeds [0,5] + * - step is not the multiple of 8 + * - a pointer alignment requirement was violated. + * OMX_Sts_Err - status error. Refer to OMX_Sts_Err of DecodeVLCZigzag_Intra. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_DecodeBlockCoef_Intra ( + const OMX_U8 **ppBitStream, + OMX_INT *pBitOffset, + OMX_U8 *pDst, + OMX_INT step, + OMX_S16 *pCoefBufRow, + OMX_S16 *pCoefBufCol, + OMX_U8 curQP, + const OMX_U8 *pQPBuf, + OMX_INT blockIndex, + OMX_INT intraDCVLC, + OMX_INT ACPredFlag, + OMX_INT shortVideoHeader +); + + + +/** + * Function: omxVCM4P2_DecodeBlockCoef_Inter (6.2.5.4.2) + * + * Description: + * Decodes the INTER block coefficients. This function performs inverse + * quantization, inverse zigzag positioning, and IDCT (with appropriate + * clipping on each step) on the coefficients. The results (residuals) are + * placed in a contiguous array of 64 elements. For INTER block, the output + * buffer holds the residuals for further reconstruction. + * + * Input Arguments: + * + * ppBitStream - pointer to the pointer to the current byte in the bit + * stream buffer. There is no boundary check for the bit stream + * buffer. + * pBitOffset - pointer to the bit position in the byte pointed to by + * *ppBitStream. *pBitOffset is valid within [0-7] + * QP - quantization parameter + * shortVideoHeader - binary flag indicating presence of + * short_video_header; shortVideoHeader==1 selects linear intra DC + * mode, and shortVideoHeader==0 selects non linear intra DC mode. + * + * Output Arguments: + * + * ppBitStream - *ppBitStream is updated after the block is decoded, so + * that it points to the current byte in the bit stream buffer + * pBitOffset - *pBitOffset is updated so that it points to the current bit + * position in the byte pointed by *ppBitStream + * pDst - pointer to the decoded residual buffer (a contiguous array of 64 + * elements of OMX_S16 data type); must be aligned on a 16-byte + * boundary. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments, if: + * - At least one of the following pointers is Null: + * ppBitStream, *ppBitStream, pBitOffset , pDst + * - *pBitOffset exceeds [0,7] + * - QP <= 0. + * - pDst is not 16-byte aligned + * OMX_Sts_Err - status error. Refer to OMX_Sts_Err of DecodeVLCZigzag_Inter . + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_DecodeBlockCoef_Inter ( + const OMX_U8 **ppBitStream, + OMX_INT *pBitOffset, + OMX_S16 *pDst, + OMX_INT QP, + OMX_INT shortVideoHeader +); + + + +/** + * Function: omxVCM4P2_PredictReconCoefIntra (6.2.5.4.3) + * + * Description: + * Performs adaptive DC/AC coefficient prediction for an intra block. Prior + * to the function call, prediction direction (predDir) should be selected as + * specified in [ISO14496-2], subclause 7.4.3.1. + * + * Input Arguments: + * + * pSrcDst - pointer to the coefficient buffer which contains the quantized + * coefficient residuals (PQF) of the current block; must be + * aligned on a 4-byte boundary. The output coefficients are + * saturated to the range [-2048, 2047]. + * pPredBufRow - pointer to the coefficient row buffer; must be aligned on + * a 4-byte boundary. + * pPredBufCol - pointer to the coefficient column buffer; must be aligned + * on a 4-byte boundary. + * curQP - quantization parameter of the current block. curQP may equal to + * predQP especially when the current block and the predictor block + * are in the same macroblock. + * predQP - quantization parameter of the predictor block + * predDir - indicates the prediction direction which takes one of the + * following values: OMX_VC_HORIZONTAL - predict horizontally + * OMX_VC_VERTICAL - predict vertically + * ACPredFlag - a flag indicating if AC prediction should be performed. It + * is equal to ac_pred_flag in the bit stream syntax of MPEG-4 + * videoComp - video component type (luminance or chrominance) of the + * current block + * + * Output Arguments: + * + * pSrcDst - pointer to the coefficient buffer which contains the quantized + * coefficients (QF) of the current block + * pPredBufRow - pointer to the updated coefficient row buffer + * pPredBufCol - pointer to the updated coefficient column buffer Note: + * Buffer update: Update the AC prediction buffer (both row and + * column buffer). + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments, if: + * - At least one of the pointers is NULL: + * pSrcDst, pPredBufRow, or pPredBufCol. + * - curQP <= 0, + * - predQP <= 0, + * - curQP >31, + * - predQP > 31, + * - preDir exceeds [1,2] + * - pSrcDst, pPredBufRow, or pPredBufCol is not 4-byte aligned. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_PredictReconCoefIntra ( + OMX_S16 *pSrcDst, + OMX_S16 *pPredBufRow, + OMX_S16 *pPredBufCol, + OMX_INT curQP, + OMX_INT predQP, + OMX_INT predDir, + OMX_INT ACPredFlag, + OMXVCM4P2VideoComponent videoComp +); + + + +/** + * Function: omxVCM4P2_MCReconBlock (6.2.5.5.1) + * + * Description: + * Performs motion compensation prediction for an 8x8 block using + * interpolation described in [ISO14496-2], subclause 7.6.2. + * + * Input Arguments: + * + * pSrc - pointer to the block in the reference plane. + * srcStep - distance between the start of consecutive lines in the + * reference plane, in bytes; must be a multiple of 8. + * dstStep - distance between the start of consecutive lines in the + * destination plane, in bytes; must be a multiple of 8. + * pSrcResidue - pointer to a buffer containing the 16-bit prediction + * residuals; must be 16-byte aligned. If the pointer is NULL, then + * no prediction is done, only motion compensation, i.e., the block + * is moved with interpolation. + * predictType - bilinear interpolation type, as defined in section + * 6.2.1.2. + * rndVal - rounding control parameter: 0 - disabled; 1 - enabled. + * + * Output Arguments: + * + * pDst - pointer to the destination buffer; must be 8-byte aligned. If + * prediction residuals are added then output intensities are + * clipped to the range [0,255]. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned under any of the following + * conditions: + * - pDst is not 8-byte aligned. + * - pSrcResidue is not 16-byte aligned. + * - one or more of the following pointers is NULL: pSrc or pDst. + * - either srcStep or dstStep is not a multiple of 8. + * - invalid type specified for the parameter predictType. + * - the parameter rndVal is not equal either to 0 or 1. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P2_MCReconBlock ( + const OMX_U8 *pSrc, + OMX_INT srcStep, + const OMX_S16 *pSrcResidue, + OMX_U8 *pDst, + OMX_INT dstStep, + OMX_INT predictType, + OMX_INT rndVal +); + + + +/* 6.3.1.1 Intra 16x16 Prediction Modes */ +/* A data type that enumerates intra_16x16 macroblock prediction modes is defined as follows: */ + +typedef enum { + OMX_VC_16X16_VERT = 0, /** Intra_16x16_Vertical */ + OMX_VC_16X16_HOR = 1, /** Intra_16x16_Horizontal */ + OMX_VC_16X16_DC = 2, /** Intra_16x16_DC */ + OMX_VC_16X16_PLANE = 3 /** Intra_16x16_Plane */ +} OMXVCM4P10Intra16x16PredMode; + + + +/* 6.3.1.2 Intra 4x4 Prediction Modes */ +/* A data type that enumerates intra_4x4 macroblock prediction modes is defined as follows: */ + +typedef enum { + OMX_VC_4X4_VERT = 0, /** Intra_4x4_Vertical */ + OMX_VC_4X4_HOR = 1, /** Intra_4x4_Horizontal */ + OMX_VC_4X4_DC = 2, /** Intra_4x4_DC */ + OMX_VC_4X4_DIAG_DL = 3, /** Intra_4x4_Diagonal_Down_Left */ + OMX_VC_4X4_DIAG_DR = 4, /** Intra_4x4_Diagonal_Down_Right */ + OMX_VC_4X4_VR = 5, /** Intra_4x4_Vertical_Right */ + OMX_VC_4X4_HD = 6, /** Intra_4x4_Horizontal_Down */ + OMX_VC_4X4_VL = 7, /** Intra_4x4_Vertical_Left */ + OMX_VC_4X4_HU = 8 /** Intra_4x4_Horizontal_Up */ +} OMXVCM4P10Intra4x4PredMode; + + + +/* 6.3.1.3 Chroma Prediction Modes */ +/* A data type that enumerates intra chroma prediction modes is defined as follows: */ + +typedef enum { + OMX_VC_CHROMA_DC = 0, /** Intra_Chroma_DC */ + OMX_VC_CHROMA_HOR = 1, /** Intra_Chroma_Horizontal */ + OMX_VC_CHROMA_VERT = 2, /** Intra_Chroma_Vertical */ + OMX_VC_CHROMA_PLANE = 3 /** Intra_Chroma_Plane */ +} OMXVCM4P10IntraChromaPredMode; + + + +/* 6.3.1.4 Motion Estimation Modes */ +/* A data type that enumerates H.264 motion estimation modes is defined as follows: */ + +typedef enum { + OMX_VC_M4P10_FAST_SEARCH = 0, /** Fast motion search */ + OMX_VC_M4P10_FULL_SEARCH = 1 /** Full motion search */ +} OMXVCM4P10MEMode; + + + +/* 6.3.1.5 Macroblock Types */ +/* A data type that enumerates H.264 macroblock types is defined as follows: */ + +typedef enum { + OMX_VC_P_16x16 = 0, /* defined by [ISO14496-10] */ + OMX_VC_P_16x8 = 1, + OMX_VC_P_8x16 = 2, + OMX_VC_P_8x8 = 3, + OMX_VC_PREF0_8x8 = 4, + OMX_VC_INTER_SKIP = 5, + OMX_VC_INTRA_4x4 = 8, + OMX_VC_INTRA_16x16 = 9, + OMX_VC_INTRA_PCM = 10 +} OMXVCM4P10MacroblockType; + + + +/* 6.3.1.6 Sub-Macroblock Types */ +/* A data type that enumerates H.264 sub-macroblock types is defined as follows: */ + +typedef enum { + OMX_VC_SUB_P_8x8 = 0, /* defined by [ISO14496-10] */ + OMX_VC_SUB_P_8x4 = 1, + OMX_VC_SUB_P_4x8 = 2, + OMX_VC_SUB_P_4x4 = 3 +} OMXVCM4P10SubMacroblockType; + + + +/* 6.3.1.7 Variable Length Coding (VLC) Information */ + +typedef struct { + OMX_U8 uTrailing_Ones; /* Trailing ones; 3 at most */ + OMX_U8 uTrailing_One_Signs; /* Trailing ones signal */ + OMX_U8 uNumCoeffs; /* Total number of non-zero coefs, including trailing ones */ + OMX_U8 uTotalZeros; /* Total number of zero coefs */ + OMX_S16 iLevels[16]; /* Levels of non-zero coefs, in reverse zig-zag order */ + OMX_U8 uRuns[16]; /* Runs for levels and trailing ones, in reverse zig-zag order */ +} OMXVCM4P10VLCInfo; + + + +/* 6.3.1.8 Macroblock Information */ + +typedef struct { + OMX_S32 sliceId; /* slice number */ + OMXVCM4P10MacroblockType mbType; /* MB type */ + OMXVCM4P10SubMacroblockType subMBType[4]; /* sub-block type */ + OMX_S32 qpy; /* qp for luma */ + OMX_S32 qpc; /* qp for chroma */ + OMX_U32 cbpy; /* CBP Luma */ + OMX_U32 cbpc; /* CBP Chroma */ + OMXVCMotionVector pMV0[4][4]; /* motion vector, represented using 1/4-pel units, pMV0[blocky][blockx] (blocky = 0~3, blockx =0~3) */ + OMXVCMotionVector pMVPred[4][4]; /* motion vector prediction, Represented using 1/4-pel units, pMVPred[blocky][blockx] (blocky = 0~3, blockx = 0~3) */ + OMX_U8 pRefL0Idx[4]; /* reference picture indices */ + OMXVCM4P10Intra16x16PredMode Intra16x16PredMode; /* best intra 16x16 prediction mode */ + OMXVCM4P10Intra4x4PredMode pIntra4x4PredMode[16]; /* best intra 4x4 prediction mode for each block, pMV0 indexed as above */ +} OMXVCM4P10MBInfo, *OMXVCM4P10MBInfoPtr; + + + +/* 6.3.1.9 Motion Estimation Parameters */ + +typedef struct { + OMX_S32 blockSplitEnable8x8; /* enables 16x8, 8x16, 8x8 */ + OMX_S32 blockSplitEnable4x4; /* enable splitting of 8x4, 4x8, 4x4 blocks */ + OMX_S32 halfSearchEnable; + OMX_S32 quarterSearchEnable; + OMX_S32 intraEnable4x4; /* 1=enable, 0=disable */ + OMX_S32 searchRange16x16; /* integer pixel units */ + OMX_S32 searchRange8x8; + OMX_S32 searchRange4x4; +} OMXVCM4P10MEParams; + + + +/** + * Function: omxVCM4P10_PredictIntra_4x4 (6.3.3.1.1) + * + * Description: + * Perform Intra_4x4 prediction for luma samples. If the upper-right block is + * not available, then duplication work should be handled inside the function. + * Users need not define them outside. + * + * Input Arguments: + * + * pSrcLeft - Pointer to the buffer of 4 left pixels: + * p[x, y] (x = -1, y = 0..3) + * pSrcAbove - Pointer to the buffer of 8 above pixels: + * p[x,y] (x = 0..7, y =-1); + * must be aligned on a 4-byte boundary. + * pSrcAboveLeft - Pointer to the above left pixels: p[x,y] (x = -1, y = -1) + * leftStep - Step of left pixel buffer; must be a multiple of 4. + * dstStep - Step of the destination buffer; must be a multiple of 4. + * predMode - Intra_4x4 prediction mode. + * availability - Neighboring 4x4 block availability flag, refer to + * "Neighboring Macroblock Availability" . + * + * Output Arguments: + * + * pDst - Pointer to the destination buffer; must be aligned on a 4-byte + * boundary. + * + * Return Value: + * If the function runs without error, it returns OMX_Sts_NoErr. + * If one of the following cases occurs, the function returns + * OMX_Sts_BadArgErr: + * pDst is NULL. + * dstStep < 4, or dstStep is not a multiple of 4. + * leftStep is not a multiple of 4. + * predMode is not in the valid range of enumeration + * OMXVCM4P10Intra4x4PredMode. + * predMode is OMX_VC_4x4_VERT, but availability doesn't set OMX_VC_UPPER + * indicating p[x,-1] (x = 0..3) is not available. + * predMode is OMX_VC_4x4_HOR, but availability doesn't set OMX_VC_LEFT + * indicating p[-1,y] (y = 0..3) is not available. + * predMode is OMX_VC_4x4_DIAG_DL, but availability doesn't set + * OMX_VC_UPPER indicating p[x, -1] (x = 0..3) is not available. + * predMode is OMX_VC_4x4_DIAG_DR, but availability doesn't set + * OMX_VC_UPPER_LEFT or OMX_VC_UPPER or OMX_VC_LEFT indicating + * p[x,-1] (x = 0..3), or p[-1,y] (y = 0..3) or p[-1,-1] is not + * available. + * predMode is OMX_VC_4x4_VR, but availability doesn't set + * OMX_VC_UPPER_LEFT or OMX_VC_UPPER or OMX_VC_LEFT indicating + * p[x,-1] (x = 0..3), or p[-1,y] (y = 0..3) or p[-1,-1] is not + * available. + * predMode is OMX_VC_4x4_HD, but availability doesn't set + * OMX_VC_UPPER_LEFT or OMX_VC_UPPER or OMX_VC_LEFT indicating + * p[x,-1] (x = 0..3), or p[-1,y] (y = 0..3) or p[-1,-1] is not + * available. + * predMode is OMX_VC_4x4_VL, but availability doesn't set OMX_VC_UPPER + * indicating p[x,-1] (x = 0..3) is not available. + * predMode is OMX_VC_4x4_HU, but availability doesn't set OMX_VC_LEFT + * indicating p[-1,y] (y = 0..3) is not available. + * availability sets OMX_VC_UPPER, but pSrcAbove is NULL. + * availability sets OMX_VC_LEFT, but pSrcLeft is NULL. + * availability sets OMX_VC_UPPER_LEFT, but pSrcAboveLeft is NULL. + * either pSrcAbove or pDst is not aligned on a 4-byte boundary. + * + * Note: + * pSrcAbove, pSrcAbove, pSrcAboveLeft may be invalid pointers if + * they are not used by intra prediction as implied in predMode. + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_PredictIntra_4x4 ( + const OMX_U8 *pSrcLeft, + const OMX_U8 *pSrcAbove, + const OMX_U8 *pSrcAboveLeft, + OMX_U8 *pDst, + OMX_INT leftStep, + OMX_INT dstStep, + OMXVCM4P10Intra4x4PredMode predMode, + OMX_S32 availability +); + +__attribute__((weak)) +OMXResult HIGHFUNC_PredictIntraTransform_4x4( + const OMX_U8* pFrame, + OMX_U8* pDst, + OMX_INT frameStep, + OMX_INT dstStep, + const OMX_U8 **pCoeff, + const OMX_U8 *totalCoeff, + const OMX_U8* modeAvail, + OMX_U32 qpY +); + +/** + * Function: omxVCM4P10_PredictIntra_16x16 (6.3.3.1.2) + * + * Description: + * Perform Intra_16x16 prediction for luma samples. If the upper-right block + * is not available, then duplication work should be handled inside the + * function. Users need not define them outside. + * + * Input Arguments: + * + * pSrcLeft - Pointer to the buffer of 16 left pixels: p[x, y] (x = -1, y = + * 0..15) + * pSrcAbove - Pointer to the buffer of 16 above pixels: p[x,y] (x = 0..15, + * y= -1); must be aligned on a 16-byte boundary. + * pSrcAboveLeft - Pointer to the above left pixels: p[x,y] (x = -1, y = -1) + * leftStep - Step of left pixel buffer; must be a multiple of 16. + * dstStep - Step of the destination buffer; must be a multiple of 16. + * predMode - Intra_16x16 prediction mode, please refer to section 3.4.1. + * availability - Neighboring 16x16 MB availability flag. Refer to + * section 3.4.4. + * + * Output Arguments: + * + * pDst -Pointer to the destination buffer; must be aligned on a 16-byte + * boundary. + * + * Return Value: + * If the function runs without error, it returns OMX_Sts_NoErr. + * If one of the following cases occurs, the function returns + * OMX_Sts_BadArgErr: + * pDst is NULL. + * dstStep < 16. or dstStep is not a multiple of 16. + * leftStep is not a multiple of 16. + * predMode is not in the valid range of enumeration + * OMXVCM4P10Intra16x16PredMode + * predMode is OMX_VC_16X16_VERT, but availability doesn't set + * OMX_VC_UPPER indicating p[x,-1] (x = 0..15) is not available. + * predMode is OMX_VC_16X16_HOR, but availability doesn't set OMX_VC_LEFT + * indicating p[-1,y] (y = 0..15) is not available. + * predMode is OMX_VC_16X16_PLANE, but availability doesn't set + * OMX_VC_UPPER_LEFT or OMX_VC_UPPER or OMX_VC_LEFT indicating + * p[x,-1](x = 0..15), or p[-1,y] (y = 0..15), or p[-1,-1] is not + * available. + * availability sets OMX_VC_UPPER, but pSrcAbove is NULL. + * availability sets OMX_VC_LEFT, but pSrcLeft is NULL. + * availability sets OMX_VC_UPPER_LEFT, but pSrcAboveLeft is NULL. + * either pSrcAbove or pDst is not aligned on a 16-byte boundary. + * + * Note: + * pSrcAbove, pSrcAbove, pSrcAboveLeft may be invalid pointers if + * they are not used by intra prediction implied in predMode. + * Note: + * OMX_VC_UPPER_RIGHT is not used in intra_16x16 luma prediction. + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_PredictIntra_16x16 ( + const OMX_U8 *pSrcLeft, + const OMX_U8 *pSrcAbove, + const OMX_U8 *pSrcAboveLeft, + OMX_U8 *pDst, + OMX_INT leftStep, + OMX_INT dstStep, + OMXVCM4P10Intra16x16PredMode predMode, + OMX_S32 availability +); + + + +/** + * Function: omxVCM4P10_PredictIntraChroma_8x8 (6.3.3.1.3) + * + * Description: + * Performs intra prediction for chroma samples. + * + * Input Arguments: + * + * pSrcLeft - Pointer to the buffer of 8 left pixels: p[x, y] (x = -1, y= + * 0..7). + * pSrcAbove - Pointer to the buffer of 8 above pixels: p[x,y] (x = 0..7, y + * = -1); must be aligned on an 8-byte boundary. + * pSrcAboveLeft - Pointer to the above left pixels: p[x,y] (x = -1, y = -1) + * leftStep - Step of left pixel buffer; must be a multiple of 8. + * dstStep - Step of the destination buffer; must be a multiple of 8. + * predMode - Intra chroma prediction mode, please refer to section 3.4.3. + * availability - Neighboring chroma block availability flag, please refer + * to "Neighboring Macroblock Availability". + * + * Output Arguments: + * + * pDst - Pointer to the destination buffer; must be aligned on an 8-byte + * boundary. + * + * Return Value: + * If the function runs without error, it returns OMX_Sts_NoErr. + * If any of the following cases occurs, the function returns + * OMX_Sts_BadArgErr: + * pDst is NULL. + * dstStep < 8 or dstStep is not a multiple of 8. + * leftStep is not a multiple of 8. + * predMode is not in the valid range of enumeration + * OMXVCM4P10IntraChromaPredMode. + * predMode is OMX_VC_CHROMA_VERT, but availability doesn't set + * OMX_VC_UPPER indicating p[x,-1] (x = 0..7) is not available. + * predMode is OMX_VC_CHROMA_HOR, but availability doesn't set OMX_VC_LEFT + * indicating p[-1,y] (y = 0..7) is not available. + * predMode is OMX_VC_CHROMA_PLANE, but availability doesn't set + * OMX_VC_UPPER_LEFT or OMX_VC_UPPER or OMX_VC_LEFT indicating + * p[x,-1](x = 0..7), or p[-1,y] (y = 0..7), or p[-1,-1] is not + * available. + * availability sets OMX_VC_UPPER, but pSrcAbove is NULL. + * availability sets OMX_VC_LEFT, but pSrcLeft is NULL. + * availability sets OMX_VC_UPPER_LEFT, but pSrcAboveLeft is NULL. + * either pSrcAbove or pDst is not aligned on a 8-byte boundary. + * + * Note: pSrcAbove, pSrcAbove, pSrcAboveLeft may be invalid pointer if + * they are not used by intra prediction implied in predMode. + * + * Note: OMX_VC_UPPER_RIGHT is not used in intra chroma prediction. + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_PredictIntraChroma_8x8 ( + const OMX_U8 *pSrcLeft, + const OMX_U8 *pSrcAbove, + const OMX_U8 *pSrcAboveLeft, + OMX_U8 *pDst, + OMX_INT leftStep, + OMX_INT dstStep, + OMXVCM4P10IntraChromaPredMode predMode, + OMX_S32 availability +); + + + +/** + * Function: omxVCM4P10_InterpolateLuma (6.3.3.2.1) + * + * Description: + * Performs quarter-pixel interpolation for inter luma MB. It is assumed that + * the frame is already padded when calling this function. + * + * Input Arguments: + * + * pSrc - Pointer to the source reference frame buffer + * srcStep - reference frame step, in bytes; must be a multiple of roi.width + * dstStep - destination frame step, in bytes; must be a multiple of + * roi.width + * dx - Fractional part of horizontal motion vector component in 1/4 pixel + * unit; valid in the range [0,3] + * dy - Fractional part of vertical motion vector y component in 1/4 pixel + * unit; valid in the range [0,3] + * roi - Dimension of the interpolation region; the parameters roi.width and + * roi.height must be equal to either 4, 8, or 16. + * + * Output Arguments: + * + * pDst - Pointer to the destination frame buffer: + * if roi.width==4, 4-byte alignment required + * if roi.width==8, 8-byte alignment required + * if roi.width==16, 16-byte alignment required + * + * Return Value: + * If the function runs without error, it returns OMX_Sts_NoErr. + * If one of the following cases occurs, the function returns + * OMX_Sts_BadArgErr: + * pSrc or pDst is NULL. + * srcStep or dstStep < roi.width. + * dx or dy is out of range [0,3]. + * roi.width or roi.height is out of range {4, 8, 16}. + * roi.width is equal to 4, but pDst is not 4 byte aligned. + * roi.width is equal to 8 or 16, but pDst is not 8 byte aligned. + * srcStep or dstStep is not a multiple of 8. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P10_InterpolateLuma ( + const OMX_U8 *pSrc, + OMX_S32 srcStep, + OMX_U8 *pDst, + OMX_S32 dstStep, + OMX_S32 dx, + OMX_S32 dy, + OMXSize roi +); + + + +/** + * Function: omxVCM4P10_InterpolateChroma (6.3.3.2.2) + * + * Description: + * Performs 1/8-pixel interpolation for inter chroma MB. + * + * Input Arguments: + * + * pSrc -Pointer to the source reference frame buffer + * srcStep -Reference frame step in bytes + * dstStep -Destination frame step in bytes; must be a multiple of + * roi.width. + * dx -Fractional part of horizontal motion vector component in 1/8 pixel + * unit; valid in the range [0,7] + * dy -Fractional part of vertical motion vector component in 1/8 pixel + * unit; valid in the range [0,7] + * roi -Dimension of the interpolation region; the parameters roi.width and + * roi.height must be equal to either 2, 4, or 8. + * + * Output Arguments: + * + * pDst -Pointer to the destination frame buffer: + * if roi.width==2, 2-byte alignment required + * if roi.width==4, 4-byte alignment required + * if roi.width==8, 8-byte alignment required + * + * Return Value: + * If the function runs without error, it returns OMX_Sts_NoErr. + * If one of the following cases occurs, the function returns + * OMX_Sts_BadArgErr: + * pSrc or pDst is NULL. + * srcStep or dstStep < 8. + * dx or dy is out of range [0-7]. + * roi.width or roi.height is out of range {2,4,8}. + * roi.width is equal to 2, but pDst is not 2-byte aligned. + * roi.width is equal to 4, but pDst is not 4-byte aligned. + * roi.width is equal to 8, but pDst is not 8 byte aligned. + * srcStep or dstStep is not a multiple of 8. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P10_InterpolateChroma ( + const OMX_U8 *pSrc, + OMX_S32 srcStep, + OMX_U8 *pDst, + OMX_S32 dstStep, + OMX_S32 dx, + OMX_S32 dy, + OMXSize roi +); + + + +/** + * Function: omxVCM4P10_FilterDeblockingLuma_VerEdge_I (6.3.3.3.1) + * + * Description: + * Performs in-place deblock filtering on four vertical edges of the luma + * macroblock (16x16). + * + * Input Arguments: + * + * pSrcDst - Pointer to the input macroblock; must be 16-byte aligned. + * srcdstStep -Step of the arrays; must be a multiple of 16. + * pAlpha -Array of size 2 of alpha thresholds (the first item is the alpha + * threshold for the external vertical edge, and the second item is + * for the internal vertical edge); per [ISO14496-10] alpha values + * must be in the range [0,255]. + * pBeta -Array of size 2 of beta thresholds (the first item is the beta + * threshold for the external vertical edge, and the second item is + * for the internal vertical edge); per [ISO14496-10] beta values + * must be in the range [0,18]. + * pThresholds -Array of size 16 of Thresholds (TC0) (values for the left + * edge of each 4x4 block, arranged in vertical block order); must + * be aligned on a 4-byte boundary.. Per [ISO14496-10] values must + * be in the range [0,25]. + * pBS -Array of size 16 of BS parameters (arranged in vertical block + * order); valid in the range [0,4] with the following + * restrictions: i) pBS[i]== 4 may occur only for 0<=i<=3, ii) + * pBS[i]== 4 if and only if pBS[i^3]== 4. Must be 4-byte aligned. + * + * Output Arguments: + * + * pSrcDst -Pointer to filtered output macroblock. + * + * Return Value: + * If the function runs without error, it returns OMX_Sts_NoErr. + * If one of the following cases occurs, the function returns + * OMX_Sts_BadArgErr: + * Either of the pointers in pSrcDst, pAlpha, pBeta, pThresholds, or pBS + * is NULL. + * Either pThresholds or pBS is not aligned on a 4-byte boundary. + * pSrcDst is not 16-byte aligned. + * srcdstStep is not a multiple of 16. + * pAlpha[0] and/or pAlpha[1] is outside the range [0,255]. + * pBeta[0] and/or pBeta[1] is outside the range [0,18]. + * One or more entries in the table pThresholds[0..15]is outside of the + * range [0,25]. + * pBS is out of range, i.e., one of the following conditions is true: + * pBS[i]<0, pBS[i]>4, pBS[i]==4 for i>=4, or (pBS[i]==4 && + * pBS[i^3]!=4) for 0<=i<=3. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P10_FilterDeblockingLuma_VerEdge_I ( + OMX_U8 *pSrcDst, + OMX_S32 srcdstStep, + const OMX_U8 *pAlpha, + const OMX_U8 *pBeta, + const OMX_U8 *pThresholds, + const OMX_U8 *pBS +); + + + +/** + * Function: omxVCM4P10_FilterDeblockingLuma_HorEdge_I (6.3.3.3.2) + * + * Description: + * Performs in-place deblock filtering on four horizontal edges of the luma + * macroblock (16x16). + * + * Input Arguments: + * + * pSrcDst - pointer to the input macroblock; must be 16-byte aligned. + * srcdstStep - step of the arrays; must be a multiple of 16. + * pAlpha - array of size 2 of alpha thresholds (the first item is the alpha + * threshold for the external vertical edge, and the second item is + * for the internal horizontal edge); per [ISO14496-10] alpha + * values must be in the range [0,255]. + * pBeta - array of size 2 of beta thresholds (the first item is the beta + * threshold for the external horizontal edge, and the second item + * is for the internal horizontal edge). Per [ISO14496-10] beta + * values must be in the range [0,18]. + * pThresholds - array of size 16 containing thresholds, TC0, for the top + * horizontal edge of each 4x4 block, arranged in horizontal block + * order; must be aligned on a 4-byte boundary. Per [ISO14496 10] + * values must be in the range [0,25]. + * pBS - array of size 16 of BS parameters (arranged in horizontal block + * order); valid in the range [0,4] with the following + * restrictions: i) pBS[i]== 4 may occur only for 0<=i<=3, ii) + * pBS[i]== 4 if and only if pBS[i^3]== 4. Must be 4-byte aligned. + * + * Output Arguments: + * + * pSrcDst -Pointer to filtered output macroblock. + * + * Return Value: + * + * OMX_Sts_NoErr, if the function runs without error. + * + * OMX_Sts_BadArgErr, if one of the following cases occurs: + * - one or more of the following pointers is NULL: pSrcDst, pAlpha, + * pBeta, pThresholds, or pBS. + * - either pThresholds or pBS is not aligned on a 4-byte boundary. + * - pSrcDst is not 16-byte aligned. + * - srcdstStep is not a multiple of 16. + * - pAlpha[0] and/or pAlpha[1] is outside the range [0,255]. + * - pBeta[0] and/or pBeta[1] is outside the range [0,18]. + * - One or more entries in the table pThresholds[0..15] is + * outside of the range [0,25]. + * - pBS is out of range, i.e., one of the following conditions is true: + * pBS[i]<0, pBS[i]>4, pBS[i]==4 for i>=4, or + * (pBS[i]==4 && pBS[i^3]!=4) for 0<=i<=3. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P10_FilterDeblockingLuma_HorEdge_I ( + OMX_U8 *pSrcDst, + OMX_S32 srcdstStep, + const OMX_U8 *pAlpha, + const OMX_U8 *pBeta, + const OMX_U8 *pThresholds, + const OMX_U8 *pBS +); + + + +/** + * Function: omxVCM4P10_FilterDeblockingChroma_VerEdge_I (6.3.3.3.3) + * + * Description: + * Performs in-place deblock filtering on four vertical edges of the chroma + * macroblock (8x8). + * + * Input Arguments: + * + * pSrcDst - Pointer to the input macroblock; must be 8-byte aligned. + * srcdstStep - Step of the arrays; must be a multiple of 8. + * pAlpha - Array of size 2 of alpha thresholds (the first item is alpha + * threshold for external vertical edge, and the second item is for + * internal vertical edge); per [ISO14496-10] alpha values must be + * in the range [0,255]. + * pBeta - Array of size 2 of beta thresholds (the first item is the beta + * threshold for the external vertical edge, and the second item is + * for the internal vertical edge); per [ISO14496-10] beta values + * must be in the range [0,18]. + * pThresholds - Array of size 8 containing thresholds, TC0, for the left + * vertical edge of each 4x2 chroma block, arranged in vertical + * block order; must be aligned on a 4-byte boundary. Per + * [ISO14496-10] values must be in the range [0,25]. + * pBS - Array of size 16 of BS parameters (values for each 2x2 chroma + * block, arranged in vertical block order). This parameter is the + * same as the pBS parameter passed into FilterDeblockLuma_VerEdge; + * valid in the range [0,4] with the following restrictions: i) + * pBS[i]== 4 may occur only for 0<=i<=3, ii) pBS[i]== 4 if and + * only if pBS[i^3]== 4. Must be 4 byte aligned. + * + * Output Arguments: + * + * pSrcDst -Pointer to filtered output macroblock. + * + * Return Value: + * + * OMX_Sts_NoErr, if the function runs without error. + * + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - one or more of the following pointers is NULL: pSrcDst, pAlpha, + * pBeta, pThresholds, or pBS. + * - pSrcDst is not 8-byte aligned. + * - srcdstStep is not a multiple of 8. + * - pThresholds is not 4-byte aligned. + * - pAlpha[0] and/or pAlpha[1] is outside the range [0,255]. + * - pBeta[0] and/or pBeta[1] is outside the range [0,18]. + * - One or more entries in the table pThresholds[0..7] is outside + * of the range [0,25]. + * - pBS is out of range, i.e., one of the following conditions is true: + * pBS[i]<0, pBS[i]>4, pBS[i]==4 for i>=4, or + * (pBS[i]==4 && pBS[i^3]!=4) for 0<=i<=3. + * - pBS is not 4-byte aligned. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P10_FilterDeblockingChroma_VerEdge_I ( + OMX_U8 *pSrcDst, + OMX_S32 srcdstStep, + const OMX_U8 *pAlpha, + const OMX_U8 *pBeta, + const OMX_U8 *pThresholds, + const OMX_U8 *pBS +); + + + +/** + * Function: omxVCM4P10_FilterDeblockingChroma_HorEdge_I (6.3.3.3.4) + * + * Description: + * Performs in-place deblock filtering on the horizontal edges of the chroma + * macroblock (8x8). + * + * Input Arguments: + * + * pSrcDst - pointer to the input macroblock; must be 8-byte aligned. + * srcdstStep - array step; must be a multiple of 8. + * pAlpha - array of size 2 containing alpha thresholds; the first element + * contains the threshold for the external horizontal edge, and the + * second element contains the threshold for internal horizontal + * edge. Per [ISO14496-10] alpha values must be in the range + * [0,255]. + * pBeta - array of size 2 containing beta thresholds; the first element + * contains the threshold for the external horizontal edge, and the + * second element contains the threshold for the internal + * horizontal edge. Per [ISO14496-10] beta values must be in the + * range [0,18]. + * pThresholds - array of size 8 containing thresholds, TC0, for the top + * horizontal edge of each 2x4 chroma block, arranged in horizontal + * block order; must be aligned on a 4-byte boundary. Per + * [ISO14496-10] values must be in the range [0,25]. + * pBS - array of size 16 containing BS parameters for each 2x2 chroma + * block, arranged in horizontal block order; valid in the range + * [0,4] with the following restrictions: i) pBS[i]== 4 may occur + * only for 0<=i<=3, ii) pBS[i]== 4 if and only if pBS[i^3]== 4. + * Must be 4-byte aligned. + * + * Output Arguments: + * + * pSrcDst -Pointer to filtered output macroblock. + * + * Return Value: + * + * OMX_Sts_NoErr, if the function runs without error. + * + * OMX_Sts_BadArgErr, if one of the following cases occurs: + * - any of the following pointers is NULL: + * pSrcDst, pAlpha, pBeta, pThresholds, or pBS. + * - pSrcDst is not 8-byte aligned. + * - srcdstStep is not a multiple of 8. + * - pThresholds is not 4-byte aligned. + * - pAlpha[0] and/or pAlpha[1] is outside the range [0,255]. + * - pBeta[0] and/or pBeta[1] is outside the range [0,18]. + * - One or more entries in the table pThresholds[0..7] is outside + * of the range [0,25]. + * - pBS is out of range, i.e., one of the following conditions is true: + * pBS[i]<0, pBS[i]>4, pBS[i]==4 for i>=4, or + * (pBS[i]==4 && pBS[i^3]!=4) for 0<=i<=3. + * - pBS is not 4-byte aligned. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P10_FilterDeblockingChroma_HorEdge_I ( + OMX_U8 *pSrcDst, + OMX_S32 srcdstStep, + const OMX_U8 *pAlpha, + const OMX_U8 *pBeta, + const OMX_U8 *pThresholds, + const OMX_U8 *pBS +); + + + +/** + * Function: omxVCM4P10_DeblockLuma_I (6.3.3.3.5) + * + * Description: + * This function performs in-place deblock filtering the horizontal and + * vertical edges of a luma macroblock (16x16). + * + * Input Arguments: + * + * pSrcDst - pointer to the input macroblock; must be 16-byte aligned. + * srcdstStep - image width; must be a multiple of 16. + * pAlpha - pointer to a 2x2 table of alpha thresholds, organized as + * follows: {external vertical edge, internal vertical edge, + * external horizontal edge, internal horizontal edge }. Per + * [ISO14496-10] alpha values must be in the range [0,255]. + * pBeta - pointer to a 2x2 table of beta thresholds, organized as follows: + * {external vertical edge, internal vertical edge, external + * horizontal edge, internal horizontal edge }. Per [ISO14496-10] + * beta values must be in the range [0,18]. + * pThresholds - pointer to a 16x2 table of threshold (TC0), organized as + * follows: {values for the left or above edge of each 4x4 block, + * arranged in vertical block order and then in horizontal block + * order}; must be aligned on a 4-byte boundary. Per [ISO14496-10] + * values must be in the range [0,25]. + * pBS - pointer to a 16x2 table of BS parameters arranged in scan block + * order for vertical edges and then horizontal edges; valid in the + * range [0,4] with the following restrictions: i) pBS[i]== 4 may + * occur only for 0<=i<=3, ii) pBS[i]== 4 if and only if pBS[i^3]== + * 4. Must be 4-byte aligned. + * + * Output Arguments: + * + * pSrcDst - pointer to filtered output macroblock. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments + * - one or more of the following pointers is NULL: pSrcDst, pAlpha, + * pBeta, pThresholds or pBS. + * - pSrcDst is not 16-byte aligned. + * - either pThresholds or pBS is not aligned on a 4-byte boundary. + * - one or more entries in the table pAlpha[0..3] is outside the range + * [0,255]. + * - one or more entries in the table pBeta[0..3] is outside the range + * [0,18]. + * - one or more entries in the table pThresholds[0..31]is outside of + * the range [0,25]. + * - pBS is out of range, i.e., one of the following conditions is true: + * pBS[i]<0, pBS[i]>4, pBS[i]==4 for i>=4, or + * (pBS[i]==4 && pBS[i^3]!=4) for 0<=i<=3. + * - srcdstStep is not a multiple of 16. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P10_DeblockLuma_I ( + OMX_U8 *pSrcDst, + OMX_S32 srcdstStep, + const OMX_U8 *pAlpha, + const OMX_U8 *pBeta, + const OMX_U8 *pThresholds, + const OMX_U8 *pBS +); + + + +/** + * Function: omxVCM4P10_DeblockChroma_I (6.3.3.3.6) + * + * Description: + * Performs in-place deblocking filtering on all edges of the chroma + * macroblock (16x16). + * + * Input Arguments: + * + * pSrcDst - pointer to the input macroblock; must be 8-byte aligned. + * srcdstStep - step of the arrays; must be a multiple of 8. + * pAlpha - pointer to a 2x2 array of alpha thresholds, organized as + * follows: {external vertical edge, internal vertical edge, + * external horizontal edge, internal horizontal edge }. Per + * [ISO14496-10] alpha values must be in the range [0,255]. + * pBeta - pointer to a 2x2 array of Beta Thresholds, organized as follows: + * { external vertical edge, internal vertical edge, external + * horizontal edge, internal horizontal edge }. Per [ISO14496-10] + * beta values must be in the range [0,18]. + * pThresholds - array of size 8x2 of Thresholds (TC0) (values for the left + * or above edge of each 4x2 or 2x4 block, arranged in vertical + * block order and then in horizontal block order); must be aligned + * on a 4-byte boundary. Per [ISO14496-10] values must be in the + * range [0,25]. + * pBS - array of size 16x2 of BS parameters (arranged in scan block order + * for vertical edges and then horizontal edges); valid in the + * range [0,4] with the following restrictions: i) pBS[i]== 4 may + * occur only for 0<=i<=3, ii) pBS[i]== 4 if and only if pBS[i^3]== + * 4. Must be 4-byte aligned. + * + * Output Arguments: + * + * pSrcDst - pointer to filtered output macroblock. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments + * - one or more of the following pointers is NULL: pSrcDst, pAlpha, + * pBeta, pThresholds, or pBS. + * - pSrcDst is not 8-byte aligned. + * - either pThresholds or pBS is not 4-byte aligned. + * - one or more entries in the table pAlpha[0..3] is outside the range + * [0,255]. + * - one or more entries in the table pBeta[0..3] is outside the range + * [0,18]. + * - one or more entries in the table pThresholds[0..15]is outside of + * the range [0,25]. + * - pBS is out of range, i.e., one of the following conditions is true: + * pBS[i]<0, pBS[i]>4, pBS[i]==4 for i>=4, or + * (pBS[i]==4 && pBS[i^3]!=4) for 0<=i<=3. + * - srcdstStep is not a multiple of 8. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P10_DeblockChroma_I ( + OMX_U8 *pSrcDst, + OMX_S32 srcdstStep, + const OMX_U8 *pAlpha, + const OMX_U8 *pBeta, + const OMX_U8 *pThresholds, + const OMX_U8 *pBS +); + + + +/** + * Function: omxVCM4P10_DecodeChromaDcCoeffsToPairCAVLC (6.3.4.1.1) + * + * Description: + * Performs CAVLC decoding and inverse raster scan for a 2x2 block of + * ChromaDCLevel. The decoded coefficients in the packed position-coefficient + * buffer are stored in reverse zig-zag order, i.e., the first buffer element + * contains the last non-zero postion-coefficient pair of the block. Within + * each position-coefficient pair, the position entry indicates the + * raster-scan position of the coefficient, while the coefficient entry + * contains the coefficient value. + * + * Input Arguments: + * + * ppBitStream - Double pointer to current byte in bit stream buffer + * pOffset - Pointer to current bit position in the byte pointed to by + * *ppBitStream; valid in the range [0,7]. + * + * Output Arguments: + * + * ppBitStream - *ppBitStream is updated after each block is decoded + * pOffset - *pOffset is updated after each block is decoded + * pNumCoeff - Pointer to the number of nonzero coefficients in this block + * ppPosCoefBuf - Double pointer to destination residual + * coefficient-position pair buffer. Buffer position + * (*ppPosCoefBuf) is updated upon return, unless there are only + * zero coefficients in the currently decoded block. In this case + * the caller is expected to bypass the transform/dequantization of + * the empty blocks. + * + * Return Value: + * + * OMX_Sts_NoErr, if the function runs without error. + * + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - ppBitStream or pOffset is NULL. + * - ppPosCoefBuf or pNumCoeff is NULL. + * OMX_Sts_Err - if one of the following is true: + * - an illegal code is encountered in the bitstream + * + */ + __attribute__((weak)) +OMXResult omxVCM4P10_DecodeChromaDcCoeffsToPairCAVLC ( + const OMX_U8 **ppBitStream, + OMX_S32*pOffset, + OMX_U8 *pNumCoeff, + OMX_U8 **ppPosCoefbuf +); + + + +/** + * Function: omxVCM4P10_DecodeCoeffsToPairCAVLC (6.3.4.1.2) + * + * Description: + * Performs CAVLC decoding and inverse zigzag scan for 4x4 block of + * Intra16x16DCLevel, Intra16x16ACLevel, LumaLevel, and ChromaACLevel. Inverse + * field scan is not supported. The decoded coefficients in the packed + * position-coefficient buffer are stored in reverse zig-zag order, i.e., the + * first buffer element contains the last non-zero postion-coefficient pair of + * the block. Within each position-coefficient pair, the position entry + * indicates the raster-scan position of the coefficient, while the + * coefficient entry contains the coefficient value. + * + * Input Arguments: + * + * ppBitStream -Double pointer to current byte in bit stream buffer + * pOffset - Pointer to current bit position in the byte pointed to by + * *ppBitStream; valid in the range [0,7]. + * sMaxNumCoeff - Maximum the number of non-zero coefficients in current + * block + * sVLCSelect - VLC table selector, obtained from the number of non-zero + * coefficients contained in the above and left 4x4 blocks. It is + * equivalent to the variable nC described in H.264 standard table + * 9 5, except its value can t be less than zero. + * + * Output Arguments: + * + * ppBitStream - *ppBitStream is updated after each block is decoded. + * Buffer position (*ppPosCoefBuf) is updated upon return, unless + * there are only zero coefficients in the currently decoded block. + * In this case the caller is expected to bypass the + * transform/dequantization of the empty blocks. + * pOffset - *pOffset is updated after each block is decoded + * pNumCoeff - Pointer to the number of nonzero coefficients in this block + * ppPosCoefBuf - Double pointer to destination residual + * coefficient-position pair buffer + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - ppBitStream or pOffset is NULL. + * - ppPosCoefBuf or pNumCoeff is NULL. + * - sMaxNumCoeff is not equal to either 15 or 16. + * - sVLCSelect is less than 0. + * + * OMX_Sts_Err - if one of the following is true: + * - an illegal code is encountered in the bitstream + * + */ + __attribute__((weak)) +OMXResult omxVCM4P10_DecodeCoeffsToPairCAVLC ( + const OMX_U8 **ppBitStream, + OMX_S32 *pOffset, + OMX_U8 *pNumCoeff, + OMX_U8 **ppPosCoefbuf, + OMX_INT sVLCSelect, + OMX_INT sMaxNumCoeff +); + + +/** + * Function: omxVCM4P10_TransformDequantLumaDCFromPair (6.3.4.2.1) + * + * Description: + * Reconstructs the 4x4 LumaDC block from the coefficient-position pair + * buffer, performs integer inverse, and dequantization for 4x4 LumaDC + * coefficients, and updates the pair buffer pointer to the next non-empty + * block. + * + * Input Arguments: + * + * ppSrc - Double pointer to residual coefficient-position pair buffer + * output by CALVC decoding + * QP - Quantization parameter QpY + * + * Output Arguments: + * + * ppSrc - *ppSrc is updated to the start of next non empty block + * pDst - Pointer to the reconstructed 4x4 LumaDC coefficients buffer; must + * be aligned on a 8-byte boundary. + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - ppSrc or pDst is NULL. + * - pDst is not 8 byte aligned. + * - QP is not in the range of [0-51]. + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_TransformDequantLumaDCFromPair ( + const OMX_U8 **ppSrc, + OMX_S16 *pDst, + OMX_INT QP +); + + + +/** + * Function: omxVCM4P10_TransformDequantChromaDCFromPair (6.3.4.2.2) + * + * Description: + * Reconstruct the 2x2 ChromaDC block from coefficient-position pair buffer, + * perform integer inverse transformation, and dequantization for 2x2 chroma + * DC coefficients, and update the pair buffer pointer to next non-empty + * block. + * + * Input Arguments: + * + * ppSrc - Double pointer to residual coefficient-position pair buffer + * output by CALVC decoding + * QP - Quantization parameter QpC + * + * Output Arguments: + * + * ppSrc - *ppSrc is updated to the start of next non empty block + * pDst - Pointer to the reconstructed 2x2 ChromaDC coefficients buffer; + * must be aligned on a 4-byte boundary. + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - ppSrc or pDst is NULL. + * - pDst is not 4-byte aligned. + * - QP is not in the range of [0-51]. + * + */ + __attribute__((weak)) +OMXResult omxVCM4P10_TransformDequantChromaDCFromPair ( + const OMX_U8 **ppSrc, + OMX_S16 *pDst, + OMX_INT QP +); + + + +/** + * Function: omxVCM4P10_DequantTransformResidualFromPairAndAdd (6.3.4.2.3) + * + * Description: + * Reconstruct the 4x4 residual block from coefficient-position pair buffer, + * perform dequantization and integer inverse transformation for 4x4 block of + * residuals with previous intra prediction or motion compensation data, and + * update the pair buffer pointer to next non-empty block. If pDC == NULL, + * there re 16 non-zero AC coefficients at most in the packed buffer starting + * from 4x4 block position 0; If pDC != NULL, there re 15 non-zero AC + * coefficients at most in the packet buffer starting from 4x4 block position + * 1. + * + * Input Arguments: + * + * ppSrc - Double pointer to residual coefficient-position pair buffer + * output by CALVC decoding + * pPred - Pointer to the predicted 4x4 block; must be aligned on a 4-byte + * boundary + * predStep - Predicted frame step size in bytes; must be a multiple of 4 + * dstStep - Destination frame step in bytes; must be a multiple of 4 + * pDC - Pointer to the DC coefficient of this block, NULL if it doesn't + * exist + * QP - QP Quantization parameter. It should be QpC in chroma 4x4 block + * decoding, otherwise it should be QpY. + * AC - Flag indicating if at least one non-zero AC coefficient exists + * + * Output Arguments: + * + * pDst - pointer to the reconstructed 4x4 block data; must be aligned on a + * 4-byte boundary + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - pPred or pDst is NULL. + * - pPred or pDst is not 4-byte aligned. + * - predStep or dstStep is not a multiple of 4. + * - AC !=0 and Qp is not in the range of [0-51] or ppSrc == NULL. + * - AC ==0 && pDC ==NULL. + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_DequantTransformResidualFromPairAndAdd ( + const OMX_U8 **ppSrc, + const OMX_U8 *pPred, + const OMX_S16 *pDC, + OMX_U8 *pDst, + OMX_INT predStep, + OMX_INT dstStep, + OMX_INT QP, + OMX_INT AC +); + + +// Same as above, but iterate 16 times for each luma 4x4 partition +__attribute__((weak)) +OMXResult HIGHFUNC_ProcessLumaInterResidual ( + const OMX_U8 **ppSrc, + const OMX_U8 *pPred, + OMX_U8 *pDst, + OMX_INT predStep, + OMX_INT dstStep, + OMX_INT QP, + OMX_U8 *AC +); + +__attribute__((weak)) +OMXResult HIGHFUNC_ProcessLumaIntra16x16Residual ( + const OMX_U8 *pSrc, + OMX_U8 *pDst, + OMX_INT srcStep, + OMX_INT dstStep, + const OMX_U8 **pCoeff, + const OMX_U8 *totalCoeff, + OMX_S32 predMode, + OMX_S32 availability, + OMX_INT QP +); + +__attribute__((weak)) +OMXResult HIGHFUNC_ProcessChromaResidual ( + const OMX_U8 **pSrc, + OMX_U8 *pDst1, + OMX_U8 *pDst2, + OMX_INT dstStep1, + OMX_INT dstStep2, + OMX_INT chromaQp, + const OMX_U8 *totalCoeff +); + + +/** + * Function: omxVCM4P10_MEGetBufSize (6.3.5.1.1) + * + * Description: + * Computes the size, in bytes, of the vendor-specific specification + * structure for the omxVCM4P10 motion estimation functions BlockMatch_Integer + * and MotionEstimationMB. + * + * Input Arguments: + * + * MEmode - motion estimation mode; available modes are defined by the + * enumerated type OMXVCM4P10MEMode + * pMEParams -motion estimation parameters + * + * Output Arguments: + * + * pSize - pointer to the number of bytes required for the motion + * estimation specification structure + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - pMEParams or pSize is NULL. + * - an invalid MEMode is specified. + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_MEGetBufSize ( + OMXVCM4P10MEMode MEmode, + const OMXVCM4P10MEParams *pMEParams, + OMX_U32 *pSize +); + + + +/** + * Function: omxVCM4P10_MEInit (6.3.5.1.2) + * + * Description: + * Initializes the vendor-specific specification structure required for the + * omxVCM4P10 motion estimation functions: BlockMatch_Integer and + * MotionEstimationMB. Memory for the specification structure *pMESpec must be + * allocated prior to calling the function, and should be aligned on a 4-byte + * boundary. The number of bytes required for the specification structure can + * be determined using the function omxVCM4P10_MEGetBufSize. Following + * initialization by this function, the vendor-specific structure *pMESpec + * should contain an implementation-specific representation of all motion + * estimation parameters received via the structure pMEParams, for example + * searchRange16x16, searchRange8x8, etc. + * + * Input Arguments: + * + * MEmode - motion estimation mode; available modes are defined by the + * enumerated type OMXVCM4P10MEMode + * pMEParams - motion estimation parameters + * pMESpec - pointer to the uninitialized ME specification structure + * + * Output Arguments: + * + * pMESpec - pointer to the initialized ME specification structure + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - pMEParams or pSize is NULL. + * - an invalid value was specified for the parameter MEmode + * - a negative or zero value was specified for one of the search ranges + * (e.g., pMBParams >searchRange8x8, pMEParams->searchRange16x16, etc.) + * - either in isolation or in combination, one or more of the enables or + * search ranges in the structure *pMEParams were configured such + * that the requested behavior fails to comply with [ISO14496-10]. + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_MEInit ( + OMXVCM4P10MEMode MEmode, + const OMXVCM4P10MEParams *pMEParams, + void *pMESpec +); + + + +/** + * Function: omxVCM4P10_BlockMatch_Integer (6.3.5.2.1) + * + * Description: + * Performs integer block match. Returns best MV and associated cost. + * + * Input Arguments: + * + * pSrcOrgY - Pointer to the top-left corner of the current block: + * If iBlockWidth==4, 4-byte alignment required. + * If iBlockWidth==8, 8-byte alignment required. + * If iBlockWidth==16, 16-byte alignment required. + * pSrcRefY - Pointer to the top-left corner of the co-located block in the + * reference picture: + * If iBlockWidth==4, 4-byte alignment required. + * If iBlockWidth==8, 8-byte alignment required. + * If iBlockWidth==16, 16-byte alignment required. + * nSrcOrgStep - Stride of the original picture plane, expressed in terms + * of integer pixels; must be a multiple of iBlockWidth. + * nSrcRefStep - Stride of the reference picture plane, expressed in terms + * of integer pixels + * pRefRect - pointer to the valid reference rectangle inside the reference + * picture plane + * nCurrPointPos - position of the current block in the current plane + * iBlockWidth - Width of the current block, expressed in terms of integer + * pixels; must be equal to either 4, 8, or 16. + * iBlockHeight - Height of the current block, expressed in terms of + * integer pixels; must be equal to either 4, 8, or 16. + * nLamda - Lamda factor; used to compute motion cost + * pMVPred - Predicted MV; used to compute motion cost, expressed in terms + * of 1/4-pel units + * pMVCandidate - Candidate MV; used to initialize the motion search, + * expressed in terms of integer pixels + * pMESpec - pointer to the ME specification structure + * + * Output Arguments: + * + * pDstBestMV - Best MV resulting from integer search, expressed in terms + * of 1/4-pel units + * pBestCost - Motion cost associated with the best MV; computed as + * SAD+Lamda*BitsUsedByMV + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - any of the following pointers are NULL: + * pSrcOrgY, pSrcRefY, pRefRect, pMVPred, pMVCandidate, or pMESpec. + * - Either iBlockWidth or iBlockHeight are values other than 4, 8, or 16. + * - Any alignment restrictions are violated + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_BlockMatch_Integer ( + const OMX_U8 *pSrcOrgY, + OMX_S32 nSrcOrgStep, + const OMX_U8 *pSrcRefY, + OMX_S32 nSrcRefStep, + const OMXRect *pRefRect, + const OMXVCM4P2Coordinate *pCurrPointPos, + OMX_U8 iBlockWidth, + OMX_U8 iBlockHeight, + OMX_U32 nLamda, + const OMXVCMotionVector *pMVPred, + const OMXVCMotionVector *pMVCandidate, + OMXVCMotionVector *pBestMV, + OMX_S32 *pBestCost, + void *pMESpec +); + + + +/** + * Function: omxVCM4P10_BlockMatch_Half (6.3.5.2.2) + * + * Description: + * Performs a half-pel block match using results from a prior integer search. + * Returns the best MV and associated cost. This function estimates the + * half-pixel motion vector by interpolating the integer resolution motion + * vector referenced by the input parameter pSrcDstBestMV, i.e., the initial + * integer MV is generated externally. The function + * omxVCM4P10_BlockMatch_Integer may be used for integer motion estimation. + * + * Input Arguments: + * + * pSrcOrgY - Pointer to the current position in original picture plane: + * If iBlockWidth==4, 4-byte alignment required. + * If iBlockWidth==8, 8-byte alignment required. + * If iBlockWidth==16, 16-byte alignment required. + * pSrcRefY - Pointer to the top-left corner of the co-located block in the + * reference picture: + * If iBlockWidth==4, 4-byte alignment required. + * If iBlockWidth==8, 8-byte alignment required. + * If iBlockWidth==16, 16-byte alignment required. + * nSrcOrgStep - Stride of the original picture plane in terms of full + * pixels; must be a multiple of iBlockWidth. + * nSrcRefStep - Stride of the reference picture plane in terms of full + * pixels + * iBlockWidth - Width of the current block in terms of full pixels; must + * be equal to either 4, 8, or 16. + * iBlockHeight - Height of the current block in terms of full pixels; must + * be equal to either 4, 8, or 16. + * nLamda - Lamda factor, used to compute motion cost + * pMVPred - Predicted MV, represented in terms of 1/4-pel units; used to + * compute motion cost + * pSrcDstBestMV - The best MV resulting from a prior integer search, + * represented in terms of 1/4-pel units + * + * Output Arguments: + * + * pSrcDstBestMV - Best MV resulting from the half-pel search, expressed in + * terms of 1/4-pel units + * pBestCost - Motion cost associated with the best MV; computed as + * SAD+Lamda*BitsUsedByMV + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - any of the following pointers is NULL: pSrcOrgY, pSrcRefY, + * pSrcDstBestMV, pMVPred, pBestCost + * - iBlockWidth or iBlockHeight are equal to values other than 4, 8, or 16. + * - Any alignment restrictions are violated + * + */ + __attribute__((weak)) +OMXResult omxVCM4P10_BlockMatch_Half ( + const OMX_U8 *pSrcOrgY, + OMX_S32 nSrcOrgStep, + const OMX_U8 *pSrcRefY, + OMX_S32 nSrcRefStep, + OMX_U8 iBlockWidth, + OMX_U8 iBlockHeight, + OMX_U32 nLamda, + const OMXVCMotionVector *pMVPred, + OMXVCMotionVector *pSrcDstBestMV, + OMX_S32 *pBestCost +); + + + +/** + * Function: omxVCM4P10_BlockMatch_Quarter (6.3.5.2.3) + * + * Description: + * Performs a quarter-pel block match using results from a prior half-pel + * search. Returns the best MV and associated cost. This function estimates + * the quarter-pixel motion vector by interpolating the half-pel resolution + * motion vector referenced by the input parameter pSrcDstBestMV, i.e., the + * initial half-pel MV is generated externally. The function + * omxVCM4P10_BlockMatch_Half may be used for half-pel motion estimation. + * + * Input Arguments: + * + * pSrcOrgY - Pointer to the current position in original picture plane: + * If iBlockWidth==4, 4-byte alignment required. + * If iBlockWidth==8, 8-byte alignment required. + * If iBlockWidth==16, 16-byte alignment required. + * pSrcRefY - Pointer to the top-left corner of the co-located block in the + * reference picture: + * If iBlockWidth==4, 4-byte alignment required. + * If iBlockWidth==8, 8-byte alignment required. + * If iBlockWidth==16, 16-byte alignment required. + * nSrcOrgStep - Stride of the original picture plane in terms of full + * pixels; must be a multiple of iBlockWidth. + * nSrcRefStep - Stride of the reference picture plane in terms of full + * pixels + * iBlockWidth - Width of the current block in terms of full pixels; must + * be equal to either 4, 8, or 16. + * iBlockHeight - Height of the current block in terms of full pixels; must + * be equal to either 4, 8, or 16. + * nLamda - Lamda factor, used to compute motion cost + * pMVPred - Predicted MV, represented in terms of 1/4-pel units; used to + * compute motion cost + * pSrcDstBestMV - The best MV resulting from a prior half-pel search, + * represented in terms of 1/4 pel units + * + * Output Arguments: + * + * pSrcDstBestMV - Best MV resulting from the quarter-pel search, expressed + * in terms of 1/4-pel units + * pBestCost - Motion cost associated with the best MV; computed as + * SAD+Lamda*BitsUsedByMV + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - One or more of the following pointers is NULL: + * pSrcOrgY, pSrcRefY, pSrcDstBestMV, pMVPred, pBestCost + * - iBlockWidth or iBlockHeight are equal to values other than 4, 8, or 16. + * - Any alignment restrictions are violated + * + */ + __attribute__((weak)) +OMXResult omxVCM4P10_BlockMatch_Quarter ( + const OMX_U8 *pSrcOrgY, + OMX_S32 nSrcOrgStep, + const OMX_U8 *pSrcRefY, + OMX_S32 nSrcRefStep, + OMX_U8 iBlockWidth, + OMX_U8 iBlockHeight, + OMX_U32 nLamda, + const OMXVCMotionVector *pMVPred, + OMXVCMotionVector *pSrcDstBestMV, + OMX_S32 *pBestCost +); + + + +/** + * Function: omxVCM4P10_MotionEstimationMB (6.3.5.3.1) + * + * Description: + * Performs MB-level motion estimation and selects best motion estimation + * strategy from the set of modes supported in baseline profile [ISO14496-10]. + * + * Input Arguments: + * + * pSrcCurrBuf - Pointer to the current position in original picture plane; + * 16-byte alignment required + * pSrcRefBufList - Pointer to an array with 16 entries. Each entry points + * to the top-left corner of the co-located MB in a reference + * picture. The array is filled from low-to-high with valid + * reference frame pointers; the unused high entries should be set + * to NULL. Ordering of the reference frames should follow + * [ISO14496-10] subclause 8.2.4 Decoding Process for Reference + * Picture Lists. The entries must be 16-byte aligned. + * pSrcRecBuf - Pointer to the top-left corner of the co-located MB in the + * reconstructed picture; must be 16-byte aligned. + * SrcCurrStep - Width of the original picture plane in terms of full + * pixels; must be a multiple of 16. + * SrcRefStep - Width of the reference picture plane in terms of full + * pixels; must be a multiple of 16. + * SrcRecStep - Width of the reconstructed picture plane in terms of full + * pixels; must be a multiple of 16. + * pRefRect - Pointer to the valid reference rectangle; relative to the + * image origin. + * pCurrPointPos - Position of the current macroblock in the current plane. + * Lambda - Lagrange factor for computing the cost function + * pMESpec - Pointer to the motion estimation specification structure; must + * have been allocated and initialized prior to calling this + * function. + * pMBInter - Array, of dimension four, containing pointers to information + * associated with four adjacent type INTER MBs (Left, Top, + * Top-Left, Top-Right). Any pointer in the array may be set equal + * to NULL if the corresponding MB doesn t exist or is not of type + * INTER. + * - pMBInter[0] - Pointer to left MB information + * - pMBInter[1] - Pointer to top MB information + * - pMBInter[2] - Pointer to top-left MB information + * - pMBInter[3] - Pointer to top-right MB information + * pMBIntra - Array, of dimension four, containing pointers to information + * associated with four adjacent type INTRA MBs (Left, Top, + * Top-Left, Top-Right). Any pointer in the array may be set equal + * to NULL if the corresponding MB doesn t exist or is not of type + * INTRA. + * - pMBIntra[0] - Pointer to left MB information + * - pMBIntra[1] - Pointer to top MB information + * - pMBIntra[2] - Pointer to top-left MB information + * - pMBIntra[3] - Pointer to top-right MB information + * pSrcDstMBCurr - Pointer to information structure for the current MB. + * The following entries should be set prior to calling the + * function: sliceID - the number of the slice the to which the + * current MB belongs. + * + * Output Arguments: + * + * pDstCost - Pointer to the minimum motion cost for the current MB. + * pDstBlockSAD - Pointer to the array of SADs for each of the sixteen luma + * 4x4 blocks in each MB. The block SADs are in scan order for + * each MB. For implementations that cannot compute the SAD values + * individually, the maximum possible value (0xffff) is returned + * for each of the 16 block SAD entries. + * pSrcDstMBCurr - Pointer to updated information structure for the current + * MB after MB-level motion estimation has been completed. The + * following fields are updated by the ME function. The following + * parameter set quantifies the MB-level ME search results: + * - MbType + * - subMBType[4] + * - pMV0[4][4] + * - pMVPred[4][4] + * - pRefL0Idx[4] + * - Intra16x16PredMode + * - pIntra4x4PredMode[4][4] + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - One or more of the following pointers is NULL: pSrcCurrBuf, + * pSrcRefBufList, pSrcRecBuf, pRefRect, pCurrPointPos, pMESpec, + * pMBInter, pMBIntra,pSrcDstMBCurr, pDstCost, pSrcRefBufList[0] + * - SrcRefStep, SrcRecStep are not multiples of 16 + * - iBlockWidth or iBlockHeight are values other than 4, 8, or 16. + * - Any alignment restrictions are violated + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_MotionEstimationMB ( + const OMX_U8 *pSrcCurrBuf, + OMX_S32 SrcCurrStep, + const OMX_U8 *pSrcRefBufList[15], + OMX_S32 SrcRefStep, + const OMX_U8 *pSrcRecBuf, + OMX_S32 SrcRecStep, + const OMXRect *pRefRect, + const OMXVCM4P2Coordinate *pCurrPointPos, + OMX_U32 Lambda, + void *pMESpec, + const OMXVCM4P10MBInfoPtr *pMBInter, + const OMXVCM4P10MBInfoPtr *pMBIntra, + OMXVCM4P10MBInfoPtr pSrcDstMBCurr, + OMX_INT *pDstCost, + OMX_U16 *pDstBlockSAD +); + + + +/** + * Function: omxVCM4P10_SAD_4x (6.3.5.4.1) + * + * Description: + * This function calculates the SAD for 4x8 and 4x4 blocks. + * + * Input Arguments: + * + * pSrcOrg -Pointer to the original block; must be aligned on a 4-byte + * boundary. + * iStepOrg -Step of the original block buffer; must be a multiple of 4. + * pSrcRef -Pointer to the reference block + * iStepRef -Step of the reference block buffer + * iHeight -Height of the block; must be equal to either 4 or 8. + * + * Output Arguments: + * + * pDstSAD -Pointer of result SAD + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - One or more of the following pointers is NULL: + * pSrcOrg, pSrcRef, or pDstSAD + * - iHeight is not equal to either 4 or 8. + * - iStepOrg is not a multiple of 4 + * - Any alignment restrictions are violated + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_SAD_4x ( + const OMX_U8 *pSrcOrg, + OMX_U32 iStepOrg, + const OMX_U8 *pSrcRef, + OMX_U32 iStepRef, + OMX_S32 *pDstSAD, + OMX_U32 iHeight +); + + + +/** + * Function: omxVCM4P10_SADQuar_4x (6.3.5.4.2) + * + * Description: + * This function calculates the SAD between one block (pSrc) and the average + * of the other two (pSrcRef0 and pSrcRef1) for 4x8 or 4x4 blocks. Rounding + * is applied according to the convention (a+b+1)>>1. + * + * Input Arguments: + * + * pSrc - Pointer to the original block; must be aligned on a 4-byte + * boundary. + * pSrcRef0 - Pointer to reference block 0 + * pSrcRef1 - Pointer to reference block 1 + * iSrcStep - Step of the original block buffer; must be a multiple of 4. + * iRefStep0 - Step of reference block 0 + * iRefStep1 - Step of reference block 1 + * iHeight - Height of the block; must be equal to either 4 or 8. + * + * Output Arguments: + * + * pDstSAD - Pointer of result SAD + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - iHeight is not equal to either 4 or 8. + * - One or more of the following pointers is NULL: pSrc, pSrcRef0, + * pSrcRef1, pDstSAD. + * - iSrcStep is not a multiple of 4 + * - Any alignment restrictions are violated + * + */ + __attribute__((weak)) +OMXResult omxVCM4P10_SADQuar_4x ( + const OMX_U8 *pSrc, + const OMX_U8 *pSrcRef0, + const OMX_U8 *pSrcRef1, + OMX_U32 iSrcStep, + OMX_U32 iRefStep0, + OMX_U32 iRefStep1, + OMX_U32 *pDstSAD, + OMX_U32 iHeight +); + + + +/** + * Function: omxVCM4P10_SADQuar_8x (6.3.5.4.3) + * + * Description: + * This function calculates the SAD between one block (pSrc) and the average + * of the other two (pSrcRef0 and pSrcRef1) for 8x16, 8x8, or 8x4 blocks. + * Rounding is applied according to the convention (a+b+1)>>1. + * + * Input Arguments: + * + * pSrc - Pointer to the original block; must be aligned on an 8-byte + * boundary. + * pSrcRef0 - Pointer to reference block 0 + * pSrcRef1 - Pointer to reference block 1 + * iSrcStep - Step of the original block buffer; must be a multiple of 8. + * iRefStep0 - Step of reference block 0 + * iRefStep1 - Step of reference block 1 + * iHeight - Height of the block; must be equal either 4, 8, or 16. + * + * Output Arguments: + * + * pDstSAD - Pointer of result SAD + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - iHeight is not equal to either 4, 8, or 16. + * - One or more of the following pointers is NULL: pSrc, pSrcRef0, + * pSrcRef1, pDstSAD. + * - iSrcStep is not a multiple of 8 + * - Any alignment restrictions are violated + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_SADQuar_8x ( + const OMX_U8 *pSrc, + const OMX_U8 *pSrcRef0, + const OMX_U8 *pSrcRef1, + OMX_U32 iSrcStep, + OMX_U32 iRefStep0, + OMX_U32 iRefStep1, + OMX_U32 *pDstSAD, + OMX_U32 iHeight +); + + + +/** + * Function: omxVCM4P10_SADQuar_16x (6.3.5.4.4) + * + * Description: + * This function calculates the SAD between one block (pSrc) and the average + * of the other two (pSrcRef0 and pSrcRef1) for 16x16 or 16x8 blocks. + * Rounding is applied according to the convention (a+b+1)>>1. + * + * Input Arguments: + * + * pSrc - Pointer to the original block; must be aligned on a 16-byte + * boundary. + * pSrcRef0 - Pointer to reference block 0 + * pSrcRef1 - Pointer to reference block 1 + * iSrcStep - Step of the original block buffer; must be a multiple of 16 + * iRefStep0 - Step of reference block 0 + * iRefStep1 - Step of reference block 1 + * iHeight - Height of the block; must be equal to either 8 or 16 + * + * Output Arguments: + * + * pDstSAD -Pointer of result SAD + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - iHeight is not equal to either 8 or 16. + * - One or more of the following pointers is NULL: pSrc, pSrcRef0, + * pSrcRef1, pDstSAD. + * - iSrcStep is not a multiple of 16 + * - Any alignment restrictions are violated + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_SADQuar_16x ( + const OMX_U8 *pSrc, + const OMX_U8 *pSrcRef0, + const OMX_U8 *pSrcRef1, + OMX_U32 iSrcStep, + OMX_U32 iRefStep0, + OMX_U32 iRefStep1, + OMX_U32 *pDstSAD, + OMX_U32 iHeight +); + + + +/** + * Function: omxVCM4P10_SATD_4x4 (6.3.5.4.5) + * + * Description: + * This function calculates the sum of absolute transform differences (SATD) + * for a 4x4 block by applying a Hadamard transform to the difference block + * and then calculating the sum of absolute coefficient values. + * + * Input Arguments: + * + * pSrcOrg - Pointer to the original block; must be aligned on a 4-byte + * boundary + * iStepOrg - Step of the original block buffer; must be a multiple of 4 + * pSrcRef - Pointer to the reference block; must be aligned on a 4-byte + * boundary + * iStepRef - Step of the reference block buffer; must be a multiple of 4 + * + * Output Arguments: + * + * pDstSAD - pointer to the resulting SAD + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: + * pSrcOrg, pSrcRef, or pDstSAD either pSrcOrg + * - pSrcRef is not aligned on a 4-byte boundary + * - iStepOrg <= 0 or iStepOrg is not a multiple of 4 + * - iStepRef <= 0 or iStepRef is not a multiple of 4 + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_SATD_4x4 ( + const OMX_U8 *pSrcOrg, + OMX_U32 iStepOrg, + const OMX_U8 *pSrcRef, + OMX_U32 iStepRef, + OMX_U32 *pDstSAD +); + + + +/** + * Function: omxVCM4P10_InterpolateHalfHor_Luma (6.3.5.5.1) + * + * Description: + * This function performs interpolation for two horizontal 1/2-pel positions + * (-1/2,0) and (1/2, 0) - around a full-pel position. + * + * Input Arguments: + * + * pSrc - Pointer to the top-left corner of the block used to interpolate in + * the reconstruction frame plane. + * iSrcStep - Step of the source buffer. + * iDstStep - Step of the destination(interpolation) buffer; must be a + * multiple of iWidth. + * iWidth - Width of the current block; must be equal to either 4, 8, or 16 + * iHeight - Height of the current block; must be equal to 4, 8, or 16 + * + * Output Arguments: + * + * pDstLeft -Pointer to the interpolation buffer of the left -pel position + * (-1/2, 0) + * If iWidth==4, 4-byte alignment required. + * If iWidth==8, 8-byte alignment required. + * If iWidth==16, 16-byte alignment required. + * pDstRight -Pointer to the interpolation buffer of the right -pel + * position (1/2, 0) + * If iWidth==4, 4-byte alignment required. + * If iWidth==8, 8-byte alignment required. + * If iWidth==16, 16-byte alignment required. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: + * pSrc, pDstLeft, or pDstRight + * - iWidth or iHeight have values other than 4, 8, or 16 + * - iWidth==4 but pDstLeft and/or pDstRight is/are not aligned on a 4-byte boundary + * - iWidth==8 but pDstLeft and/or pDstRight is/are not aligned on a 8-byte boundary + * - iWidth==16 but pDstLeft and/or pDstRight is/are not aligned on a 16-byte boundary + * - any alignment restrictions are violated + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_InterpolateHalfHor_Luma ( + const OMX_U8 *pSrc, + OMX_U32 iSrcStep, + OMX_U8 *pDstLeft, + OMX_U8 *pDstRight, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight +); + + + +/** + * Function: omxVCM4P10_InterpolateHalfVer_Luma (6.3.5.5.2) + * + * Description: + * This function performs interpolation for two vertical 1/2-pel positions - + * (0, -1/2) and (0, 1/2) - around a full-pel position. + * + * Input Arguments: + * + * pSrc - Pointer to top-left corner of block used to interpolate in the + * reconstructed frame plane + * iSrcStep - Step of the source buffer. + * iDstStep - Step of the destination (interpolation) buffer; must be a + * multiple of iWidth. + * iWidth - Width of the current block; must be equal to either 4, 8, or 16 + * iHeight - Height of the current block; must be equal to either 4, 8, or 16 + * + * Output Arguments: + * + * pDstUp -Pointer to the interpolation buffer of the -pel position above + * the current full-pel position (0, -1/2) + * If iWidth==4, 4-byte alignment required. + * If iWidth==8, 8-byte alignment required. + * If iWidth==16, 16-byte alignment required. + * pDstDown -Pointer to the interpolation buffer of the -pel position below + * the current full-pel position (0, 1/2) + * If iWidth==4, 4-byte alignment required. + * If iWidth==8, 8-byte alignment required. + * If iWidth==16, 16-byte alignment required. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: + * pSrc, pDstUp, or pDstDown + * - iWidth or iHeight have values other than 4, 8, or 16 + * - iWidth==4 but pDstUp and/or pDstDown is/are not aligned on a 4-byte boundary + * - iWidth==8 but pDstUp and/or pDstDown is/are not aligned on a 8-byte boundary + * - iWidth==16 but pDstUp and/or pDstDown is/are not aligned on a 16-byte boundary + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_InterpolateHalfVer_Luma ( + const OMX_U8 *pSrc, + OMX_U32 iSrcStep, + OMX_U8 *pDstUp, + OMX_U8 *pDstDown, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight +); + + + +/** + * Function: omxVCM4P10_Average_4x (6.3.5.5.3) + * + * Description: + * This function calculates the average of two 4x4, 4x8 blocks. The result + * is rounded according to (a+b+1)/2. + * + * Input Arguments: + * + * pPred0 - Pointer to the top-left corner of reference block 0 + * pPred1 - Pointer to the top-left corner of reference block 1 + * iPredStep0 - Step of reference block 0; must be a multiple of 4. + * iPredStep1 - Step of reference block 1; must be a multiple of 4. + * iDstStep - Step of the destination buffer; must be a multiple of 4. + * iHeight - Height of the blocks; must be either 4 or 8. + * + * Output Arguments: + * + * pDstPred - Pointer to the destination buffer. 4-byte alignment required. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: + * pPred0, pPred1, or pDstPred + * - pDstPred is not aligned on a 4-byte boundary + * - iPredStep0 <= 0 or iPredStep0 is not a multiple of 4 + * - iPredStep1 <= 0 or iPredStep1 is not a multiple of 4 + * - iDstStep <= 0 or iDstStep is not a multiple of 4 + * - iHeight is not equal to either 4 or 8 + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_Average_4x ( + const OMX_U8 *pPred0, + const OMX_U8 *pPred1, + OMX_U32 iPredStep0, + OMX_U32 iPredStep1, + OMX_U8 *pDstPred, + OMX_U32 iDstStep, + OMX_U32 iHeight +); + + + +/** + * Function: omxVCM4P10_TransformQuant_ChromaDC (6.3.5.6.1) + * + * Description: + * This function performs 2x2 Hadamard transform of chroma DC coefficients + * and then quantizes the coefficients. + * + * Input Arguments: + * + * pSrcDst - Pointer to the 2x2 array of chroma DC coefficients. 8-byte + * alignment required. + * iQP - Quantization parameter; must be in the range [0,51]. + * bIntra - Indicate whether this is an INTRA block. 1-INTRA, 0-INTER + * + * Output Arguments: + * + * pSrcDst - Pointer to transformed and quantized coefficients. 8-byte + * alignment required. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: + * pSrcDst + * - pSrcDst is not aligned on an 8-byte boundary + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_TransformQuant_ChromaDC ( + OMX_S16 *pSrcDst, + OMX_U32 iQP, + OMX_U8 bIntra +); + + + +/** + * Function: omxVCM4P10_TransformQuant_LumaDC (6.3.5.6.2) + * + * Description: + * This function performs a 4x4 Hadamard transform of luma DC coefficients + * and then quantizes the coefficients. + * + * Input Arguments: + * + * pSrcDst - Pointer to the 4x4 array of luma DC coefficients. 16-byte + * alignment required. + * iQP - Quantization parameter; must be in the range [0,51]. + * + * Output Arguments: + * + * pSrcDst - Pointer to transformed and quantized coefficients. 16-byte + * alignment required. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: pSrcDst + * - pSrcDst is not aligned on an 16-byte boundary + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_TransformQuant_LumaDC ( + OMX_S16 *pSrcDst, + OMX_U32 iQP +); + + + +/** + * Function: omxVCM4P10_InvTransformDequant_LumaDC (6.3.5.6.3) + * + * Description: + * This function performs inverse 4x4 Hadamard transform and then dequantizes + * the coefficients. + * + * Input Arguments: + * + * pSrc - Pointer to the 4x4 array of the 4x4 Hadamard-transformed and + * quantized coefficients. 16 byte alignment required. + * iQP - Quantization parameter; must be in the range [0,51]. + * + * Output Arguments: + * + * pDst - Pointer to inverse-transformed and dequantized coefficients. + * 16-byte alignment required. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: pSrc + * - pSrc or pDst is not aligned on a 16-byte boundary + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_InvTransformDequant_LumaDC ( + const OMX_S16 *pSrc, + OMX_S16 *pDst, + OMX_U32 iQP +); + + + +/** + * Function: omxVCM4P10_InvTransformDequant_ChromaDC (6.3.5.6.4) + * + * Description: + * This function performs inverse 2x2 Hadamard transform and then dequantizes + * the coefficients. + * + * Input Arguments: + * + * pSrc - Pointer to the 2x2 array of the 2x2 Hadamard-transformed and + * quantized coefficients. 8 byte alignment required. + * iQP - Quantization parameter; must be in the range [0,51]. + * + * Output Arguments: + * + * pDst - Pointer to inverse-transformed and dequantized coefficients. + * 8-byte alignment required. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: pSrc + * - pSrc or pDst is not aligned on an 8-byte boundary + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_InvTransformDequant_ChromaDC ( + const OMX_S16 *pSrc, + OMX_S16 *pDst, + OMX_U32 iQP +); + + + +/** + * Function: omxVCM4P10_InvTransformResidualAndAdd (6.3.5.7.1) + * + * Description: + * This function performs inverse an 4x4 integer transformation to produce + * the difference signal and then adds the difference to the prediction to get + * the reconstructed signal. + * + * Input Arguments: + * + * pSrcPred - Pointer to prediction signal. 4-byte alignment required. + * pDequantCoeff - Pointer to the transformed coefficients. 8-byte + * alignment required. + * iSrcPredStep - Step of the prediction buffer; must be a multiple of 4. + * iDstReconStep - Step of the destination reconstruction buffer; must be a + * multiple of 4. + * bAC - Indicate whether there is AC coefficients in the coefficients + * matrix. + * + * Output Arguments: + * + * pDstRecon -Pointer to the destination reconstruction buffer. 4-byte + * alignment required. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: + * pSrcPred, pDequantCoeff, pDstRecon + * - pSrcPred is not aligned on a 4-byte boundary + * - iSrcPredStep or iDstReconStep is not a multiple of 4. + * - pDequantCoeff is not aligned on an 8-byte boundary + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_InvTransformResidualAndAdd ( + const OMX_U8 *pSrcPred, + const OMX_S16 *pDequantCoeff, + OMX_U8 *pDstRecon, + OMX_U32 iSrcPredStep, + OMX_U32 iDstReconStep, + OMX_U8 bAC +); + + + +/** + * Function: omxVCM4P10_SubAndTransformQDQResidual (6.3.5.8.1) + * + * Description: + * This function subtracts the prediction signal from the original signal to + * produce the difference signal and then performs a 4x4 integer transform and + * quantization. The quantized transformed coefficients are stored as + * pDstQuantCoeff. This function can also output dequantized coefficients or + * unquantized DC coefficients optionally by setting the pointers + * pDstDeQuantCoeff, pDCCoeff. + * + * Input Arguments: + * + * pSrcOrg - Pointer to original signal. 4-byte alignment required. + * pSrcPred - Pointer to prediction signal. 4-byte alignment required. + * iSrcOrgStep - Step of the original signal buffer; must be a multiple of + * 4. + * iSrcPredStep - Step of the prediction signal buffer; must be a multiple + * of 4. + * pNumCoeff -Number of non-zero coefficients after quantization. If this + * parameter is not required, it is set to NULL. + * nThreshSAD - Zero-block early detection threshold. If this parameter is + * not required, it is set to 0. + * iQP - Quantization parameter; must be in the range [0,51]. + * bIntra - Indicates whether this is an INTRA block, either 1-INTRA or + * 0-INTER + * + * Output Arguments: + * + * pDstQuantCoeff - Pointer to the quantized transformed coefficients. + * 8-byte alignment required. + * pDstDeQuantCoeff - Pointer to the dequantized transformed coefficients + * if this parameter is not equal to NULL. 8-byte alignment + * required. + * pDCCoeff - Pointer to the unquantized DC coefficient if this parameter + * is not equal to NULL. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: + * pSrcOrg, pSrcPred, pNumCoeff, pDstQuantCoeff, + * pDstDeQuantCoeff, pDCCoeff + * - pSrcOrg is not aligned on a 4-byte boundary + * - pSrcPred is not aligned on a 4-byte boundary + * - iSrcOrgStep is not a multiple of 4 + * - iSrcPredStep is not a multiple of 4 + * - pDstQuantCoeff or pDstDeQuantCoeff is not aligned on an 8-byte boundary + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_SubAndTransformQDQResidual ( + const OMX_U8 *pSrcOrg, + const OMX_U8 *pSrcPred, + OMX_U32 iSrcOrgStep, + OMX_U32 iSrcPredStep, + OMX_S16 *pDstQuantCoeff, + OMX_S16 *pDstDeQuantCoeff, + OMX_S16 *pDCCoeff, + OMX_S8 *pNumCoeff, + OMX_U32 nThreshSAD, + OMX_U32 iQP, + OMX_U8 bIntra +); + + + +/** + * Function: omxVCM4P10_GetVLCInfo (6.3.5.9.1) + * + * Description: + * This function extracts run-length encoding (RLE) information from the + * coefficient matrix. The results are returned in an OMXVCM4P10VLCInfo + * structure. + * + * Input Arguments: + * + * pSrcCoeff - pointer to the transform coefficient matrix. 8-byte + * alignment required. + * pScanMatrix - pointer to the scan order definition matrix. For a luma + * block the scan matrix should follow [ISO14496-10] section 8.5.4, + * and should contain the values 0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, + * 10, 7, 11, 14, 15. For a chroma block, the scan matrix should + * contain the values 0, 1, 2, 3. + * bAC - indicates presence of a DC coefficient; 0 = DC coefficient + * present, 1= DC coefficient absent. + * MaxNumCoef - specifies the number of coefficients contained in the + * transform coefficient matrix, pSrcCoeff. The value should be 16 + * for blocks of type LUMADC, LUMAAC, LUMALEVEL, and CHROMAAC. The + * value should be 4 for blocks of type CHROMADC. + * + * Output Arguments: + * + * pDstVLCInfo - pointer to structure that stores information for + * run-length coding. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: + * pSrcCoeff, pScanMatrix, pDstVLCInfo + * - pSrcCoeff is not aligned on an 8-byte boundary + * + */ + __attribute__((weak)) + OMXResult omxVCM4P10_GetVLCInfo ( + const OMX_S16 *pSrcCoeff, + const OMX_U8 *pScanMatrix, + OMX_U8 bAC, + OMX_U32 MaxNumCoef, + OMXVCM4P10VLCInfo*pDstVLCInfo +); + + + +#ifdef __cplusplus +} +#endif + +#endif /** end of #define _OMXVC_H_ */ + +/** EOF */ + diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_DeblockChroma_I.c b/src/video/h264_decoder/omxdl/omxVCM4P10_DeblockChroma_I.c new file mode 100644 index 000000000..a07b1bb07 --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_DeblockChroma_I.c @@ -0,0 +1,107 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: omxVCM4P10_DeblockChroma_I.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 intra chroma deblock + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +/** + * Function: omxVCM4P10_DeblockChroma_I (6.3.3.3.6) + * + * Description: + * Performs in-place deblocking filtering on all edges of the chroma + * macroblock (16x16). + * + * Input Arguments: + * + * pSrcDst - pointer to the input macroblock; must be 8-byte aligned. + * srcdstStep - step of the arrays; must be a multiple of 8. + * pAlpha - pointer to a 2x2 array of alpha thresholds, organized as + * follows: {external vertical edge, internal vertical edge, + * external horizontal edge, internal horizontal edge }. Per + * [ISO14496-10] alpha values must be in the range [0,255]. + * pBeta - pointer to a 2x2 array of Beta Thresholds, organized as follows: + * { external vertical edge, internal vertical edge, external + * horizontal edge, internal horizontal edge }. Per [ISO14496-10] + * beta values must be in the range [0,18]. + * pThresholds - array of size 8x2 of Thresholds (TC0) (values for the left + * or above edge of each 4x2 or 2x4 block, arranged in vertical + * block order and then in horizontal block order); must be aligned + * on a 4-byte boundary. Per [ISO14496-10] values must be in the + * range [0,25]. + * pBS - array of size 16x2 of BS parameters (arranged in scan block order + * for vertical edges and then horizontal edges); valid in the + * range [0,4] with the following restrictions: i) pBS[i]== 4 may + * occur only for 0<=i<=3, ii) pBS[i]== 4 if and only if pBS[i^3]== + * 4. Must be 4-byte aligned. + * + * Output Arguments: + * + * pSrcDst - pointer to filtered output macroblock. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments + * - one or more of the following pointers is NULL: pSrcDst, pAlpha, + * pBeta, pThresholds, or pBS. pSrcDst is not 8-byte aligned. + * either pThresholds or pBS is not 4-byte aligned. + * - one or more entries in the table pAlpha[0..3] is outside the range + * [0,255]. + * - one or more entries in the table pBeta[0..3] is outside the range + * [0,18]. + * - one or more entries in the table pThresholds[0..15]is outside of + * the range [0,25]. + * - pBS is out of range, i.e., one of the following conditions is true: + * pBS[i]<0, pBS[i]>4, pBS[i]==4 for i>=4, or + * (pBS[i]==4 && pBS[i^3]!=4) for 0<=i<=3. + * - srcdstStep is not a multiple of 8. + * + */ +OMXResult omxVCM4P10_DeblockChroma_I( + OMX_U8* pSrcDst, + OMX_S32 srcdstStep, + const OMX_U8* pAlpha, + const OMX_U8* pBeta, + const OMX_U8* pThresholds, + const OMX_U8 *pBS +) +{ + OMXResult errorCode; + + armRetArgErrIf(pSrcDst == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot8ByteAligned(pSrcDst), OMX_Sts_BadArgErr); + armRetArgErrIf(srcdstStep & 7, OMX_Sts_BadArgErr); + armRetArgErrIf(pAlpha == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(pBeta == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(pThresholds == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pThresholds), OMX_Sts_BadArgErr); + armRetArgErrIf(pBS == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pBS), OMX_Sts_BadArgErr); + + errorCode = omxVCM4P10_FilterDeblockingChroma_VerEdge_I( + pSrcDst, srcdstStep, pAlpha, pBeta, pThresholds, pBS); + + armRetArgErrIf(errorCode != OMX_Sts_NoErr, errorCode) + + errorCode = omxVCM4P10_FilterDeblockingChroma_HorEdge_I( + pSrcDst, srcdstStep, pAlpha+2, pBeta+2, pThresholds+8, pBS+16); + + return errorCode; +} diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_DeblockLuma_I.c b/src/video/h264_decoder/omxdl/omxVCM4P10_DeblockLuma_I.c new file mode 100644 index 000000000..1f3a64648 --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_DeblockLuma_I.c @@ -0,0 +1,109 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: omxVCM4P10_DeblockLuma_I.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 luma deblock + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + + +/** + * Function: omxVCM4P10_DeblockLuma_I (6.3.3.3.5) + * + * Description: + * This function performs in-place deblock filtering the horizontal and + * vertical edges of a luma macroblock (16x16). + * + * Input Arguments: + * + * pSrcDst - pointer to the input macroblock; must be 16-byte aligned. + * srcdstStep - image width; must be a multiple of 16. + * pAlpha - pointer to a 2x2 table of alpha thresholds, organized as + * follows: {external vertical edge, internal vertical edge, + * external horizontal edge, internal horizontal edge }. Per + * [ISO14496-10] alpha values must be in the range [0,255]. + * pBeta - pointer to a 2x2 table of beta thresholds, organized as follows: + * {external vertical edge, internal vertical edge, external + * horizontal edge, internal horizontal edge }. Per [ISO14496-10] + * beta values must be in the range [0,18]. + * pThresholds - pointer to a 16x2 table of threshold (TC0), organized as + * follows: {values for the left or above edge of each 4x4 block, + * arranged in vertical block order and then in horizontal block + * order}; must be aligned on a 4-byte boundary. Per [ISO14496-10] + * values must be in the range [0,25]. + * pBS - pointer to a 16x2 table of BS parameters arranged in scan block + * order for vertical edges and then horizontal edges; valid in the + * range [0,4] with the following restrictions: i) pBS[i]== 4 may + * occur only for 0<=i<=3, ii) pBS[i]== 4 if and only if pBS[i^3]== + * 4. Must be 4-byte aligned. + * + * Output Arguments: + * + * pSrcDst - pointer to filtered output macroblock. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments + * - one or more of the following pointers is NULL: pSrcDst, pAlpha, + * pBeta, pThresholds or pBS. pSrcDst is not 16-byte aligned. + * either pThresholds or pBS is not aligned on a 4-byte boundary. + * - one or more entries in the table pAlpha[0..3] is outside the range + * [0,255]. + * - one or more entries in the table pBeta[0..3] is outside the range + * [0,18]. + * - one or more entries in the table pThresholds[0..31]is outside of + * the range [0,25]. + * - pBS is out of range, i.e., one of the following conditions is true: + * pBS[i]<0, pBS[i]>4, pBS[i]==4 for i>=4, or + * (pBS[i]==4 && pBS[i^3]!=4) for 0<=i<=3. + * - srcdstStep is not a multiple of 16. + * + */ + +OMXResult omxVCM4P10_DeblockLuma_I( + OMX_U8* pSrcDst, + OMX_S32 srcdstStep, + const OMX_U8* pAlpha, + const OMX_U8* pBeta, + const OMX_U8* pThresholds, + const OMX_U8 *pBS +) +{ + OMXResult errorCode; + + armRetArgErrIf(pSrcDst == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot16ByteAligned(pSrcDst), OMX_Sts_BadArgErr); + armRetArgErrIf(srcdstStep & 15, OMX_Sts_BadArgErr); + armRetArgErrIf(pAlpha == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(pBeta == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(pThresholds == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pThresholds), OMX_Sts_BadArgErr); + armRetArgErrIf(pBS == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pBS), OMX_Sts_BadArgErr); + + errorCode = omxVCM4P10_FilterDeblockingLuma_VerEdge_I( + pSrcDst, srcdstStep, pAlpha, pBeta, pThresholds, pBS); + + armRetArgErrIf(errorCode != OMX_Sts_NoErr, errorCode) + + errorCode = omxVCM4P10_FilterDeblockingLuma_HorEdge_I( + pSrcDst, srcdstStep, pAlpha+2, pBeta+2, pThresholds+16, pBS+16); + + return errorCode; +} diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_DecodeChromaDcCoeffsToPairCAVLC.c b/src/video/h264_decoder/omxdl/omxVCM4P10_DecodeChromaDcCoeffsToPairCAVLC.c new file mode 100644 index 000000000..830ddc7a6 --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_DecodeChromaDcCoeffsToPairCAVLC.c @@ -0,0 +1,86 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: omxVCM4P10_DecodeChromaDcCoeffsToPairCAVLC.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 decode coefficients module + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +/** + * Function: omxVCM4P10_DecodeChromaDcCoeffsToPairCAVLC (6.3.4.1.1) + * + * Description: + * Performs CAVLC decoding and inverse raster scan for a 2x2 block of + * ChromaDCLevel. The decoded coefficients in the packed position-coefficient + * buffer are stored in reverse zig-zag order, i.e., the first buffer element + * contains the last non-zero postion-coefficient pair of the block. Within + * each position-coefficient pair, the position entry indicates the + * raster-scan position of the coefficient, while the coefficient entry + * contains the coefficient value. + * + * Input Arguments: + * + * ppBitStream - Double pointer to current byte in bit stream buffer + * pOffset - Pointer to current bit position in the byte pointed to by + * *ppBitStream; valid in the range [0,7]. + * + * Output Arguments: + * + * ppBitStream - *ppBitStream is updated after each block is decoded + * pOffset - *pOffset is updated after each block is decoded + * pNumCoeff - Pointer to the number of nonzero coefficients in this block + * ppPosCoefBuf - Double pointer to destination residual + * coefficient-position pair buffer. Buffer position + * (*ppPosCoefBuf) is updated upon return, unless there are only + * zero coefficients in the currently decoded block. In this case + * the caller is expected to bypass the transform/dequantization of + * the empty blocks. + * + * Return Value: + * + * OMX_Sts_NoErr, if the function runs without error. + * + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - ppBitStream or pOffset is NULL. + * - ppPosCoefBuf or pNumCoeff is NULL. + * OMX_Sts_Err - if one of the following is true: + * - an illegal code is encountered in the bitstream + * + */ + +OMXResult omxVCM4P10_DecodeChromaDcCoeffsToPairCAVLC ( + const OMX_U8** ppBitStream, + OMX_S32* pOffset, + OMX_U8* pNumCoeff, + OMX_U8** ppPosCoefbuf + ) + +{ + armRetArgErrIf(ppBitStream==NULL , OMX_Sts_BadArgErr); + armRetArgErrIf(*ppBitStream==NULL , OMX_Sts_BadArgErr); + armRetArgErrIf(pOffset==NULL , OMX_Sts_BadArgErr); + armRetArgErrIf(*pOffset<0 , OMX_Sts_BadArgErr); + armRetArgErrIf(*pOffset>7 , OMX_Sts_BadArgErr); + armRetArgErrIf(pNumCoeff==NULL , OMX_Sts_BadArgErr); + armRetArgErrIf(ppPosCoefbuf==NULL , OMX_Sts_BadArgErr); + armRetArgErrIf(*ppPosCoefbuf==NULL , OMX_Sts_BadArgErr); + + return armVCM4P10_DecodeCoeffsToPair(ppBitStream, pOffset, pNumCoeff, + ppPosCoefbuf, 4, 4); + +} diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_DecodeCoeffsToPairCAVLC.c b/src/video/h264_decoder/omxdl/omxVCM4P10_DecodeCoeffsToPairCAVLC.c new file mode 100644 index 000000000..7e83d1e31 --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_DecodeCoeffsToPairCAVLC.c @@ -0,0 +1,117 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: omxVCM4P10_DecodeCoeffsToPairCAVLC.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 decode coefficients module + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" +#include "armCOMM.h" +#include "armVC.h" + +/** + * Function: omxVCM4P10_DecodeCoeffsToPairCAVLC (6.3.4.1.2) + * + * Description: + * Performs CAVLC decoding and inverse zigzag scan for 4x4 block of + * Intra16x16DCLevel, Intra16x16ACLevel, LumaLevel, and ChromaACLevel. Inverse + * field scan is not supported. The decoded coefficients in the packed + * position-coefficient buffer are stored in reverse zig-zag order, i.e., the + * first buffer element contains the last non-zero postion-coefficient pair of + * the block. Within each position-coefficient pair, the position entry + * indicates the raster-scan position of the coefficient, while the + * coefficient entry contains the coefficient value. + * + * Input Arguments: + * + * ppBitStream -Double pointer to current byte in bit stream buffer + * pOffset - Pointer to current bit position in the byte pointed to by + * *ppBitStream; valid in the range [0,7]. + * sMaxNumCoeff - Maximum the number of non-zero coefficients in current + * block + * sVLCSelect - VLC table selector, obtained from the number of non-zero + * coefficients contained in the above and left 4x4 blocks. It is + * equivalent to the variable nC described in H.264 standard table + * 9 5, except its value can t be less than zero. + * + * Output Arguments: + * + * ppBitStream - *ppBitStream is updated after each block is decoded. + * Buffer position (*ppPosCoefBuf) is updated upon return, unless + * there are only zero coefficients in the currently decoded block. + * In this case the caller is expected to bypass the + * transform/dequantization of the empty blocks. + * pOffset - *pOffset is updated after each block is decoded + * pNumCoeff - Pointer to the number of nonzero coefficients in this block + * ppPosCoefBuf - Double pointer to destination residual + * coefficient-position pair buffer + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - ppBitStream or pOffset is NULL. + * - ppPosCoefBuf or pNumCoeff is NULL. + * - sMaxNumCoeff is not equal to either 15 or 16. + * - sVLCSelect is less than 0. + * + * OMX_Sts_Err - if one of the following is true: + * - an illegal code is encountered in the bitstream + * + */ + +OMXResult omxVCM4P10_DecodeCoeffsToPairCAVLC( + const OMX_U8** ppBitStream, + OMX_S32* pOffset, + OMX_U8* pNumCoeff, + OMX_U8**ppPosCoefbuf, + OMX_INT sVLCSelect, + OMX_INT sMaxNumCoeff + ) +{ + int nTable; + + armRetArgErrIf(ppBitStream==NULL , OMX_Sts_BadArgErr); + armRetArgErrIf(*ppBitStream==NULL , OMX_Sts_BadArgErr); + armRetArgErrIf(pOffset==NULL , OMX_Sts_BadArgErr); + armRetArgErrIf(*pOffset<0 , OMX_Sts_BadArgErr); + armRetArgErrIf(*pOffset>7 , OMX_Sts_BadArgErr); + armRetArgErrIf(pNumCoeff==NULL , OMX_Sts_BadArgErr); + armRetArgErrIf(ppPosCoefbuf==NULL , OMX_Sts_BadArgErr); + armRetArgErrIf(*ppPosCoefbuf==NULL , OMX_Sts_BadArgErr); + armRetArgErrIf(sVLCSelect<0 , OMX_Sts_BadArgErr); + armRetArgErrIf(sMaxNumCoeff<15 , OMX_Sts_BadArgErr); + armRetArgErrIf(sMaxNumCoeff>16 , OMX_Sts_BadArgErr); + + /* Find VLC table number */ + if (sVLCSelect<2) + { + nTable = 0; + } + else if (sVLCSelect<4) + { + nTable = 1; + } + else if (sVLCSelect<8) + { + nTable = 2; + } + else /* sVLCSelect >= 8 */ + { + nTable = 3; + } + + return armVCM4P10_DecodeCoeffsToPair(ppBitStream, pOffset, pNumCoeff, + ppPosCoefbuf, nTable, sMaxNumCoeff); +} diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_DequantTransformResidualFromPairAndAdd.c b/src/video/h264_decoder/omxdl/omxVCM4P10_DequantTransformResidualFromPairAndAdd.c new file mode 100644 index 000000000..ba7da8a5b --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_DequantTransformResidualFromPairAndAdd.c @@ -0,0 +1,541 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: omxVCM4P10_DequantTransformResidualFromPairAndAdd.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 inverse quantize and transform module + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +#ifdef H264BSD_N64 +#include +#include "../../fastcache.h" +#include "../../rsph264_internal.h" +#include "../h264bsd_macroblock_layer.h" +#endif + +/* + * Description: + * Dequantize Luma AC block + */ +static void DequantLumaAC4x4( + OMX_S16* pSrcDst, + OMX_INT QP +) +{ + const OMX_U8 *pVRow = &armVCM4P10_VMatrix[QP%6][0]; + int Shift = QP / 6; + int i; + OMX_S32 Value; + + for (i=0; i<16; i++) + { + + Value = (pSrcDst[i] * pVRow[armVCM4P10_PosToVCol4x4[i]]) << Shift; + pSrcDst[i] = (OMX_S16)Value; + } +} + +/** + * Function: omxVCM4P10_DequantTransformResidualFromPairAndAdd (6.3.4.2.3) + * + * Description: + * Reconstruct the 4x4 residual block from coefficient-position pair buffer, + * perform dequantization and integer inverse transformation for 4x4 block of + * residuals with previous intra prediction or motion compensation data, and + * update the pair buffer pointer to next non-empty block. If pDC == NULL, + * there re 16 non-zero AC coefficients at most in the packed buffer starting + * from 4x4 block position 0; If pDC != NULL, there re 15 non-zero AC + * coefficients at most in the packet buffer starting from 4x4 block position + * 1. + * + * Input Arguments: + * + * ppSrc - Double pointer to residual coefficient-position pair buffer + * output by CALVC decoding + * pPred - Pointer to the predicted 4x4 block; must be aligned on a 4-byte + * boundary + * predStep - Predicted frame step size in bytes; must be a multiple of 4 + * dstStep - Destination frame step in bytes; must be a multiple of 4 + * pDC - Pointer to the DC coefficient of this block, NULL if it doesn't + * exist + * QP - QP Quantization parameter. It should be QpC in chroma 4x4 block + * decoding, otherwise it should be QpY. + * AC - Flag indicating if at least one non-zero AC coefficient exists + * + * Output Arguments: + * + * pDst - pointer to the reconstructed 4x4 block data; must be aligned on a + * 4-byte boundary + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - pPred or pDst is NULL. + * - pPred or pDst is not 4-byte aligned. + * - predStep or dstStep is not a multiple of 4. + * - AC !=0 and Qp is not in the range of [0-51] or ppSrc == NULL. + * - AC ==0 && pDC ==NULL. + * + */ + +OMXResult omxVCM4P10_DequantTransformResidualFromPairAndAdd( + const OMX_U8 **ppSrc, + const OMX_U8 *pPred, + const OMX_S16 *pDC, + OMX_U8 *pDst, + OMX_INT predStep, + OMX_INT dstStep, + OMX_INT QP, + OMX_INT AC +) +{ +#ifdef H264BSD_N64 + (void)DequantLumaAC4x4; + assert(pPred == pDst); + assert(predStep == dstStep); + + #if 1 // Alternate implementation to compare to reference implementation + rsph264_queue_set_packed_delta_buffer_if_changed(0, *ppSrc); + rsph264_queue_dequant_transform_residual(RSPH264_CACHE_SKIP_ALL, + pDst, dstStep, pDC, QP, AC); + // rsph264_sync(); + + #else + + static uint8_t tmpSrc[8*8]; + static uint8_t tmpDest[8*8]; + + for (int y=0; y<4; y++) { + for (int x=0; x<4; x++) { + tmpSrc[y*8+x] = tmpDest[y*8+x] = pPred[y*predStep+x]; + } + } + fast_data_cache_hit_writeback_invalidate(tmpDest, sizeof(tmpDest)); + + rsph264_queue_set_packed_delta_buffer_if_changed(0, *ppSrc); + rsph264_queue_dequant_transform_residual(0, + tmpDest, 8, pDC, QP, AC); + rsph264_sync(); + + const uint8_t *rspPPSrc = rsph264_cur_delta_buffer(*ppSrc); + const uint8_t *origSrc = *ppSrc; + printf("Dequant: deltaptr: %lx\n", (uint32_t)rspPPSrc); + + + // REFERENCE IMPLEMENTATION + // ************************ + + OMX_S16 pBuffer[16+4]; + OMX_S16 *pDelta; + int i,x,y; + + armRetArgErrIf(pPred == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pPred),OMX_Sts_BadArgErr); + armRetArgErrIf(pDst == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pDst), OMX_Sts_BadArgErr); + armRetArgErrIf(predStep & 3, OMX_Sts_BadArgErr); + armRetArgErrIf(dstStep & 3, OMX_Sts_BadArgErr); + armRetArgErrIf(AC!=0 && (QP<0), OMX_Sts_BadArgErr); + armRetArgErrIf(AC!=0 && (QP>51), OMX_Sts_BadArgErr); + armRetArgErrIf(AC!=0 && ppSrc==NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(AC!=0 && *ppSrc==NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(AC==0 && pDC==NULL, OMX_Sts_BadArgErr); + + pDelta = armAlignTo8Bytes(pBuffer); + + for (i=0; i<16; i++) + { + pDelta[i] = 0; + } + if (AC) + { + armVCM4P10_UnpackBlock4x4(ppSrc, pDelta); + DequantLumaAC4x4(pDelta, QP); + } + if (pDC) + { + pDelta[0] = pDC[0]; + } + armVCM4P10_TransformResidual4x4(pDelta,pDelta); + + for (y=0; y<4; y++) + { + for (x=0; x<4; x++) + { + pDst[y*dstStep+x] = (OMX_U8)armClip(0,255,pPred[y*predStep+x] + pDelta[4*y+x]); + } + } + + // Find differences + for (y=0; y<4; y++) + { + for (x=0; x<4; x++) + { + if (tmpDest[y*8+x] != pDst[y*dstStep+x]) { + printf("DEQUANT COMPARISON FAILED:\n"); + printf("QP=%d/%d AC=%d\n", QP/6, QP%6, AC); + if (pDC != NULL) + printf("DC=[%x,%x,%x,%x],[%x,%x,%x,%x]", pDC[0],pDC[1],pDC[2],pDC[3],pDC[4],pDC[5],pDC[6],pDC[7]); + printf("(%d,%d): RSP=%02x REF=%02x\n", x, y, tmpDest[y*8+x], pPred[y*dstStep+x]); + printf("SRC:\n"); + for (int y=0;y<4;y++) { + uint8_t *p = &tmpSrc[y*8]; + printf("%02x %02x %02x %02x\n", p[0], p[1], p[2], p[3]); + } + printf("RSP:\n"); + for (int y=0;y<4;y++) { + uint8_t *p = &tmpDest[y*8]; + printf("%02x %02x %02x %02x\n", p[0], p[1], p[2], p[3]); + } + printf("REF:\n"); + for (int y=0;y<4;y++) { + const uint8_t *p = &pPred[y*predStep]; + printf("%02x %02x %02x %02x\n", p[0], p[1], p[2], p[3]); + } + printf("SRC: "); + for (const uint8_t *p = origSrc; p != *ppSrc; p++) + printf("%02x ", *p); + printf("\n"); + while(1) {} + } + } + } + (void)rspPPSrc; + if (rspPPSrc != *ppSrc) { + printf("SRC: orig:%lx ref:%lx rsp:%lx AC:%d\n", (uint32_t)origSrc, (uint32_t)*ppSrc, (uint32_t)rspPPSrc, AC); + while(1) {} + } + + #endif +#else // H264BSD_N64 + OMX_S16 pBuffer[16+4]; + OMX_S16 *pDelta; + int i,x,y; + + #ifdef H264BSD_N64 + *ppSrc = rsph264_cur_delta_buffer(*ppSrc); + #endif + + armRetArgErrIf(pPred == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pPred),OMX_Sts_BadArgErr); + armRetArgErrIf(pDst == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pDst), OMX_Sts_BadArgErr); + armRetArgErrIf(predStep & 3, OMX_Sts_BadArgErr); + armRetArgErrIf(dstStep & 3, OMX_Sts_BadArgErr); + armRetArgErrIf(AC!=0 && (QP<0), OMX_Sts_BadArgErr); + armRetArgErrIf(AC!=0 && (QP>51), OMX_Sts_BadArgErr); + armRetArgErrIf(AC!=0 && ppSrc==NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(AC!=0 && *ppSrc==NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(AC==0 && pDC==NULL, OMX_Sts_BadArgErr); + + pDelta = armAlignTo8Bytes(pBuffer); + + for (i=0; i<16; i++) + { + pDelta[i] = 0; + } + if (AC) + { + armVCM4P10_UnpackBlock4x4(ppSrc, pDelta); + DequantLumaAC4x4(pDelta, QP); + // if (AC == 2) { + // printf("After dequant:\n"); + // for (i=0;i<4;i++) { + // printf("%04x %04x %04x %04x\n", (uint16_t)pDelta[i*4+0], (uint16_t)pDelta[i*4+1], (uint16_t)pDelta[i*4+2], (uint16_t)pDelta[i*4+3]); + // } + // } + } + if (pDC) + { + pDelta[0] = pDC[0]; + } + armVCM4P10_TransformResidual4x4(pDelta,pDelta); + + // if (AC == 2) { + // printf("After IDCT:\n"); + // for (i=0;i<4;i++) { + // printf("%04x %04x %04x %04x\n", (uint16_t)pDelta[i*4+0], (uint16_t)pDelta[i*4+1], (uint16_t)pDelta[i*4+2], (uint16_t)pDelta[i*4+3]); + // } + // printf("Pixels:\n"); + // for (i=0;i<4;i++) { + // printf("%04x %04x %04x %04x\n", (uint16_t)pPred[i*predStep+0], (uint16_t)pPred[i*predStep+1], (uint16_t)pPred[i*predStep+2], (uint16_t)pPred[i*predStep+3]); + // } + // } + + for (y=0; y<4; y++) + { + for (x=0; x<4; x++) + { + pDst[y*dstStep+x] = (OMX_U8)armClip(0,255,pPred[y*predStep+x] + pDelta[4*y+x]); + } + } +#endif + + return OMX_Sts_NoErr; +} + + +OMXResult HIGHFUNC_ProcessLumaInterResidual ( + const OMX_U8 **ppSrc, + const OMX_U8 *pPred, + OMX_U8 *pDst, + OMX_INT predStep, + OMX_INT dstStep, + OMX_INT QP, + OMX_U8 *AC +) { +#ifdef H264BSD_N64 + assert(pPred == pDst); + assert(predStep == dstStep); + uint32_t totalCoeffMask = AC ? h264bsdTotalCoeffMask(AC) : 0; + + rsph264_queue_set_packed_delta_buffer_if_changed(0, *ppSrc); + rsph264_queue_process_luma_inter_residual(RSPH264_CACHE_SKIP_ALL, + pDst, dstStep, 0, QP, totalCoeffMask); +#else + static const OMX_U32 offset[16][2] = { + {0,0}, {4,0}, {0,4}, {4,4}, + {8,0}, {12,0}, {8,4}, {12,4}, + {0,8}, {4,8}, {0,12}, {4,12}, + {8,8}, {12,8}, {8,12}, {12,12} + }; + + for (int i = 0; i < 16; i++, AC++) + { + int x = offset[i][0]; + int y = offset[i][1]; + OMX_U8 *p = pDst + y*dstStep + x; + if (*AC) + { + OMX_U32 res = omxVCM4P10_DequantTransformResidualFromPairAndAdd( + ppSrc, p, 0, p, predStep, dstStep, QP, *AC); + if (res != OMX_Sts_NoErr) { + return res; + } + } + } +#endif + + return OMX_Sts_NoErr; +} + +OMXResult HIGHFUNC_ProcessLumaIntra16x16Residual ( + const OMX_U8 *pSrc, + OMX_U8 *pDst, + OMX_INT srcStep, + OMX_INT dstStep, + const OMX_U8 **pCoeff, + const OMX_U8 *totalCoeff, + OMX_S32 predMode, + OMX_S32 availability, + OMX_INT QP +) { +#if H264BSD_N64_INTRA + uint32_t totalCoeffMask = totalCoeff ? h264bsdTotalCoeffMask(totalCoeff) : 0; + rsph264_queue_set_packed_delta_buffer_if_changed(0, *pCoeff); + rsph264_queue_process_luma_intra16_residual(RSPH264_CACHE_SKIP_ALL, + pSrc, pDst, srcStep, dstStep, + predMode, availability, + QP, totalCoeffMask); +#else + static const OMX_U32 dcCoeffIndex[16] = + {0, 1, 4, 5, 2, 3, 6, 7, 8, 9, 12, 13, 10, 11, 14, 15}; + static const OMX_U32 offset[16][2] = { + {0,0}, {4,0}, {0,4}, {4,4}, + {8,0}, {12,0}, {8,4}, {12,4}, + {0,8}, {4,8}, {0,12}, {4,12}, + {8,8}, {12,8}, {8,12}, {12,12} + }; + + OMXResult result; + OMX_S16 dc[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; + int i; + + if (totalCoeff[24]) { + result = omxVCM4P10_TransformDequantLumaDCFromPair(pCoeff, dc, QP); + if (result != OMX_Sts_NoErr) + return result; + } + + result = omxVCM4P10_PredictIntra_16x16( + (pSrc-1), + (pSrc - srcStep), + (pSrc - srcStep-1), + pDst, + srcStep, + dstStep, + (OMXVCM4P10Intra16x16PredMode)predMode, + availability); + if (result != OMX_Sts_NoErr) + return result; + + for (i = 0; i < 16; i++, totalCoeff++) + { + int x = offset[i][0]; + int y = offset[i][1]; + OMX_U8 *p = pDst + y*dstStep + x; + OMX_S16 *pDc = &dc[dcCoeffIndex[i]]; + if (*totalCoeff || *pDc) + { + result = omxVCM4P10_DequantTransformResidualFromPairAndAdd( + pCoeff, + p, + pDc, + p, + dstStep, + dstStep, + QP, + *totalCoeff); + if (result != OMX_Sts_NoErr) + return result; + } + } +#endif + return OMX_Sts_NoErr; +} + + +OMXResult HIGHFUNC_ProcessChromaResidual ( + const OMX_U8 **pSrc, + OMX_U8 *pDst1, + OMX_U8 *pDst2, + OMX_INT dstStep1, + OMX_INT dstStep2, + OMX_INT chromaQp, + const OMX_U8 *totalCoeff +) { +#ifdef H264BSD_N64 + assert(dstStep1 == dstStep2); + uint32_t totalCoeffMask = totalCoeff ? h264bsdTotalCoeffMask(totalCoeff) : 0; + + rsph264_queue_set_packed_delta_buffer_if_changed(0, *pSrc); + rsph264_queue_process_chroma_residual(RSPH264_CACHE_SKIP_ALL, + pDst1, pDst2, dstStep1, chromaQp, totalCoeffMask); +#else + #define COMPARE_RSP 0 + + #if COMPARE_RSP + const OMX_U8 *pOrigSrc = *pSrc; + OMX_U8 fakeDst[128], fakeDstPre[128]; + assert(pDst1+64 == pDst2); + assert(dstStep1 == dstStep2); + assert(dstStep1 == 8); + memcpy(fakeDst, pDst1, 128); + memcpy(fakeDstPre, pDst1, 128); + uint32_t acflag = 0; + for (int i=0;i<8;i++) + if (totalCoeff[16+i]) + acflag |= 1<4, pBS[i]==4 for i>=4, or + * (pBS[i]==4 && pBS[i^3]!=4) for 0<=i<=3. + * - pBS is not 4-byte aligned. + * + */ + +OMXResult omxVCM4P10_FilterDeblockingChroma_HorEdge_I( + OMX_U8* pSrcDst, + OMX_S32 srcdstStep, + const OMX_U8* pAlpha, + const OMX_U8* pBeta, + const OMX_U8* pThresholds, + const OMX_U8 *pBS + ) +{ + int I, X, Y, Internal=0; + + armRetArgErrIf(pSrcDst == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot8ByteAligned(pSrcDst), OMX_Sts_BadArgErr); + armRetArgErrIf(srcdstStep & 7, OMX_Sts_BadArgErr); + armRetArgErrIf(pAlpha == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(pBeta == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(pThresholds == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pThresholds), OMX_Sts_BadArgErr); + armRetArgErrIf(pBS == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pBS), OMX_Sts_BadArgErr); + + for (Y=0; Y<8; Y+=4, Internal=1) + { + for (X=0; X<8; X++) + { + I = (X>>1)+4*(Y>>1); + + armRetArgErrIf(pBS[I] > 4, OMX_Sts_BadArgErr) + + armRetArgErrIf( (I > 3) && (pBS[I] == 4), + OMX_Sts_BadArgErr) + + armRetArgErrIf( (I < 4) && + ( (pBS[I] == 4) && (pBS[I^1] != 4) ), + OMX_Sts_BadArgErr) + + + /* Filter horizontal edge with q0 at (X,Y) */ + armVCM4P10_DeBlockPixel( + pSrcDst + Y*srcdstStep + X, + srcdstStep, + pThresholds[(X>>1)+4*(Y>>2)], + pAlpha[Internal], + pBeta[Internal], + pBS[I], + 1); + } + } + + return OMX_Sts_NoErr; +} diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_FilterDeblockingChroma_VerEdge_I.c b/src/video/h264_decoder/omxdl/omxVCM4P10_FilterDeblockingChroma_VerEdge_I.c new file mode 100644 index 000000000..10b25925d --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_FilterDeblockingChroma_VerEdge_I.c @@ -0,0 +1,131 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: omxVCM4P10_FilterDeblockingChroma_VerEdge_I.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 deblocking module + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +/** + * Function: omxVCM4P10_FilterDeblockingChroma_VerEdge_I (6.3.3.3.3) + * + * Description: + * Performs in-place deblock filtering on four vertical edges of the chroma + * macroblock (8x8). + * + * Input Arguments: + * + * pSrcDst - Pointer to the input macroblock; must be 8-byte aligned. + * srcdstStep - Step of the arrays; must be a multiple of 8. + * pAlpha - Array of size 2 of alpha thresholds (the first item is alpha + * threshold for external vertical edge, and the second item is for + * internal vertical edge); per [ISO14496-10] alpha values must be + * in the range [0,255]. + * pBeta - Array of size 2 of beta thresholds (the first item is the beta + * threshold for the external vertical edge, and the second item is + * for the internal vertical edge); per [ISO14496-10] beta values + * must be in the range [0,18]. + * pThresholds - Array of size 8 containing thresholds, TC0, for the left + * vertical edge of each 4x2 chroma block, arranged in vertical + * block order; must be aligned on a 4-byte boundary. Per + * [ISO14496-10] values must be in the range [0,25]. + * pBS - Array of size 16 of BS parameters (values for each 2x2 chroma + * block, arranged in vertical block order). This parameter is the + * same as the pBSparameter passed into FilterDeblockLuma_VerEdge; + * valid in the range [0,4] with the following restrictions: i) + * pBS[i]== 4 may occur only for 0<=i<=3, ii) pBS[i]== 4 if and + * only if pBS[i^3]== 4. Must be 4 byte aligned. + * + * Output Arguments: + * + * pSrcDst -Pointer to filtered output macroblock. + * + * Return Value: + * + * OMX_Sts_NoErr, if the function runs without error. + * + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - one or more of the following pointers is NULL: pSrcDst, pAlpha, + * pBeta, pThresholds, or pBS. + * - pSrcDst is not 8-byte aligned. + * - srcdstStep is not a multiple of 8. + * - pThresholds is not 4-byte aligned. + * - pAlpha[0] and/or pAlpha[1] is outside the range [0,255]. + * - pBeta[0] and/or pBeta[1] is outside the range [0,18]. + * - One or more entries in the table pThresholds[0..7] is outside + * of the range [0,25]. + * - pBS is out of range, i.e., one of the following conditions is true: + * pBS[i]<0, pBS[i]>4, pBS[i]==4 for i>=4, or + * (pBS[i]==4 && pBS[i^3]!=4) for 0<=i<=3. + * - pBS is not 4-byte aligned. + * + */ + +OMXResult omxVCM4P10_FilterDeblockingChroma_VerEdge_I( + OMX_U8* pSrcDst, + OMX_S32 srcdstStep, + const OMX_U8* pAlpha, + const OMX_U8* pBeta, + const OMX_U8* pThresholds, + const OMX_U8 *pBS + ) +{ + int I, X, Y, Internal=0; + + armRetArgErrIf(pSrcDst == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot8ByteAligned(pSrcDst), OMX_Sts_BadArgErr); + armRetArgErrIf(srcdstStep & 7, OMX_Sts_BadArgErr); + armRetArgErrIf(pAlpha == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(pBeta == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(pThresholds == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pThresholds), OMX_Sts_BadArgErr); + armRetArgErrIf(pBS == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pBS), OMX_Sts_BadArgErr); + armRetArgErrIf(pBeta[0] > 18, OMX_Sts_BadArgErr); + armRetArgErrIf(pBeta[1] > 18, OMX_Sts_BadArgErr); + + for (X=0; X<8; X+=4, Internal=1) + { + for (Y=0; Y<8; Y++) + { + I = (Y>>1)+4*(X>>1); + + armRetArgErrIf(pBS[I] > 4, OMX_Sts_BadArgErr); + + armRetArgErrIf( (I > 3) && (pBS[I] == 4), + OMX_Sts_BadArgErr); + + armRetArgErrIf( ( (pBS[I] == 4) && (pBS[I^3] != 4) ), + OMX_Sts_BadArgErr); + armRetArgErrIf(pThresholds[Y] > 25, OMX_Sts_BadArgErr); + + + /* Filter vertical edge with q0 at (X,Y) */ + armVCM4P10_DeBlockPixel( + pSrcDst + Y*srcdstStep + X, + 1, + pThresholds[(Y>>1)+4*(X>>2)], + pAlpha[Internal], + pBeta[Internal], + pBS[I], + 1); + } + } + + return OMX_Sts_NoErr; +} diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_FilterDeblockingLuma_HorEdge_I.c b/src/video/h264_decoder/omxdl/omxVCM4P10_FilterDeblockingLuma_HorEdge_I.c new file mode 100644 index 000000000..30a37da1f --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_FilterDeblockingLuma_HorEdge_I.c @@ -0,0 +1,125 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: omxVCM4P10_FilterDeblockingLuma_HorEdge_I.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 luma deblock module + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +/** + * Function: omxVCM4P10_FilterDeblockingLuma_HorEdge_I (6.3.3.3.2) + * + * Description: + * Performs in-place deblock filtering on four horizontal edges of the luma + * macroblock (16x16). + * + * Input Arguments: + * + * pSrcDst - pointer to the input macroblock; must be 16-byte aligned. + * srcdstStep -s tep of the arrays; must be a multiple of 16. + * pAlpha - array of size 2 of alpha thresholds (the first item is the alpha + * threshold for the external vertical edge, and the second item is + * for the internal horizontal edge); per [ISO14496-10] alpha + * values must be in the range [0,255]. + * pBeta - array of size 2 of beta thresholds (the first item is the beta + * threshold for the external horizontal edge, and the second item + * is for the internal horizontal edge). Per [ISO14496-10] beta + * values must be in the range [0,18]. + * pThresholds - array of size 16 containing thresholds, TC0, for the top + * horizontal edge of each 4x4 block, arranged in horizontal block + * order; must be aligned on a 4-byte boundary. Per [ISO14496 10] + * values must be in the range [0,25]. + * pBS - array of size 16 of BS parameters (arranged in horizontal block + * order); valid in the range [0,4] with the following + * restrictions: i) pBS[i]== 4 may occur only for 0<=i<=3, ii) + * pBS[i]== 4 if and only if pBS[i^3]== 4. Must be 4-byte aligned. + * + * Output Arguments: + * + * pSrcDst -Pointer to filtered output macroblock. + * + * Return Value: + * + * OMX_Sts_NoErr, if the function runs without error. + * + * OMX_Sts_BadArgErr, if one of the following cases occurs: + * - one or more of the following pointers is NULL: pSrcDst, pAlpha, + * pBeta, pThresholds, or pBS. + * - either pThresholds or pBS is not aligned on a 4-byte boundary. + * - pSrcDst is not 16-byte aligned. + * - srcdstStep is not a multiple of 16. + * - pAlpha[0] and/or pAlpha[1] is outside the range [0,255]. + * - pBeta[0] and/or pBeta[1] is outside the range [0,18]. + * - One or more entries in the table pThresholds[0..15] is + * outside of the range [0,25]. + * - pBS is out of range, i.e., one of the following conditions is true: + * pBS[i]<0, pBS[i]>4, pBS[i]==4 for i>=4, or + * (pBS[i]==4 && pBS[i^3]!=4) for 0<=i<=3. + * + */ + +OMXResult omxVCM4P10_FilterDeblockingLuma_HorEdge_I( + OMX_U8* pSrcDst, + OMX_S32 srcdstStep, + const OMX_U8* pAlpha, + const OMX_U8* pBeta, + const OMX_U8* pThresholds, + const OMX_U8 *pBS + ) +{ + int I, X, Y, Internal=0; + + armRetArgErrIf(pSrcDst == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot8ByteAligned(pSrcDst), OMX_Sts_BadArgErr); + armRetArgErrIf(srcdstStep & 7, OMX_Sts_BadArgErr); + armRetArgErrIf(pAlpha == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(pBeta == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(pThresholds == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pThresholds), OMX_Sts_BadArgErr); + armRetArgErrIf(pBS == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pBS), OMX_Sts_BadArgErr); + + for (Y=0; Y<16; Y+=4, Internal=1) + { + for (X=0; X<16; X++) + { + I = (X>>2)+4*(Y>>2); + + armRetArgErrIf(pBS[I] > 4, OMX_Sts_BadArgErr) + + armRetArgErrIf( (I > 3) && (pBS[I] == 4), + OMX_Sts_BadArgErr) + + armRetArgErrIf( (I < 4) && + ( (pBS[I] == 4) && (pBS[I^1] != 4) ), + OMX_Sts_BadArgErr) + + /* Filter horizontal edge with q0 at (X,Y) */ + armVCM4P10_DeBlockPixel( + pSrcDst + Y*srcdstStep + X, + srcdstStep, + pThresholds[I], + pAlpha[Internal], + pBeta[Internal], + pBS[I], + 0); + } + } + + return OMX_Sts_NoErr; +} diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_FilterDeblockingLuma_VerEdge_I.c b/src/video/h264_decoder/omxdl/omxVCM4P10_FilterDeblockingLuma_VerEdge_I.c new file mode 100644 index 000000000..8733427e3 --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_FilterDeblockingLuma_VerEdge_I.c @@ -0,0 +1,128 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: omxVCM4P10_FilterDeblockingLuma_VerEdge_I.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 luma deblock module + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +/** + * Function: omxVCM4P10_FilterDeblockingLuma_VerEdge_I (6.3.3.3.1) + * + * Description: + * Performs in-place deblock filtering on four vertical edges of the luma + * macroblock (16x16). + * + * Input Arguments: + * + * pSrcDst - Pointer to the input macroblock; must be 16-byte aligned. + * srcdstStep -Step of the arrays; must be a multiple of 16. + * pAlpha -Array of size 2 of alpha thresholds (the first item is the alpha + * threshold for the external vertical edge, and the second item is + * for the internal vertical edge); per [ISO14496-10] alpha values + * must be in the range [0,255]. + * pBeta -Array of size 2 of beta thresholds (the first item is the beta + * threshold for the external vertical edge, and the second item is + * for the internal vertical edge); per [ISO14496-10] beta values + * must be in the range [0,18]. + * pThresholds -Array of size 16 of Thresholds (TC0) (values for the left + * edge of each 4x4 block, arranged in vertical block order); must + * be aligned on a 4-byte boundary.. Per [ISO14496-10] values must + * be in the range [0,25]. + * pBS -Array of size 16 of BS parameters (arranged in vertical block + * order); valid in the range [0,4] with the following + * restrictions: i) pBS[i]== 4 may occur only for 0<=i<=3, ii) + * pBS[i]== 4 if and only if pBS[i^3]== 4. Must be 4-byte aligned. + * + * Output Arguments: + * + * pSrcDst -Pointer to filtered output macroblock. + * + * Return Value: + * If the function runs without error, it returns OMX_Sts_NoErr. + * If one of the following cases occurs, the function returns + * OMX_Sts_BadArgErr: + * Either of the pointers in pSrcDst, pAlpha, pBeta, pThresholds, or pBS + * is NULL. + * Either pThresholds or pBS is not aligned on a 4-byte boundary. + * pSrcDst is not 16-byte aligned. + * srcdstStep is not a multiple of 16. + * pAlpha[0] and/or pAlpha[1] is outside the range [0,255]. + * pBeta[0] and/or pBeta[1] is outside the range [0,18]. + * One or more entries in the table pThresholds[0..15]is outside of the + * range [0,25]. + * pBS is out of range, i.e., one of the following conditions is true: + * pBS[i]<0, pBS[i]>4, pBS[i]==4 for i>=4, or (pBS[i]==4 && + * pBS[i^3]!=4) for 0<=i<=3. + * + */ + +OMXResult omxVCM4P10_FilterDeblockingLuma_VerEdge_I( + OMX_U8* pSrcDst, + OMX_S32 srcdstStep, + const OMX_U8* pAlpha, + const OMX_U8* pBeta, + const OMX_U8* pThresholds, + const OMX_U8 *pBS + ) +{ + int X, Y, I, Internal=0; + + armRetArgErrIf(pSrcDst == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot16ByteAligned(pSrcDst),OMX_Sts_BadArgErr); + armRetArgErrIf(srcdstStep & 15, OMX_Sts_BadArgErr); + armRetArgErrIf(pAlpha == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(pBeta == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(pThresholds == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pThresholds), OMX_Sts_BadArgErr); + armRetArgErrIf(pBS == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pBS), OMX_Sts_BadArgErr); + armRetArgErrIf(pBeta[0] > 18, OMX_Sts_BadArgErr); + armRetArgErrIf(pBeta[1] > 18, OMX_Sts_BadArgErr); + + + for (X=0; X<16; X+=4, Internal=1) + { + for (Y=0; Y<16; Y++) + { + I = (Y>>2)+4*(X>>2); + + armRetArgErrIf(pBS[Y] > 4, OMX_Sts_BadArgErr); + + armRetArgErrIf((pBS[Y] == 4) && (Y > 3), + OMX_Sts_BadArgErr); + + armRetArgErrIf(( (pBS[Y] == 4) && (pBS[Y^3] != 4) ), + OMX_Sts_BadArgErr); + + armRetArgErrIf(pThresholds[Y] > 25, OMX_Sts_BadArgErr); + + /* Filter vertical edge with q0 at (X,Y) */ + armVCM4P10_DeBlockPixel( + pSrcDst + Y*srcdstStep + X, + 1, + pThresholds[I], + pAlpha[Internal], + pBeta[Internal], + pBS[I], + 0); + } + } + + return OMX_Sts_NoErr; +} diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_GetVLCInfo.c b/src/video/h264_decoder/omxdl/omxVCM4P10_GetVLCInfo.c new file mode 100644 index 000000000..81c59d6ea --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_GetVLCInfo.c @@ -0,0 +1,192 @@ +/** + * + * File Name: omxVCM4P10_GetVLCInfo.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * Description: + * + * This function extracts run-length encoding (RLE) information + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +/** + * Function: omxVCM4P10_GetVLCInfo (6.3.5.9.1) + * + * Description: + * This function extracts run-length encoding (RLE) information from the + * coefficient matrix. The results are returned in an OMXVCM4P10VLCInfo + * structure. + * + * Input Arguments: + * + * pSrcCoeff - pointer to the transform coefficient matrix. 8-byte + * alignment required. + * pScanMatrix - pointer to the scan order definition matrix. For a luma + * block the scan matrix should follow [ISO14496-10] section 8.5.4, + * and should contain the values 0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, + * 10, 7, 11, 14, 15. For a chroma block, the scan matrix should + * contain the values 0, 1, 2, 3. + * bAC - indicates presence of a DC coefficient; 0 = DC coefficient + * present, 1= DC coefficient absent. + * MaxNumCoef - specifies the number of coefficients contained in the + * transform coefficient matrix, pSrcCoeff. The value should be 16 + * for blocks of type LUMADC, LUMAAC, LUMALEVEL, and CHROMAAC. The + * value should be 4 for blocks of type CHROMADC. + * + * Output Arguments: + * + * pDstVLCInfo - pointer to structure that stores information for + * run-length coding. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: + * pSrcCoeff, pScanMatrix, pDstVLCInfo + * - pSrcCoeff is not aligned on an 8-byte boundary + * + */ +OMXResult omxVCM4P10_GetVLCInfo ( + const OMX_S16* pSrcCoeff, + const OMX_U8* pScanMatrix, + OMX_U8 bAC, + OMX_U32 MaxNumCoef, + OMXVCM4P10VLCInfo* pDstVLCInfo +) +{ + OMX_INT i, MinIndex; + OMX_S32 Value; + OMX_U32 Mask = 4, RunBefore; + OMX_S16 *pLevel; + OMX_U8 *pRun; + OMX_S16 Buf [16]; + + /* check for argument error */ + armRetArgErrIf(pSrcCoeff == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(armNot8ByteAligned(pSrcCoeff), OMX_Sts_BadArgErr) + armRetArgErrIf(pScanMatrix == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pDstVLCInfo == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(bAC > 1, OMX_Sts_BadArgErr) + armRetArgErrIf(MaxNumCoef > 16, OMX_Sts_BadArgErr) + + /* Initialize RLE Info structure */ + pDstVLCInfo->uTrailing_Ones = 0; + pDstVLCInfo->uTrailing_One_Signs = 0; + pDstVLCInfo->uNumCoeffs = 0; + pDstVLCInfo->uTotalZeros = 0; + + for (i = 0; i < 16; i++) + { + pDstVLCInfo->iLevels [i] = 0; + pDstVLCInfo->uRuns [i] = 0; + } + + MinIndex = (bAC == 0 && MaxNumCoef == 15) ? 1 : 0; + for (i = MinIndex; i < (MaxNumCoef + MinIndex); i++) + { + /* Scan */ + Buf [i - MinIndex] = pSrcCoeff [pScanMatrix [i]]; + } + + /* skip zeros at the end */ + i = MaxNumCoef - 1; + while (!Buf [i] && i >= 0) + { + i--; + } + + if (i < 0) + { + return OMX_Sts_NoErr; + } + + /* Fill RLE Info structure */ + pLevel = pDstVLCInfo->iLevels; + pRun = pDstVLCInfo->uRuns; + RunBefore = 0; + + /* Handle first non zero separate */ + pDstVLCInfo->uNumCoeffs++; + Value = Buf [i]; + if (Value == 1 || Value == -1) + { + pDstVLCInfo->uTrailing_Ones++; + + pDstVLCInfo->uTrailing_One_Signs |= + Value == -1 ? Mask : 0; + Mask >>= 1; + } + else + { + Value -= (Value > 0 ? 1 : -1); + *pLevel++ = Value; + Mask = 0; + } + + /* Remaining non zero */ + while (--i >= 0) + { + Value = Buf [i]; + if (Value) + { + pDstVLCInfo->uNumCoeffs++; + + /* Mask becomes zero after entering */ + if (Mask && + (Value == 1 || + Value == -1)) + { + pDstVLCInfo->uTrailing_Ones++; + + pDstVLCInfo->uTrailing_One_Signs |= + Value == -1 ? Mask : 0; + Mask >>= 1; + *pRun++ = RunBefore; + RunBefore = 0; + } + else + { + /* If 3 trailing ones are not completed */ + if (Mask) + { + Mask = 0; + Value -= (Value > 0 ? 1 : -1); + } + *pLevel++ = Value; + *pRun++ = RunBefore; + RunBefore = 0; + } + } + else + { + pDstVLCInfo->uTotalZeros++; + RunBefore++; + } + } + + /* Update last run */ + if (RunBefore) + { + *pRun++ = RunBefore; + } + + return OMX_Sts_NoErr; +} + +/***************************************************************************** + * END OF FILE + *****************************************************************************/ + diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_InterpolateChroma.c b/src/video/h264_decoder/omxdl/omxVCM4P10_InterpolateChroma.c new file mode 100644 index 000000000..8824de2f8 --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_InterpolateChroma.c @@ -0,0 +1,99 @@ +/** + * + * File Name: omxVCM4P10_InterpolateChroma.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * Description: + * This function will calculate 1/8 Pixel interpolation for Chroma Block + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armVC.h" +#include "armCOMM.h" + + +/** + * Function: omxVCM4P10_InterpolateChroma (6.3.3.2.2) + * + * Description: + * Performs 1/8-pixel interpolation for inter chroma MB. + * + * Input Arguments: + * + * pSrc -Pointer to the source reference frame buffer + * srcStep -Reference frame step in bytes + * dstStep -Destination frame step in bytes; must be a multiple of + * roi.width. + * dx -Fractional part of horizontal motion vector component in 1/8 pixel + * unit; valid in the range [0,7] + * dy -Fractional part of vertical motion vector component in 1/8 pixel + * unit; valid in the range [0,7] + * roi -Dimension of the interpolation region; the parameters roi.width and + * roi.height must be equal to either 2, 4, or 8. + * + * Output Arguments: + * + * pDst -Pointer to the destination frame buffer if roi.width==2, 2-byte + * alignment required if roi.width==4, 4-byte alignment required + * if roi.width==8, 8-byte alignment required + * + * Return Value: + * If the function runs without error, it returns OMX_Sts_NoErr. + * If one of the following cases occurs, the function returns + * OMX_Sts_BadArgErr: + * pSrc or pDst is NULL. + * srcStep or dstStep < 8. + * dx or dy is out of range [0-7]. + * roi.width or roi.height is out of range {2,4,8}. + * roi.width is equal to 2, but pDst is not 2-byte aligned. + * roi.width is equal to 4, but pDst is not 4-byte aligned. + * roi.width is equal to 8, but pDst is not 8 byte aligned. + * srcStep or dstStep is not a multiple of 8. + * + */ + +OMXResult omxVCM4P10_InterpolateChroma ( + const OMX_U8* pSrc, + OMX_S32 srcStep, + OMX_U8* pDst, + OMX_S32 dstStep, + OMX_S32 dx, + OMX_S32 dy, + OMXSize roi + ) +{ + /* check for argument error */ + armRetArgErrIf(pSrc == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pDst == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(srcStep < 8, OMX_Sts_BadArgErr) + armRetArgErrIf(dstStep < 8, OMX_Sts_BadArgErr) + armRetArgErrIf(dx < 0, OMX_Sts_BadArgErr) + armRetArgErrIf(dx > 7, OMX_Sts_BadArgErr) + armRetArgErrIf(dy < 0, OMX_Sts_BadArgErr) + armRetArgErrIf(dy > 7, OMX_Sts_BadArgErr) + armRetArgErrIf((roi.width != 2) && (roi.width != 4) && (roi.width != 8), OMX_Sts_BadArgErr) + armRetArgErrIf((roi.height != 2) && (roi.height != 4) && (roi.height != 8), OMX_Sts_BadArgErr) + armRetArgErrIf((roi.width == 2) && armNot2ByteAligned(pDst), OMX_Sts_BadArgErr) + armRetArgErrIf((roi.width == 4) && armNot4ByteAligned(pDst), OMX_Sts_BadArgErr) + armRetArgErrIf((roi.width == 8) && armNot8ByteAligned(pDst), OMX_Sts_BadArgErr) + armRetArgErrIf(srcStep & 7, OMX_Sts_BadArgErr) + armRetArgErrIf(dstStep & 7, OMX_Sts_BadArgErr) + + return armVCM4P10_Interpolate_Chroma + ((OMX_U8*)pSrc, srcStep, pDst, dstStep, roi.width, roi.height, dx, dy); +} + + +/***************************************************************************** + * END OF FILE + *****************************************************************************/ + diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_InterpolateHalfHor_Luma.c b/src/video/h264_decoder/omxdl/omxVCM4P10_InterpolateHalfHor_Luma.c new file mode 100644 index 000000000..ef0befac2 --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_InterpolateHalfHor_Luma.c @@ -0,0 +1,124 @@ +/** + * + * File Name: omxVCM4P10_InterpolateHalfHor_Luma.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * Description: + * This function will calculate Half horizontal luma interpolation + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +/** + * Function: omxVCM4P10_InterpolateHalfHor_Luma (6.3.5.5.1) + * + * Description: + * This function performs interpolation for two horizontal 1/2-pel positions + * (-1/2,0) and (1/2, 0) - around a full-pel position. + * + * Input Arguments: + * + * pSrc - Pointer to the top-left corner of the block used to interpolate in + * the reconstruction frame plane. + * iSrcStep - Step of the source buffer. + * iDstStep - Step of the destination(interpolation) buffer; must be a + * multiple of iWidth. + * iWidth - Width of the current block; must be equal to either 4, 8, or 16 + * iHeight - Height of the current block; must be equal to 4, 8, or 16 + * + * Output Arguments: + * + * pDstLeft -Pointer to the interpolation buffer of the left -pel position + * (-1/2, 0) + * If iWidth==4, 4-byte alignment required. + * If iWidth==8, 8-byte alignment required. + * If iWidth==16, 16-byte alignment required. + * pDstRight -Pointer to the interpolation buffer of the right -pel + * position (1/2, 0) + * If iWidth==4, 4-byte alignment required. + * If iWidth==8, 8-byte alignment required. + * If iWidth==16, 16-byte alignment required. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: + * pSrc, pDstLeft, or pDstRight + * - iWidth or iHeight have values other than 4, 8, or 16 + * - iWidth==4 but pDstLeft and/or pDstRight is/are not aligned on a 4-byte boundary + * - iWidth==8 but pDstLeft and/or pDstRight is/are not aligned on a 8-byte boundary + * - iWidth==16 but pDstLeft and/or pDstRight is/are not aligned on a 16-byte boundary + * - any alignment restrictions are violated + * + */ + +OMXResult omxVCM4P10_InterpolateHalfHor_Luma( + const OMX_U8* pSrc, + OMX_U32 iSrcStep, + OMX_U8* pDstLeft, + OMX_U8* pDstRight, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight +) +{ + OMXResult RetValue; + + /* check for argument error */ + armRetArgErrIf(pSrc == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pDstLeft == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pDstRight == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf((iWidth == 4) && + armNot4ByteAligned(pDstLeft) && + armNot4ByteAligned(pDstRight), OMX_Sts_BadArgErr) + armRetArgErrIf((iWidth == 8) && + armNot8ByteAligned(pDstLeft) && + armNot8ByteAligned(pDstRight), OMX_Sts_BadArgErr) + armRetArgErrIf((iWidth == 16) && + armNot16ByteAligned(pDstLeft) && + armNot16ByteAligned(pDstRight), OMX_Sts_BadArgErr) + + armRetArgErrIf((iHeight != 16) && (iHeight != 8)&& (iHeight != 4), OMX_Sts_BadArgErr) + armRetArgErrIf((iWidth != 16) && (iWidth != 8)&& (iWidth != 4), OMX_Sts_BadArgErr) + + RetValue = armVCM4P10_InterpolateHalfHor_Luma ( + pSrc - 1, + iSrcStep, + pDstLeft, + iDstStep, + iWidth, + iHeight); + + if (RetValue != OMX_Sts_NoErr) + { + return RetValue; + } + + RetValue = armVCM4P10_InterpolateHalfHor_Luma ( + pSrc, + iSrcStep, + pDstRight, + iDstStep, + iWidth, + iHeight); + + return RetValue; +} + +/***************************************************************************** + * END OF FILE + *****************************************************************************/ + diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_InterpolateHalfVer_Luma.c b/src/video/h264_decoder/omxdl/omxVCM4P10_InterpolateHalfVer_Luma.c new file mode 100644 index 000000000..3560ff84c --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_InterpolateHalfVer_Luma.c @@ -0,0 +1,123 @@ +/** + * + * File Name: omxVCM4P10_InterpolateHalfVer_Luma.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * Description: + * This function will calculate SAD for 4x4 blocks + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + + +/** + * Function: omxVCM4P10_InterpolateHalfVer_Luma (6.3.5.5.2) + * + * Description: + * This function performs interpolation for two vertical 1/2-pel positions - + * (0, -1/2) and (0, 1/2) - around a full-pel position. + * + * Input Arguments: + * + * pSrc - Pointer to top-left corner of block used to interpolate in the + * reconstructed frame plane + * iSrcStep - Step of the source buffer. + * iDstStep - Step of the destination (interpolation) buffer; must be a + * multiple of iWidth. + * iWidth - Width of the current block; must be equal to either 4, 8, or 16 + * iHeight - Height of the current block; must be equal to either 4, 8, or 16 + * + * Output Arguments: + * + * pDstUp -Pointer to the interpolation buffer of the -pel position above + * the current full-pel position (0, -1/2) + * If iWidth==4, 4-byte alignment required. + * If iWidth==8, 8-byte alignment required. + * If iWidth==16, 16-byte alignment required. + * pDstDown -Pointer to the interpolation buffer of the -pel position below + * the current full-pel position (0, 1/2) + * If iWidth==4, 4-byte alignment required. + * If iWidth==8, 8-byte alignment required. + * If iWidth==16, 16-byte alignment required. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: + * pSrc, pDstUp, or pDstDown + * - iWidth or iHeight have values other than 4, 8, or 16 + * - iWidth==4 but pDstUp and/or pDstDown is/are not aligned on a 4-byte boundary + * - iWidth==8 but pDstUp and/or pDstDown is/are not aligned on a 8-byte boundary + * - iWidth==16 but pDstUp and/or pDstDown is/are not aligned on a 16-byte boundary + * + */ + OMXResult omxVCM4P10_InterpolateHalfVer_Luma( + const OMX_U8* pSrc, + OMX_U32 iSrcStep, + OMX_U8* pDstUp, + OMX_U8* pDstDown, + OMX_U32 iDstStep, + OMX_U32 iWidth, + OMX_U32 iHeight +) +{ + OMXResult RetValue; + + /* check for argument error */ + armRetArgErrIf(pSrc == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pDstUp == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pDstDown == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf((iWidth == 4) && + armNot4ByteAligned(pDstUp) && + armNot4ByteAligned(pDstDown), OMX_Sts_BadArgErr) + armRetArgErrIf((iWidth == 8) && + armNot8ByteAligned(pDstUp) && + armNot8ByteAligned(pDstDown), OMX_Sts_BadArgErr) + armRetArgErrIf((iWidth == 16) && + armNot16ByteAligned(pDstUp) && + armNot16ByteAligned(pDstDown), OMX_Sts_BadArgErr) + + armRetArgErrIf((iHeight != 16) && (iHeight != 8)&& (iHeight != 4), OMX_Sts_BadArgErr) + armRetArgErrIf((iWidth != 16) && (iWidth != 8)&& (iWidth != 4), OMX_Sts_BadArgErr) + + RetValue = armVCM4P10_InterpolateHalfVer_Luma( + pSrc - iSrcStep, + iSrcStep, + pDstUp, + iDstStep, + iWidth, + iHeight); + + if (RetValue != OMX_Sts_NoErr) + { + return RetValue; + } + + RetValue = armVCM4P10_InterpolateHalfVer_Luma( + pSrc, + iSrcStep, + pDstDown, + iDstStep, + iWidth, + iHeight); + + return RetValue; +} + +/***************************************************************************** + * END OF FILE + *****************************************************************************/ + diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_InterpolateLuma.c b/src/video/h264_decoder/omxdl/omxVCM4P10_InterpolateLuma.c new file mode 100644 index 000000000..d23373501 --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_InterpolateLuma.c @@ -0,0 +1,99 @@ +/** + * + * File Name: omxVCM4P10_InterpolateLuma.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * Description: + * This function will calculate Performs quarter-pixel interpolation + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armVC.h" +#include "armCOMM.h" + +/** + * Function: omxVCM4P10_InterpolateLuma (6.3.3.2.1) + * + * Description: + * Performs quarter-pixel interpolation for inter luma MB. It is assumed that + * the frame is already padded when calling this function. + * + * Input Arguments: + * + * pSrc -Pointer to the source reference frame buffer + * srcStep -reference frame step, in bytes; must be a multiple of roi.width + * dstStep -destination frame step, in bytes; must be a multiple of + * roi.width + * dx -Fractional part of horizontal motion vector component in 1/4 pixel + * unit; valid in the range [0,3] + * dy -Fractional part of vertical motion vector y component in 1/4 pixel + * unit; valid in the range [0,3] + * roi -Dimension of the interpolation region; the parameters roi.width and + * roi.height must be equal to either 4, 8, or 16. + * + * Output Arguments: + * + * pDst -Pointer to the destination frame buffer if roi.width==4, 4-byte + * alignment required if roi.width==8, 8-byte alignment required + * if roi.width==16, 16-byte alignment required + * + * Return Value: + * If the function runs without error, it returns OMX_Sts_NoErr. + * If one of the following cases occurs, the function returns + * OMX_Sts_BadArgErr: + * pSrc or pDst is NULL. + * srcStep or dstStep < roi.width. + * dx or dy is out of range [0,3]. + * roi.width or roi.height is out of range {4, 8, 16}. + * roi.width is equal to 4, but pDst is not 4 byte aligned. + * roi.width is equal to 8 or 16, but pDst is not 8 byte aligned. + * srcStep or dstStep is not a multiple of 8. + * + */ + +OMXResult omxVCM4P10_InterpolateLuma ( + const OMX_U8* pSrc, + OMX_S32 srcStep, + OMX_U8* pDst, + OMX_S32 dstStep, + OMX_S32 dx, + OMX_S32 dy, + OMXSize roi + ) +{ + /* check for argument error */ + armRetArgErrIf(pSrc == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pDst == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(srcStep < roi.width, OMX_Sts_BadArgErr) + armRetArgErrIf(dstStep < roi.width, OMX_Sts_BadArgErr) + armRetArgErrIf(dx < 0, OMX_Sts_BadArgErr) + armRetArgErrIf(dx > 3, OMX_Sts_BadArgErr) + armRetArgErrIf(dy < 0, OMX_Sts_BadArgErr) + armRetArgErrIf(dy > 3, OMX_Sts_BadArgErr) + armRetArgErrIf((roi.width != 4) && (roi.width != 8) && (roi.width != 16), OMX_Sts_BadArgErr) + armRetArgErrIf((roi.height != 4) && (roi.height != 8) && (roi.height != 16), OMX_Sts_BadArgErr) + armRetArgErrIf((roi.width == 4) && armNot4ByteAligned(pDst), OMX_Sts_BadArgErr) + armRetArgErrIf((roi.width == 8) && armNot8ByteAligned(pDst), OMX_Sts_BadArgErr) + armRetArgErrIf((roi.width == 16) && armNot16ByteAligned(pDst), OMX_Sts_BadArgErr) + armRetArgErrIf(srcStep & 7, OMX_Sts_BadArgErr) + armRetArgErrIf(dstStep & 7, OMX_Sts_BadArgErr) + + return armVCM4P10_Interpolate_Luma + (pSrc, srcStep, pDst, dstStep, roi.width, roi.height, dx, dy); + +} + + +/***************************************************************************** + * END OF FILE + *****************************************************************************/ + diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_InvTransformDequant_ChromaDC.c b/src/video/h264_decoder/omxdl/omxVCM4P10_InvTransformDequant_ChromaDC.c new file mode 100644 index 000000000..92ba0319b --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_InvTransformDequant_ChromaDC.c @@ -0,0 +1,102 @@ +/** + * + * File Name: omxVCM4P10_InvTransformDequant_ChromaDC.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * Description: + * This function will calculate 4x4 hadamard transform of chroma DC + * coefficients and quantization + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armVC.h" +#include "armCOMM.h" + +/** + * Function: omxVCM4P10_InvTransformDequant_ChromaDC (6.3.5.6.4) + * + * Description: + * This function performs inverse 2x2 Hadamard transform and then dequantizes + * the coefficients. + * + * Input Arguments: + * + * pSrc - Pointer to the 2x2 array of the 2x2 Hadamard-transformed and + * quantized coefficients. 8 byte alignment required. + * iQP - Quantization parameter; must be in the range [0,51]. + * + * Output Arguments: + * + * pDst - Pointer to inverse-transformed and dequantized coefficients. + * 8-byte alignment required. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: pSrc + * - pSrc or pDst is not aligned on an 8-byte boundary + * + */ +OMXResult omxVCM4P10_InvTransformDequant_ChromaDC( + const OMX_S16* pSrc, + OMX_S16* pDst, + OMX_U32 iQP +) +{ + OMX_INT i, j; + OMX_S32 m[2][2]; + OMX_S32 QPer, V00, Value; + + /* check for argument error */ + armRetArgErrIf(pSrc == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(armNot8ByteAligned(pSrc), OMX_Sts_BadArgErr); + armRetArgErrIf(pDst == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(armNot8ByteAligned(pDst), OMX_Sts_BadArgErr); + armRetArgErrIf(iQP > 51, OMX_Sts_BadArgErr) + + /* Inv Hadamard Transform for 2x2 block */ + m[0][0] = pSrc[0] + pSrc[1] + pSrc[2] + pSrc[3]; + m[0][1] = pSrc[0] - pSrc[1] + pSrc[2] - pSrc[3]; + m[1][0] = pSrc[0] + pSrc[1] - pSrc[2] - pSrc[3]; + m[1][1] = pSrc[0] - pSrc[1] - pSrc[2] + pSrc[3]; + + /* Quantization */ + /* Scaling */ + QPer = iQP / 6; + V00 = armVCM4P10_VMatrix [iQP % 6][0]; + + for (j = 0; j < 2; j++) + { + for (i = 0; i < 2; i++) + { + if (QPer < 1) + { + Value = (m[j][i] * V00) >> 1; + } + else + { + Value = (m[j][i] * V00) << (QPer - 1); + } + + pDst[j * 2 + i] = (OMX_S16) Value; + } + } + + return OMX_Sts_NoErr; +} + +/***************************************************************************** + * END OF FILE + *****************************************************************************/ + diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_InvTransformDequant_LumaDC.c b/src/video/h264_decoder/omxdl/omxVCM4P10_InvTransformDequant_LumaDC.c new file mode 100644 index 000000000..a3b1200fb --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_InvTransformDequant_LumaDC.c @@ -0,0 +1,128 @@ +/** + * + * File Name: omxVCM4P10_InvTransformDequant_LumaDC.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * Description: + * This function will calculate 4x4 hadamard transform of luma DC coefficients + * and quantization + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +/** + * Function: omxVCM4P10_InvTransformDequant_LumaDC (6.3.5.6.3) + * + * Description: + * This function performs inverse 4x4 Hadamard transform and then dequantizes + * the coefficients. + * + * Input Arguments: + * + * pSrc - Pointer to the 4x4 array of the 4x4 Hadamard-transformed and + * quantized coefficients. 16 byte alignment required. + * iQP - Quantization parameter; must be in the range [0,51]. + * + * Output Arguments: + * + * pDst - Pointer to inverse-transformed and dequantized coefficients. + * 16-byte alignment required. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: pSrc + * - pSrc or pDst is not aligned on a 16-byte boundary + * + */ +OMXResult omxVCM4P10_InvTransformDequant_LumaDC( + const OMX_S16* pSrc, + OMX_S16* pDst, + OMX_U32 iQP +) +{ + OMX_INT i, j; + OMX_S32 m1[4][4], m2[4][4], Value; + OMX_S32 QPer, V; + + /* check for argument error */ + armRetArgErrIf(pSrc == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(pDst == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(iQP > 51, OMX_Sts_BadArgErr) + armRetArgErrIf(armNot16ByteAligned(pSrc), OMX_Sts_BadArgErr) + armRetArgErrIf(armNot16ByteAligned(pDst), OMX_Sts_BadArgErr) + + /* Inv Hadamard Transform for DC Luma 4x4 block */ + /* Horizontal */ + for (i = 0; i < 4; i++) + { + j = i * 4; + + m1[i][0] = pSrc[j + 0] + pSrc[j + 2]; /* a+c */ + m1[i][1] = pSrc[j + 1] + pSrc[j + 3]; /* b+d */ + m1[i][2] = pSrc[j + 0] - pSrc[j + 2]; /* a-c */ + m1[i][3] = pSrc[j + 1] - pSrc[j + 3]; /* b-d */ + + m2[i][0] = m1[i][0] + m1[i][1]; /* a+b+c+d */ + m2[i][1] = m1[i][2] + m1[i][3]; /* a+b-c-d */ + m2[i][2] = m1[i][2] - m1[i][3]; /* a-b-c+d */ + m2[i][3] = m1[i][0] - m1[i][1]; /* a-b+c-d */ + + } + + /* Vertical */ + for (i = 0; i < 4; i++) + { + m1[0][i] = m2[0][i] + m2[2][i]; + m1[1][i] = m2[1][i] + m2[3][i]; + m1[2][i] = m2[0][i] - m2[2][i]; + m1[3][i] = m2[1][i] - m2[3][i]; + + m2[0][i] = m1[0][i] + m1[1][i]; + m2[1][i] = m1[2][i] + m1[3][i]; + m2[2][i] = m1[2][i] - m1[3][i]; + m2[3][i] = m1[0][i] - m1[1][i]; + } + + + /* Scaling */ + QPer = iQP / 6; + V = armVCM4P10_VMatrix [iQP % 6][0]; + + for (j = 0; j < 4; j++) + { + for (i = 0; i < 4; i++) + { + if (QPer < 2) + { + Value = (m2[j][i] * V + (1 << (1 - QPer))) >> (2 - QPer); + } + else + { + Value = m2[j][i] * V * (1 << (QPer - 2)); + } + + pDst[j * 4 + i] = (OMX_S16) Value; + + } + } + return OMX_Sts_NoErr; +} + +/***************************************************************************** + * END OF FILE + *****************************************************************************/ + diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_InvTransformResidualAndAdd.c b/src/video/h264_decoder/omxdl/omxVCM4P10_InvTransformResidualAndAdd.c new file mode 100644 index 000000000..3303997b9 --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_InvTransformResidualAndAdd.c @@ -0,0 +1,124 @@ +/** + * + * File Name: omxVCM4P10_InvTransformResidualAndAdd.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * Description: + * This function will inverse integer 4x4 transform + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +/** + * Function: omxVCM4P10_InvTransformResidualAndAdd (6.3.5.7.1) + * + * Description: + * This function performs inverse an 4x4 integer transformation to produce + * the difference signal and then adds the difference to the prediction to get + * the reconstructed signal. + * + * Input Arguments: + * + * pSrcPred - Pointer to prediction signal. 4-byte alignment required. + * pDequantCoeff - Pointer to the transformed coefficients. 8-byte + * alignment required. + * iSrcPredStep - Step of the prediction buffer; must be a multiple of 4. + * iDstReconStep - Step of the destination reconstruction buffer; must be a + * multiple of 4. + * bAC - Indicate whether there is AC coefficients in the coefficients + * matrix. + * + * Output Arguments: + * + * pDstRecon -Pointer to the destination reconstruction buffer. 4-byte + * alignment required. + * + * Return Value: + * + * OMX_Sts_NoErr - no error + * OMX_Sts_BadArgErr - bad arguments; returned if any of the following + * conditions are true: + * - at least one of the following pointers is NULL: + * pSrcPred, pDequantCoeff, pDstRecon + * - pSrcPred is not aligned on a 4-byte boundary + * - iSrcPredStep or iDstReconStep is not a multiple of 4. + * - pDequantCoeff is not aligned on an 8-byte boundary + * + */ +OMXResult omxVCM4P10_InvTransformResidualAndAdd( + const OMX_U8* pSrcPred, + const OMX_S16* pDequantCoeff, + OMX_U8* pDstRecon, + OMX_U32 iSrcPredStep, + OMX_U32 iDstReconStep, + OMX_U8 bAC +) +{ + OMX_INT i, j; + OMX_S16 In[16], Out[16]; + OMX_S32 Value; + + /* check for argument error */ + armRetArgErrIf(pSrcPred == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(armNot4ByteAligned(pSrcPred), OMX_Sts_BadArgErr) + armRetArgErrIf(pDequantCoeff == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(armNot8ByteAligned(pDequantCoeff), OMX_Sts_BadArgErr) + armRetArgErrIf(pDstRecon == NULL, OMX_Sts_BadArgErr) + armRetArgErrIf(armNot4ByteAligned(pDstRecon), OMX_Sts_BadArgErr) + armRetArgErrIf(bAC > 1, OMX_Sts_BadArgErr) + armRetArgErrIf(iSrcPredStep == 0 || iSrcPredStep & 3, OMX_Sts_BadArgErr) + armRetArgErrIf(iDstReconStep == 0 || iDstReconStep & 3, OMX_Sts_BadArgErr) + + if (bAC) + { + for (i = 0; i < 16; i++) + { + In[i] = pDequantCoeff [i]; + } + } + else + { + /* Copy DC */ + In[0] = pDequantCoeff [0]; + + for (i = 1; i < 16; i++) + { + In[i] = 0; + } + } + + /* Residual Transform */ + armVCM4P10_TransformResidual4x4 (Out, In); + + for (j = 0; j < 4; j++) + { + for (i = 0; i < 4; i++) + { + /* Add predition */ + Value = (OMX_S32) Out [j * 4 + i] + pSrcPred [j * iSrcPredStep + i]; + + /* Saturate Value to OMX_U8 */ + Value = armClip (0, 255, Value); + + pDstRecon[j * iDstReconStep + i] = (OMX_U8) Value; + } + } + + return OMX_Sts_NoErr; +} + +/***************************************************************************** + * END OF FILE + *****************************************************************************/ + diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_MEGetBufSize.c b/src/video/h264_decoder/omxdl/omxVCM4P10_MEGetBufSize.c new file mode 100644 index 000000000..8c3a5c312 --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_MEGetBufSize.c @@ -0,0 +1,70 @@ +/** + * + * File Name: omxVCM4P10_MEGetBufSize.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * Description: + * Initialization modules for the vendor specific Motion Estimation structure. + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armVC.h" +#include "armCOMM.h" + +/** + * Function: omxVCM4P10_MEGetBufSize (6.3.5.1.1) + * + * Description: + * Computes the size, in bytes, of the vendor-specific specification + * structure for the omxVCM4P10 motion estimation functions BlockMatch_Integer + * and MotionEstimationMB. + * + * Input Arguments: + * + * MEmode - motion estimation mode; available modes are defined by the + * enumerated type OMXVCM4P10MEMode + * pMEParams -motion estimation parameters + * + * Output Arguments: + * + * pSize - pointer to the number of bytes required for the motion + * estimation specification structure + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - pMEParams or pSize is NULL. + * - an invalid MEMode is specified. + * + */ + +OMXResult omxVCM4P10_MEGetBufSize( + OMXVCM4P10MEMode MEMode, + const OMXVCM4P10MEParams *pMEParams, + OMX_U32 *pSize + ) +{ + armRetArgErrIf(!pMEParams, OMX_Sts_BadArgErr); + armRetArgErrIf(!pSize, OMX_Sts_BadArgErr); + armRetArgErrIf((MEMode != OMX_VC_M4P10_FAST_SEARCH) && + (MEMode != OMX_VC_M4P10_FULL_SEARCH), OMX_Sts_BadArgErr); + armRetArgErrIf((pMEParams->searchRange16x16 <= 0) || + (pMEParams->searchRange8x8 <= 0) || + (pMEParams->searchRange4x4 <= 0), OMX_Sts_BadArgErr); + + *pSize = (OMX_INT) sizeof(ARMVCM4P10_MESpec); + + return OMX_Sts_NoErr; +} + +/* End of file */ diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_MEInit.c b/src/video/h264_decoder/omxdl/omxVCM4P10_MEInit.c new file mode 100644 index 000000000..58ecc886a --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_MEInit.c @@ -0,0 +1,92 @@ +/** + * + * File Name: omxVCM4P10_MEInit.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * Description: + * Initialization modules for the vendor specific Motion Estimation structure. + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armVC.h" +#include "armCOMM.h" + +/** + * Function: omxVCM4P10_MEInit (6.3.5.1.2) + * + * Description: + * Initializes the vendor-specific specification structure required for the + * omxVCM4P10 motion estimation functions: BlockMatch_Integer and + * MotionEstimationMB. Memory for the specification structure *pMESpec must be + * allocated prior to calling the function, and should be aligned on a 4-byte + * boundary. The number of bytes required for the specification structure can + * be determined using the function omxVCM4P10_MEGetBufSize. Following + * initialization by this function, the vendor-specific structure *pMESpec + * should contain an implementation-specific representation of all motion + * estimation parameters received via the structure pMEParams, for example + * searchRange16x16, searchRange8x8, etc. + * + * Input Arguments: + * + * MEmode - motion estimation mode; available modes are defined by the + * enumerated type OMXVCM4P10MEMode + * pMEParams - motion estimation parameters + * pMESpec - pointer to the uninitialized ME specification structure + * + * Output Arguments: + * + * pMESpec - pointer to the initialized ME specification structure + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - pMEParams or pSize is NULL. + * - an invalid value was specified for the parameter MEmode + * - a negative or zero value was specified for one of the search ranges + * (e.g., pMBParams >searchRange8x8, pMEParams->searchRange16x16, etc.) + * - either in isolation or in combination, one or more of the enables or + * search ranges in the structure *pMEParams were configured such + * that the requested behavior fails to comply with [ISO14496-10]. + * + */ + +OMXResult omxVCM4P10_MEInit( + OMXVCM4P10MEMode MEMode, + const OMXVCM4P10MEParams *pMEParams, + void *pMESpec + ) +{ + ARMVCM4P10_MESpec *armMESpec = (ARMVCM4P10_MESpec *) pMESpec; + + armRetArgErrIf(!pMEParams, OMX_Sts_BadArgErr); + armRetArgErrIf(!pMESpec, OMX_Sts_BadArgErr); + armRetArgErrIf((MEMode != OMX_VC_M4P10_FAST_SEARCH) && + (MEMode != OMX_VC_M4P10_FULL_SEARCH), OMX_Sts_BadArgErr); + armRetArgErrIf((pMEParams->searchRange16x16 <= 0) || + (pMEParams->searchRange8x8 <= 0) || + (pMEParams->searchRange4x4 <= 0), OMX_Sts_BadArgErr); + + armMESpec->MEParams.blockSplitEnable8x8 = pMEParams->blockSplitEnable8x8; + armMESpec->MEParams.blockSplitEnable4x4 = pMEParams->blockSplitEnable4x4; + armMESpec->MEParams.halfSearchEnable = pMEParams->halfSearchEnable; + armMESpec->MEParams.quarterSearchEnable = pMEParams->quarterSearchEnable; + armMESpec->MEParams.intraEnable4x4 = pMEParams->intraEnable4x4; + armMESpec->MEParams.searchRange16x16 = pMEParams->searchRange16x16; + armMESpec->MEParams.searchRange8x8 = pMEParams->searchRange8x8; + armMESpec->MEParams.searchRange4x4 = pMEParams->searchRange4x4; + armMESpec->MEMode = MEMode; + + return OMX_Sts_NoErr; +} + +/* End of file */ diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_PredictIntraChroma_8x8.c b/src/video/h264_decoder/omxdl/omxVCM4P10_PredictIntraChroma_8x8.c new file mode 100644 index 000000000..214925557 --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_PredictIntraChroma_8x8.c @@ -0,0 +1,301 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: omxVCM4P10_PredictIntraChroma_8x8.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 Chroma 8x8 intra prediction module + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +#ifdef H264BSD_N64 +#include "../../rsph264_internal.h" +#endif + + +/* + * Description: + * Perform DC style intra prediction, upper block has priority + * + * Parameters: + * [in] pSrcLeft Pointer to the buffer of 16 left coefficients: + * p[x, y] (x = -1, y = 0..3) + * [in] pSrcAbove Pointer to the buffer of 16 above coefficients: + * p[x,y] (x = 0..3, y = -1) + * [in] leftStep Step of left coefficient buffer + * [in] dstStep Step of the destination buffer + * [in] availability Neighboring 16x16 MB availability flag + * [out] pDst Pointer to the destination buffer + * + * Return Value: + * None + */ +#if !H264BSD_N64_INTRA +static void armVCM4P10_PredictIntraDCUp4x4( + const OMX_U8* pSrcLeft, + const OMX_U8 *pSrcAbove, + OMX_U8* pDst, + OMX_INT leftStep, + OMX_INT dstStep, + OMX_S32 availability +) +{ + int x, y, Sum=0, Count = 0; + + if (availability & OMX_VC_UPPER) + { + for (x=0; x<4; x++) + { + Sum += pSrcAbove[x]; + } + Count++; + } + else if (availability & OMX_VC_LEFT) + { + for (y=0; y<4; y++) + { + Sum += pSrcLeft[y*leftStep]; + } + Count++; + } + if (Count==0) + { + Sum = 128; + } + else + { + Sum = (Sum + 2) >> 2; + } + for (y=0; y<4; y++) + { + for (x=0; x<4; x++) + { + pDst[y*dstStep+x] = (OMX_U8)Sum; + } + } +} +#endif + +/* + * Description: + * Perform DC style intra prediction, left block has priority + * + * Parameters: + * [in] pSrcLeft Pointer to the buffer of 16 left coefficients: + * p[x, y] (x = -1, y = 0..3) + * [in] pSrcAbove Pointer to the buffer of 16 above coefficients: + * p[x,y] (x = 0..3, y = -1) + * [in] leftStep Step of left coefficient buffer + * [in] dstStep Step of the destination buffer + * [in] availability Neighboring 16x16 MB availability flag + * [out] pDst Pointer to the destination buffer + * + * Return Value: + * None + */ +#if !H264BSD_N64_INTRA +static void armVCM4P10_PredictIntraDCLeft4x4( + const OMX_U8* pSrcLeft, + const OMX_U8 *pSrcAbove, + OMX_U8* pDst, + OMX_INT leftStep, + OMX_INT dstStep, + OMX_S32 availability +) +{ + int x, y, Sum=0, Count = 0; + + if (availability & OMX_VC_LEFT) + { + for (y=0; y<4; y++) + { + Sum += pSrcLeft[y*leftStep]; + } + Count++; + } + else if (availability & OMX_VC_UPPER) + { + for (x=0; x<4; x++) + { + Sum += pSrcAbove[x]; + } + Count++; + } + if (Count==0) + { + Sum = 128; + } + else + { + Sum = (Sum + 2) >> 2; + } + for (y=0; y<4; y++) + { + for (x=0; x<4; x++) + { + pDst[y*dstStep+x] = (OMX_U8)Sum; + } + } +} +#endif + +/** + * Function: omxVCM4P10_PredictIntraChroma_8x8 (6.3.3.1.3) + * + * Description: + * Performs intra prediction for chroma samples. + * + * Input Arguments: + * + * pSrcLeft - Pointer to the buffer of 8 left pixels: p[x, y] (x = -1, y= + * 0..7). + * pSrcAbove - Pointer to the buffer of 8 above pixels: p[x,y] (x = 0..7, y + * = -1); must be aligned on an 8-byte boundary. + * pSrcAboveLeft - Pointer to the above left pixels: p[x,y] (x = -1, y = -1) + * leftStep - Step of left pixel buffer; must be a multiple of 8. + * dstStep - Step of the destination buffer; must be a multiple of 8. + * predMode - Intra chroma prediction mode, please refer to section 3.4.3. + * availability - Neighboring chroma block availability flag, please refer + * to "Neighboring Macroblock Availability". + * + * Output Arguments: + * + * pDst - Pointer to the destination buffer; must be aligned on an 8-byte + * boundary. + * + * Return Value: + * If the function runs without error, it returns OMX_Sts_NoErr. + * If any of the following cases occurs, the function returns + * OMX_Sts_BadArgErr: + * pDst is NULL. + * dstStep < 8 or dstStep is not a multiple of 8. + * leftStep is not a multiple of 8. + * predMode is not in the valid range of enumeration + * OMXVCM4P10IntraChromaPredMode. + * predMode is OMX_VC_CHROMA_VERT, but availability doesn't set + * OMX_VC_UPPER indicating p[x,-1] (x = 0..7) is not available. + * predMode is OMX_VC_CHROMA_HOR, but availability doesn't set OMX_VC_LEFT + * indicating p[-1,y] (y = 0..7) is not available. + * predMode is OMX_VC_CHROMA_PLANE, but availability doesn't set + * OMX_VC_UPPER_LEFT or OMX_VC_UPPER or OMX_VC_LEFT indicating + * p[x,-1](x = 0..7), or p[-1,y] (y = 0..7), or p[-1,-1] is not + * available. + * availability sets OMX_VC_UPPER, but pSrcAbove is NULL. + * availability sets OMX_VC_LEFT, but pSrcLeft is NULL. + * availability sets OMX_VC_UPPER_LEFT, but pSrcAboveLeft is NULL. + * either pSrcAbove or pDst is not aligned on a 8-byte boundary. Note: + * pSrcAbove, pSrcAbove, pSrcAboveLeft may be invalid pointer if + * they are not used by intra prediction implied in predMode. + * Note: OMX_VC_UPPER_RIGHT is not used in intra chroma + * prediction. + * + */ +OMXResult omxVCM4P10_PredictIntraChroma_8x8( + const OMX_U8* pSrcLeft, + const OMX_U8 *pSrcAbove, + const OMX_U8 *pSrcAboveLeft, + OMX_U8* pDst, + OMX_INT leftStep, + OMX_INT dstStep, + OMXVCM4P10IntraChromaPredMode predMode, + OMX_S32 availability + ) +{ +#if H264BSD_N64_INTRA + // FIXME: we must flush dest cache here, otherwise 1st frame + // of joker is corrupted. Investigate. + rsph264_queue_intrapred_chroma_8x8(0, //RSPH264_CACHE_SKIP_ALL, + pSrcLeft, pSrcAbove, pSrcAboveLeft, + pDst, leftStep, dstStep, + predMode, availability); + // rsph264_sync(); +#else + int x, y, Sum; + int H, V, a, b, c; + + armRetArgErrIf(pDst == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(dstStep < 8, OMX_Sts_BadArgErr); + armRetArgErrIf((dstStep % 8) != 0, OMX_Sts_BadArgErr); + armRetArgErrIf((leftStep % 8) != 0, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot8ByteAligned(pSrcAbove), OMX_Sts_BadArgErr); + armRetArgErrIf(armNot8ByteAligned(pDst), OMX_Sts_BadArgErr); + armRetArgErrIf((availability & OMX_VC_UPPER) && pSrcAbove == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf((availability & OMX_VC_LEFT ) && pSrcLeft == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf((availability & OMX_VC_UPPER_LEFT) && pSrcAboveLeft == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_CHROMA_VERT && !(availability & OMX_VC_UPPER), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_CHROMA_HOR && !(availability & OMX_VC_LEFT), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_CHROMA_PLANE && !(availability & OMX_VC_UPPER), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_CHROMA_PLANE && !(availability & OMX_VC_UPPER_LEFT), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_CHROMA_PLANE && !(availability & OMX_VC_LEFT), OMX_Sts_BadArgErr); + armRetArgErrIf((unsigned)predMode > OMX_VC_CHROMA_PLANE, OMX_Sts_BadArgErr); + + switch (predMode) + { + case OMX_VC_CHROMA_DC: + armVCM4P10_PredictIntraDC4x4( pSrcLeft, pSrcAbove, pDst, leftStep, dstStep, availability); + armVCM4P10_PredictIntraDCUp4x4( pSrcLeft, pSrcAbove+4, pDst+4, leftStep, dstStep, availability); + armVCM4P10_PredictIntraDCLeft4x4( pSrcLeft+4*leftStep, pSrcAbove, pDst+4*dstStep, leftStep, dstStep, availability); + armVCM4P10_PredictIntraDC4x4( pSrcLeft+4*leftStep, pSrcAbove+4, pDst+4+4*dstStep, leftStep, dstStep, availability); + break; + + case OMX_VC_CHROMA_HOR: + for (y=0; y<8; y++) + { + for (x=0; x<8; x++) + { + pDst[y*dstStep+x] = pSrcLeft[y*leftStep]; + } + } + break; + + case OMX_VC_CHROMA_VERT: + for (y=0; y<8; y++) + { + for (x=0; x<8; x++) + { + pDst[y*dstStep+x] = pSrcAbove[x]; + } + } + break; + + case OMX_VC_CHROMA_PLANE: + H = 4*(pSrcAbove[7] - pSrcAboveLeft[0]); + for (x=2; x>=0; x--) + { + H += (x+1)*(pSrcAbove[4+x] - pSrcAbove[2-x]); + } + V = 4*(pSrcLeft[7*leftStep] - pSrcAboveLeft[0]); + for (y=2; y>=0; y--) + { + V += (y+1)*(pSrcLeft[(4+y)*leftStep] - pSrcLeft[(2-y)*leftStep]); + } + a = 16*(pSrcAbove[7] + pSrcLeft[7*leftStep]); + b = (17*H+16)>>5; + c = (17*V+16)>>5; + for (y=0; y<8; y++) + { + for (x=0; x<8; x++) + { + Sum = (a + b*(x-3) + c*(y-3) + 16)>>5; + pDst[y*dstStep+x] = (OMX_U8)armClip(0,255,Sum); + } + } + break; + } + +#endif + return OMX_Sts_NoErr; +} diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_PredictIntra_16x16.c b/src/video/h264_decoder/omxdl/omxVCM4P10_PredictIntra_16x16.c new file mode 100644 index 000000000..a94d566f2 --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_PredictIntra_16x16.c @@ -0,0 +1,202 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: omxVCM4P10_PredictIntra_16x16.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 16x16 intra prediction module + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +#ifdef H264BSD_N64 +#include "../../rsph264_internal.h" +#endif + + +/** + * Function: omxVCM4P10_PredictIntra_16x16 (6.3.3.1.2) + * + * Description: + * Perform Intra_16x16 prediction for luma samples. If the upper-right block + * is not available, then duplication work should be handled inside the + * function. Users need not define them outside. + * + * Input Arguments: + * + * pSrcLeft - Pointer to the buffer of 16 left pixels: p[x, y] (x = -1, y = + * 0..15) + * pSrcAbove - Pointer to the buffer of 16 above pixels: p[x,y] (x = 0..15, + * y= -1); must be aligned on a 16-byte boundary. + * pSrcAboveLeft - Pointer to the above left pixels: p[x,y] (x = -1, y = -1) + * leftStep - Step of left pixel buffer; must be a multiple of 16. + * dstStep - Step of the destination buffer; must be a multiple of 16. + * predMode - Intra_16x16 prediction mode, please refer to section 3.4.1. + * availability - Neighboring 16x16 MB availability flag. Refer to + * section 3.4.4. + * + * Output Arguments: + * + * pDst -Pointer to the destination buffer; must be aligned on a 16-byte + * boundary. + * + * Return Value: + * If the function runs without error, it returns OMX_Sts_NoErr. + * If one of the following cases occurs, the function returns + * OMX_Sts_BadArgErr: + * pDst is NULL. + * dstStep < 16. or dstStep is not a multiple of 16. + * leftStep is not a multiple of 16. + * predMode is not in the valid range of enumeration + * OMXVCM4P10Intra16x16PredMode + * predMode is OMX_VC_16X16_VERT, but availability doesn't set + * OMX_VC_UPPER indicating p[x,-1] (x = 0..15) is not available. + * predMode is OMX_VC_16X16_HOR, but availability doesn't set OMX_VC_LEFT + * indicating p[-1,y] (y = 0..15) is not available. + * predMode is OMX_VC_16X16_PLANE, but availability doesn't set + * OMX_VC_UPPER_LEFT or OMX_VC_UPPER or OMX_VC_LEFT indicating + * p[x,-1](x = 0..15), or p[-1,y] (y = 0..15), or p[-1,-1] is not + * available. + * availability sets OMX_VC_UPPER, but pSrcAbove is NULL. + * availability sets OMX_VC_LEFT, but pSrcLeft is NULL. + * availability sets OMX_VC_UPPER_LEFT, but pSrcAboveLeft is NULL. + * either pSrcAbove or pDst is not aligned on a 16-byte boundary. + * + * Note: + * pSrcAbove, pSrcAbove, pSrcAboveLeft may be invalid pointers if + * they are not used by intra prediction implied in predMode. + * Note: + * OMX_VC_UPPER_RIGHT is not used in intra_16x16 luma prediction. + * + */ +OMXResult omxVCM4P10_PredictIntra_16x16( + const OMX_U8* pSrcLeft, + const OMX_U8 *pSrcAbove, + const OMX_U8 *pSrcAboveLeft, + OMX_U8* pDst, + OMX_INT leftStep, + OMX_INT dstStep, + OMXVCM4P10Intra16x16PredMode predMode, + OMX_S32 availability) +{ + int x,y,Sum,Count; + int H,V,a,b,c; + + armRetArgErrIf(pDst == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(dstStep < 16, OMX_Sts_BadArgErr); + armRetArgErrIf((dstStep % 16) != 0, OMX_Sts_BadArgErr); + armRetArgErrIf((leftStep % 16) != 0, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot16ByteAligned(pSrcAbove), OMX_Sts_BadArgErr); + armRetArgErrIf(armNot16ByteAligned(pDst), OMX_Sts_BadArgErr); + armRetArgErrIf((availability & OMX_VC_UPPER) && pSrcAbove == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf((availability & OMX_VC_LEFT ) && pSrcLeft == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf((availability & OMX_VC_UPPER_LEFT) && pSrcAboveLeft == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_16X16_VERT && !(availability & OMX_VC_UPPER), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_16X16_HOR && !(availability & OMX_VC_LEFT), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_16X16_PLANE && !(availability & OMX_VC_UPPER), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_16X16_PLANE && !(availability & OMX_VC_UPPER_LEFT), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_16X16_PLANE && !(availability & OMX_VC_LEFT), OMX_Sts_BadArgErr); + armRetArgErrIf((unsigned)predMode > OMX_VC_16X16_PLANE, OMX_Sts_BadArgErr); + + switch (predMode) + { + case OMX_VC_16X16_VERT: + for (y=0; y<16; y++) + { + for (x=0; x<16; x++) + { + pDst[y*dstStep+x] = pSrcAbove[x]; + } + } + break; + + case OMX_VC_16X16_HOR: + for (y=0; y<16; y++) + { + for (x=0; x<16; x++) + { + pDst[y*dstStep+x] = pSrcLeft[y*leftStep]; + } + } + break; + + case OMX_VC_16X16_DC: + /* This can always be used even if no blocks available */ + Sum = 0; + Count = 0; + if (availability & OMX_VC_LEFT) + { + for (y=0; y<16; y++) + { + Sum += pSrcLeft[y*leftStep]; + } + Count++; + } + if (availability & OMX_VC_UPPER) + { + for (x=0; x<16; x++) + { + Sum += pSrcAbove[x]; + } + Count++; + } + if (Count==0) + { + Sum = 128; + } + else if (Count==1) + { + Sum = (Sum + 8) >> 4; + } + else /* Count = 2 */ + { + Sum = (Sum + 16) >> 5; + } + for (y=0; y<16; y++) + { + for (x=0; x<16; x++) + { + pDst[y*dstStep+x] = (OMX_U8)Sum; + } + } + break; + + case OMX_VC_16X16_PLANE: + H = 8*(pSrcAbove[15] - pSrcAboveLeft[0]); + for (x=6; x>=0; x--) + { + H += (x+1)*(pSrcAbove[8+x] - pSrcAbove[6-x]); + } + V = 8*(pSrcLeft[15*leftStep] - pSrcAboveLeft[0]); + for (y=6; y>=0; y--) + { + V += (y+1)*(pSrcLeft[(8+y)*leftStep] - pSrcLeft[(6-y)*leftStep]); + } + a = 16*(pSrcAbove[15] + pSrcLeft[15*leftStep]); + b = (5*H+32)>>6; + c = (5*V+32)>>6; + for (y=0; y<16; y++) + { + for (x=0; x<16; x++) + { + Sum = (a + b*(x-7) + c*(y-7) + 16)>>5; + pDst[y*dstStep+x] = (OMX_U8)armClip(0,255,Sum); + } + } + break; + } + return OMX_Sts_NoErr; +} + diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_PredictIntra_4x4.c b/src/video/h264_decoder/omxdl/omxVCM4P10_PredictIntra_4x4.c new file mode 100644 index 000000000..81ca3f0aa --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_PredictIntra_4x4.c @@ -0,0 +1,438 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: omxVCM4P10_PredictIntra_4x4.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 4x4 intra prediction module + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +#ifdef H264BSD_N64 +#include "../../rsph264_internal.h" +#include "../h264bsd_macroblock_layer.h" +#endif + +/** + * Function: omxVCM4P10_PredictIntra_4x4 (6.3.3.1.1) + * + * Description: + * Perform Intra_4x4 prediction for luma samples. If the upper-right block is + * not available, then duplication work should be handled inside the function. + * Users need not define them outside. + * + * Input Arguments: + * + * pSrcLeft - Pointer to the buffer of 4 left pixels: + * p[x, y] (x = -1, y = 0..3) + * pSrcAbove - Pointer to the buffer of 8 above pixels: + * p[x,y] (x = 0..7, y =-1); + * must be aligned on a 4-byte boundary. + * pSrcAboveLeft - Pointer to the above left pixels: p[x,y] (x = -1, y = -1) + * leftStep - Step of left pixel buffer; must be a multiple of 4. + * dstStep - Step of the destination buffer; must be a multiple of 4. + * predMode - Intra_4x4 prediction mode. + * availability - Neighboring 4x4 block availability flag, refer to + * "Neighboring Macroblock Availability" . + * + * Output Arguments: + * + * pDst - Pointer to the destination buffer; must be aligned on a 4-byte + * boundary. + * + * Return Value: + * If the function runs without error, it returns OMX_Sts_NoErr. + * If one of the following cases occurs, the function returns + * OMX_Sts_BadArgErr: + * pDst is NULL. + * dstStep < 4, or dstStep is not a multiple of 4. + * leftStep is not a multiple of 4. + * predMode is not in the valid range of enumeration + * OMXVCM4P10Intra4x4PredMode. + * predMode is OMX_VC_4x4_VERT, but availability doesn't set OMX_VC_UPPER + * indicating p[x,-1] (x = 0..3) is not available. + * predMode is OMX_VC_4x4_HOR, but availability doesn't set OMX_VC_LEFT + * indicating p[-1,y] (y = 0..3) is not available. + * predMode is OMX_VC_4x4_DIAG_DL, but availability doesn't set + * OMX_VC_UPPER indicating p[x, 1] (x = 0..3) is not available. + * predMode is OMX_VC_4x4_DIAG_DR, but availability doesn't set + * OMX_VC_UPPER_LEFT or OMX_VC_UPPER or OMX_VC_LEFT indicating + * p[x,-1] (x = 0..3), or p[-1,y] (y = 0..3) or p[-1,-1] is not + * available. + * predMode is OMX_VC_4x4_VR, but availability doesn't set + * OMX_VC_UPPER_LEFT or OMX_VC_UPPER or OMX_VC_LEFT indicating + * p[x,-1] (x = 0..3), or p[-1,y] (y = 0..3) or p[-1,-1] is not + * available. + * predMode is OMX_VC_4x4_HD, but availability doesn't set + * OMX_VC_UPPER_LEFT or OMX_VC_UPPER or OMX_VC_LEFT indicating + * p[x,-1] (x = 0..3), or p[-1,y] (y = 0..3) or p[-1,-1] is not + * available. + * predMode is OMX_VC_4x4_VL, but availability doesn't set OMX_VC_UPPER + * indicating p[x,-1] (x = 0..3) is not available. + * predMode is OMX_VC_4x4_HU, but availability doesn't set OMX_VC_LEFT + * indicating p[-1,y] (y = 0..3) is not available. + * availability sets OMX_VC_UPPER, but pSrcAbove is NULL. + * availability sets OMX_VC_LEFT, but pSrcLeft is NULL. + * availability sets OMX_VC_UPPER_LEFT, but pSrcAboveLeft is NULL. + * either pSrcAbove or pDst is not aligned on a 4-byte boundary. + * + * Note: + * pSrcAbove, pSrcAbove, pSrcAboveLeft may be invalid pointers if + * they are not used by intra prediction as implied in predMode. + * + */ + + +OMXResult omxVCM4P10_PredictIntra_4x4( + const OMX_U8* pSrcLeft, + const OMX_U8 *pSrcAbove, + const OMX_U8 *pSrcAboveLeft, + OMX_U8* pDst, + OMX_INT leftStep, + OMX_INT dstStep, + OMXVCM4P10Intra4x4PredMode predMode, + OMX_S32 availability + ) +{ + int x, y; + OMX_U8 pTmp[10]; + + armRetArgErrIf(pDst == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf((leftStep % 4) != 0, OMX_Sts_BadArgErr); + armRetArgErrIf((dstStep % 4) != 0, OMX_Sts_BadArgErr); + armRetArgErrIf((dstStep < 4), OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pSrcAbove), OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pDst), OMX_Sts_BadArgErr); + armRetArgErrIf((availability & OMX_VC_UPPER) && pSrcAbove == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf((availability & OMX_VC_LEFT ) && pSrcLeft == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf((availability & OMX_VC_UPPER_LEFT) && pSrcAboveLeft == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_4X4_VERT && !(availability & OMX_VC_UPPER), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_4X4_HOR && !(availability & OMX_VC_LEFT), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_4X4_DIAG_DL && !(availability & OMX_VC_UPPER), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_4X4_DIAG_DR && !(availability & OMX_VC_UPPER), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_4X4_DIAG_DR && !(availability & OMX_VC_UPPER_LEFT), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_4X4_DIAG_DR && !(availability & OMX_VC_LEFT), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_4X4_VR && !(availability & OMX_VC_UPPER), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_4X4_VR && !(availability & OMX_VC_UPPER_LEFT), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_4X4_VR && !(availability & OMX_VC_LEFT), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_4X4_HD && !(availability & OMX_VC_UPPER), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_4X4_HD && !(availability & OMX_VC_UPPER_LEFT), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_4X4_HD && !(availability & OMX_VC_LEFT), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_4X4_VL && !(availability & OMX_VC_UPPER), OMX_Sts_BadArgErr); + armRetArgErrIf(predMode==OMX_VC_4X4_HU && !(availability & OMX_VC_LEFT), OMX_Sts_BadArgErr); + armRetArgErrIf((unsigned)predMode > OMX_VC_4X4_HU, OMX_Sts_BadArgErr); + + + /* Note: This code must not read the pSrc arrays unless the corresponding + * block is marked as available. If the block is not avaibable then pSrc + * may not be a valid pointer. + * + * Note: To make the code more readable we refer to the neighbouring pixels + * in variables named as below: + * + * UL U0 U1 U2 U3 U4 U5 U6 U7 + * L0 xx xx xx xx + * L1 xx xx xx xx + * L2 xx xx xx xx + * L3 xx xx xx xx + */ + +#define UL pSrcAboveLeft[0] +#define U0 pSrcAbove[0] +#define U1 pSrcAbove[1] +#define U2 pSrcAbove[2] +#define U3 pSrcAbove[3] +#define U4 pSrcAbove[4] +#define U5 pSrcAbove[5] +#define U6 pSrcAbove[6] +#define U7 pSrcAbove[7] +#define L0 pSrcLeft[0*leftStep] +#define L1 pSrcLeft[1*leftStep] +#define L2 pSrcLeft[2*leftStep] +#define L3 pSrcLeft[3*leftStep] + + switch (predMode) + { + case OMX_VC_4X4_VERT: + for (y=0; y<4; y++) + { + pDst[y*dstStep+0] = U0; + pDst[y*dstStep+1] = U1; + pDst[y*dstStep+2] = U2; + pDst[y*dstStep+3] = U3; + } + break; + + case OMX_VC_4X4_HOR: + for (x=0; x<4; x++) + { + pDst[0*dstStep+x] = L0; + pDst[1*dstStep+x] = L1; + pDst[2*dstStep+x] = L2; + pDst[3*dstStep+x] = L3; + } + break; + + case OMX_VC_4X4_DC: + /* This can always be used even if no blocks available */ + armVCM4P10_PredictIntraDC4x4(pSrcLeft, pSrcAbove, pDst, leftStep, dstStep, availability); + break; + + case OMX_VC_4X4_DIAG_DL: + pTmp[0] = (OMX_U8)((U0 + 2*U1 + U2 + 2)>>2); + pTmp[1] = (OMX_U8)((U1 + 2*U2 + U3 + 2)>>2); + if (availability & OMX_VC_UPPER_RIGHT) + { + pTmp[2] = (OMX_U8)((U2 + 2*U3 + U4 + 2)>>2); + pTmp[3] = (OMX_U8)((U3 + 2*U4 + U5 + 2)>>2); + pTmp[4] = (OMX_U8)((U4 + 2*U5 + U6 + 2)>>2); + pTmp[5] = (OMX_U8)((U5 + 2*U6 + U7 + 2)>>2); + pTmp[6] = (OMX_U8)((U6 + 3*U7 + 2)>>2); + } + else + { + pTmp[2] = (OMX_U8)((U2 + 3*U3 + 2)>>2); + pTmp[3] = U3; + pTmp[4] = U3; + pTmp[5] = U3; + pTmp[6] = U3; + } + for (y=0; y<4; y++) + { + for (x=0; x<4; x++) + { + pDst[y*dstStep+x] = pTmp[x+y]; + } + } + break; + + case OMX_VC_4X4_DIAG_DR: + /* x-y = -3, -2, -1, 0, 1, 2, 3 */ + pTmp[0] = (OMX_U8)((L1 + 2*L2 + L3 + 2)>>2); + pTmp[1] = (OMX_U8)((L0 + 2*L1 + L2 + 2)>>2); + pTmp[2] = (OMX_U8)((UL + 2*L0 + L1 + 2)>>2); + pTmp[3] = (OMX_U8)((U0 + 2*UL + L0 + 2)>>2); + pTmp[4] = (OMX_U8)((U1 + 2*U0 + UL + 2)>>2); + pTmp[5] = (OMX_U8)((U2 + 2*U1 + U0 + 2)>>2); + pTmp[6] = (OMX_U8)((U3 + 2*U2 + U1 + 2)>>2); + for (y=0; y<4; y++) + { + for (x=0; x<4; x++) + { + pDst[y*dstStep+x] = pTmp[3+x-y]; + } + } + break; + + case OMX_VC_4X4_VR: + /* zVR=2x-y = -3, -2, -1, 0, 1, 2, 3, 4, 5, 6 + * x-(y>>1) = -1, -1, 0, 0, 1, 1, 2, 2, 3, 3 + * y = 3, 2, ?, ?, ?, ?, ?, ?, 1, 0 + */ + pTmp[0] = (OMX_U8)((L2 + 2*L1 + L0 + 2)>>2); + pTmp[1] = (OMX_U8)((L1 + 2*L0 + UL + 2)>>2); + pTmp[2] = (OMX_U8)((L0 + 2*UL + U0 + 2)>>2); + pTmp[3] = (OMX_U8)((UL + U0 + 1)>>1); + pTmp[4] = (OMX_U8)((UL + 2*U0 + U1 + 2)>>2); + pTmp[5] = (OMX_U8)((U0 + U1 + 1)>>1); + pTmp[6] = (OMX_U8)((U0 + 2*U1 + U2 + 2)>>2); + pTmp[7] = (OMX_U8)((U1 + U2 + 1)>>1); + pTmp[8] = (OMX_U8)((U1 + 2*U2 + U3 + 2)>>2); + pTmp[9] = (OMX_U8)((U2 + U3 + 1)>>1); + for (y=0; y<4; y++) + { + for (x=0; x<4; x++) + { + pDst[y*dstStep+x] = pTmp[3+2*x-y]; + } + } + break; + + case OMX_VC_4X4_HD: + /* zHD=2y-x = -3 -2 -1 0 1 2 3 4 5 6 + * y-(x>>1) = -1 -1 0 0 1 1 2 2 3 3 + * x = 3 2 1 0 + */ + pTmp[0] = (OMX_U8)((U2 + 2*U1 + U0 + 2)>>2); + pTmp[1] = (OMX_U8)((U1 + 2*U0 + UL + 2)>>2); + pTmp[2] = (OMX_U8)((U0 + 2*UL + L0 + 2)>>2); + pTmp[3] = (OMX_U8)((UL + L0 + 1)>>1); + pTmp[4] = (OMX_U8)((UL + 2*L0 + L1 + 2)>>2); + pTmp[5] = (OMX_U8)((L0 + L1 + 1)>>1); + pTmp[6] = (OMX_U8)((L0 + 2*L1 + L2 + 2)>>2); + pTmp[7] = (OMX_U8)((L1 + L2 + 1)>>1); + pTmp[8] = (OMX_U8)((L1 + 2*L2 + L3 + 2)>>2); + pTmp[9] = (OMX_U8)((L2 + L3 + 1)>>1); + for (y=0; y<4; y++) + { + for (x=0; x<4; x++) + { + pDst[y*dstStep+x] = pTmp[3+2*y-x]; + } + } + break; + + case OMX_VC_4X4_VL: + /* Note: x+(y>>1) = (2*x+y)>>1 + * 2x+y = 0 1 2 3 4 5 6 7 8 9 + */ + pTmp[0] = (OMX_U8)((U0 + U1 + 1)>>1); + pTmp[1] = (OMX_U8)((U0 + 2*U1 + U2 + 2)>>2); + pTmp[2] = (OMX_U8)((U1 + U2 + 1)>>1); + pTmp[3] = (OMX_U8)((U1 + 2*U2 + U3 + 2)>>2); + pTmp[4] = (OMX_U8)((U2 + U3 + 1)>>1); + if (availability & OMX_VC_UPPER_RIGHT) + { + pTmp[5] = (OMX_U8)((U2 + 2*U3 + U4 + 2)>>2); + pTmp[6] = (OMX_U8)((U3 + U4 + 1)>>1); + pTmp[7] = (OMX_U8)((U3 + 2*U4 + U5 + 2)>>2); + pTmp[8] = (OMX_U8)((U4 + U5 + 1)>>1); + pTmp[9] = (OMX_U8)((U4 + 2*U5 + U6 + 2)>>2); + } + else + { + pTmp[5] = (OMX_U8)((U2 + 3*U3 + 2)>>2); + pTmp[6] = U3; + pTmp[7] = U3; + pTmp[8] = U3; + pTmp[9] = U3; + } + for (y=0; y<4; y++) + { + for (x=0; x<4; x++) + { + pDst[y*dstStep+x] = pTmp[2*x+y]; + } + } + break; + + case OMX_VC_4X4_HU: + /* zHU = x+2*y */ + pTmp[0] = (OMX_U8)((L0 + L1 + 1)>>1); + pTmp[1] = (OMX_U8)((L0 + 2*L1 + L2 + 2)>>2); + pTmp[2] = (OMX_U8)((L1 + L2 + 1)>>1); + pTmp[3] = (OMX_U8)((L1 + 2*L2 + L3 + 2)>>2); + pTmp[4] = (OMX_U8)((L2 + L3 + 1)>>1); + pTmp[5] = (OMX_U8)((L2 + 3*L3 + 2)>>2); + pTmp[6] = L3; + pTmp[7] = L3; + pTmp[8] = L3; + pTmp[9] = L3; + for (y=0; y<4; y++) + { + for (x=0; x<4; x++) + { + pDst[y*dstStep+x] = pTmp[x+2*y]; + } + } + break; + } + + + + return OMX_Sts_NoErr; +} + +OMXResult HIGHFUNC_PredictIntraTransform_4x4( + const OMX_U8* pFrame, + OMX_U8* pDst, + OMX_INT frameStep, + OMX_INT dstStep, + const OMX_U8 **pCoeff, + const OMX_U8 *totalCoeff, + const OMX_U8* modeAvail, + OMX_U32 qpY +) { +#if H264BSD_N64_INTRA + uint32_t totalCoeffMask = totalCoeff ? h264bsdTotalCoeffMask(totalCoeff) : 0; + + rsph264_queue_set_packed_delta_buffer_if_changed(0, *pCoeff); + rsph264_queue_process_luma_intra4_residual(0, + pFrame, pDst, frameStep, dstStep, + modeAvail, qpY, totalCoeffMask); + // rsph264_sync(); + return OMX_Sts_NoErr; +#else + OMX_S32 availability; + OMXVCM4P10Intra4x4PredMode mode; + OMXResult omxRes; + static const OMX_U32 offsetBlockX[16] = + { 0, 4, 0, 4, 8, 12, 8, 12, 0, 4, 0, 4, 8, 12, 8, 12 }; + static const OMX_U32 offsetBlockY[16] = + { 0, 0, 4, 4, 0, 0, 4, 4, 8, 8, 12, 12, 8, 8, 12, 12 }; + + for (int block = 0; block < 16; block++, totalCoeff++) { + int x = offsetBlockX[block]; + int y = offsetBlockY[block]; + int width = frameStep; + OMX_U8 *p = pDst + y*dstStep + x; + const OMX_U8 *l, *a, *al; + + if (y == 0) + a = pFrame - width + x; + else + a = p-dstStep; + + if (x == 0) + l = pFrame + y * width -1; + else + { + l = p-1; + width = dstStep; + } + + if (x == 0) + al = l-width; + else + al = a-1; + + availability = modeAvail[block] & 0xF; + if (availability & (1<<2)) availability |= 1<<5; + if (availability & (1<<3)) availability |= 1<<6; + availability &= ~((1<<2)|(1<<3)); + + mode = modeAvail[block] >> 4; + + omxRes = omxVCM4P10_PredictIntra_4x4( l, + a, + al, + p, + width, + dstStep, + mode, + availability); + if (omxRes != OMX_Sts_NoErr) + return omxRes; + + + if (*totalCoeff) + { + omxRes = omxVCM4P10_DequantTransformResidualFromPairAndAdd( + pCoeff, + p, + NULL, + p, + dstStep, + dstStep, + qpY, + *totalCoeff); + if (omxRes != OMX_Sts_NoErr) + return omxRes; + } + } + return OMX_Sts_NoErr; +#endif +} diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_TransformDequantChromaDCFromPair.c b/src/video/h264_decoder/omxdl/omxVCM4P10_TransformDequantChromaDCFromPair.c new file mode 100644 index 000000000..b437b07ef --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_TransformDequantChromaDCFromPair.c @@ -0,0 +1,146 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: omxVCM4P10_TransformDequantChromaDCFromPair.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 inverse quantize and transform module + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +#ifdef H264BSD_N64 +#include "../../rsph264_internal.h" +#endif + +/* + * Description: + * Dequantize Chroma 2x2 DC block + */ + +static void DequantChromaDC2x2( + OMX_S16* pDst, + OMX_INT QP +) +{ + int Shift = (QP/6)-1 ; + int Scale = armVCM4P10_VMatrix[QP%6][0]; + int i, Value; + + if (Shift >= 0) + { + for (i=0; i<4; i++) + { + Value = (pDst[i] * Scale) << Shift; + pDst[i] = (OMX_S16)Value; + } + } + else + { + for (i=0; i<4; i++) + { + Value = (pDst[i] * Scale) >> 1; + pDst[i] = (OMX_S16)Value; + } + } +} + + +/* + * Description: + * Inverse Transform DC 2x2 Coefficients + */ + +static void InvTransformDC2x2(OMX_S16* pData) +{ + int c00 = pData[0]; + int c01 = pData[1]; + int c10 = pData[2]; + int c11 = pData[3]; + + int d00 = c00 + c01; + int d01 = c00 - c01; + int d10 = c10 + c11; + int d11 = c10 - c11; + + pData[0] = (OMX_S16)(d00 + d10); + pData[1] = (OMX_S16)(d01 + d11); + pData[2] = (OMX_S16)(d00 - d10); + pData[3] = (OMX_S16)(d01 - d11); +} + + +/** + * Function: omxVCM4P10_TransformDequantChromaDCFromPair (6.3.4.2.2) + * + * Description: + * Reconstruct the 2x2 ChromaDC block from coefficient-position pair buffer, + * perform integer inverse transformation, and dequantization for 2x2 chroma + * DC coefficients, and update the pair buffer pointer to next non-empty + * block. + * + * Input Arguments: + * + * ppSrc - Double pointer to residual coefficient-position pair buffer + * output by CALVC decoding + * QP - Quantization parameter QpC + * + * Output Arguments: + * + * ppSrc - *ppSrc is updated to the start of next non empty block + * pDst - Pointer to the reconstructed 2x2 ChromaDC coefficients buffer; + * must be aligned on a 4-byte boundary. + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - ppSrc or pDst is NULL. + * - pDst is not 4-byte aligned. + * - QP is not in the range of [0-51]. + * + */ + +OMXResult omxVCM4P10_TransformDequantChromaDCFromPair( + const OMX_U8 **ppSrc, + OMX_S16* pDst, + OMX_INT QP + ) +{ +#ifdef H264BSD_N64 + (void)InvTransformDC2x2; + (void)DequantChromaDC2x2; + + rsph264_queue_set_packed_delta_buffer_if_changed(0, *ppSrc); + rsph264_queue_transform_dequant_chromadc(0, pDst, QP); +#else + #ifdef H264BSD_N64 + *ppSrc = rsph264_cur_delta_buffer(*ppSrc); + #endif + + armRetArgErrIf(ppSrc == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(*ppSrc == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(pDst == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot4ByteAligned(pDst), OMX_Sts_BadArgErr); + armRetArgErrIf(QP<0, OMX_Sts_BadArgErr); + armRetArgErrIf(QP>51, OMX_Sts_BadArgErr); + + armVCM4P10_UnpackBlock2x2(ppSrc, pDst); + InvTransformDC2x2(pDst); + DequantChromaDC2x2(pDst, QP); +#endif + return OMX_Sts_NoErr; +} + +/* End of file */ diff --git a/src/video/h264_decoder/omxdl/omxVCM4P10_TransformDequantLumaDCFromPair.c b/src/video/h264_decoder/omxdl/omxVCM4P10_TransformDequantLumaDCFromPair.c new file mode 100644 index 000000000..16c8be1b0 --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxVCM4P10_TransformDequantLumaDCFromPair.c @@ -0,0 +1,148 @@ +/* ---------------------------------------------------------------- + * + * + * File Name: omxVCM4P10_TransformDequantLumaDCFromPair.c + * OpenMAX DL: v1.0.2 + * Revision: 9641 + * Date: Thursday, February 7, 2008 + * + * (c) Copyright 2007-2008 ARM Limited. All Rights Reserved. + * + * + * + * H.264 inverse quantize and transform module + * + */ + +#include "omxtypes.h" +#include "armOMX.h" +#include "omxVC.h" + +#include "armCOMM.h" +#include "armVC.h" + +/* + * Description: + * Dequantize Luma DC block + */ + +static void DequantLumaDC4x4( + OMX_S16* pDst, + OMX_INT QP +) +{ + int Shift = (QP/6)-2 ; + int Scale = armVCM4P10_VMatrix[QP%6][0]; + int i, Round, Value; + + if (Shift >= 0) + { + for (i=0; i<16; i++) + { + Value = (pDst[i] * Scale) << Shift; + pDst[i] = (OMX_S16)Value; + } + } + else + { + Shift = -Shift;; + Round = 1<<(Shift-1); + + for (i=0; i<16; i++) + { + Value = (pDst[i] * Scale + Round) >> Shift; + pDst[i] = (OMX_S16)Value; + } + } +} + + + +/* + * Description: + * Inverse Transform DC 4x4 Coefficients + */ +static void InvTransformDC4x4(OMX_S16* pData) +{ + int i; + + /* Transform rows */ + for (i=0; i<16; i+=4) + { + int c0 = pData[i+0]; + int c1 = pData[i+1]; + int c2 = pData[i+2]; + int c3 = pData[i+3]; + pData[i+0] = (OMX_S16)(c0+c1+c2+c3); + pData[i+1] = (OMX_S16)(c0+c1-c2-c3); + pData[i+2] = (OMX_S16)(c0-c1-c2+c3); + pData[i+3] = (OMX_S16)(c0-c1+c2-c3); + } + + /* Transform columns */ + for (i=0; i<4; i++) + { + int c0 = pData[i+0]; + int c1 = pData[i+4]; + int c2 = pData[i+8]; + int c3 = pData[i+12]; + pData[i+0] = (OMX_S16)(c0+c1+c2+c3); + pData[i+4] = (OMX_S16)(c0+c1-c2-c3); + pData[i+8] = (OMX_S16)(c0-c1-c2+c3); + pData[i+12] = (OMX_S16)(c0-c1+c2-c3); + } +} + + +/** + * Function: omxVCM4P10_TransformDequantLumaDCFromPair (6.3.4.2.1) + * + * Description: + * Reconstructs the 4x4 LumaDC block from the coefficient-position pair + * buffer, performs integer inverse, and dequantization for 4x4 LumaDC + * coefficients, and updates the pair buffer pointer to the next non-empty + * block. + * + * Input Arguments: + * + * ppSrc - Double pointer to residual coefficient-position pair buffer + * output by CALVC decoding + * QP - Quantization parameter QpY + * + * Output Arguments: + * + * ppSrc - *ppSrc is updated to the start of next non empty block + * pDst - Pointer to the reconstructed 4x4 LumaDC coefficients buffer; must + * be aligned on a 8-byte boundary. + * + * Return Value: + * OMX_Sts_NoErr, if the function runs without error. + * OMX_Sts_BadArgErr - bad arguments: if one of the following cases occurs: + * - ppSrc or pDst is NULL. + * - pDst is not 8 byte aligned. + * - QP is not in the range of [0-51]. + * + */ + +OMXResult omxVCM4P10_TransformDequantLumaDCFromPair( + const OMX_U8 **ppSrc, + OMX_S16* pDst, + OMX_INT QP + ) +{ + armRetArgErrIf(ppSrc == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(*ppSrc == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(pDst == NULL, OMX_Sts_BadArgErr); + armRetArgErrIf(armNot8ByteAligned(pDst), OMX_Sts_BadArgErr); + armRetArgErrIf(QP<0, OMX_Sts_BadArgErr); + armRetArgErrIf(QP>51, OMX_Sts_BadArgErr); + + armVCM4P10_UnpackBlock4x4(ppSrc, pDst); + /*InvTransformDequantLumaDC4x4(pDst, QP);*/ + InvTransformDC4x4(pDst); + DequantLumaDC4x4(pDst, QP); + + return OMX_Sts_NoErr; +} + +/* End of file */ diff --git a/src/video/h264_decoder/omxdl/omxtypes.h b/src/video/h264_decoder/omxdl/omxtypes.h new file mode 100644 index 000000000..8b295a6fe --- /dev/null +++ b/src/video/h264_decoder/omxdl/omxtypes.h @@ -0,0 +1,252 @@ +/** + * File: omxtypes.h + * Brief: Defines basic Data types used in OpenMAX v1.0.2 header files. + * + * Copyright © 2005-2008 The Khronos Group Inc. All Rights Reserved. + * + * These materials are protected by copyright laws and contain material + * proprietary to the Khronos Group, Inc. You may use these materials + * for implementing Khronos specifications, without altering or removing + * any trademark, copyright or other notice from the specification. + * + * Khronos Group makes no, and expressly disclaims any, representations + * or warranties, express or implied, regarding these materials, including, + * without limitation, any implied warranties of merchantability or fitness + * for a particular purpose or non-infringement of any intellectual property. + * Khronos Group makes no, and expressly disclaims any, warranties, express + * or implied, regarding the correctness, accuracy, completeness, timeliness, + * and reliability of these materials. + * + * Under no circumstances will the Khronos Group, or any of its Promoters, + * Contributors or Members or their respective partners, officers, directors, + * employees, agents or representatives be liable for any damages, whether + * direct, indirect, special or consequential damages for lost revenues, + * lost profits, or otherwise, arising from or in connection with these + * materials. + * + * Khronos and OpenMAX are trademarks of the Khronos Group Inc. + * + */ + +#ifndef _OMXTYPES_H_ +#define _OMXTYPES_H_ + +#include + +#define OMX_IN +#define OMX_OUT +#define OMX_INOUT + + +typedef enum { + + /* Mandatory return codes - use cases are explicitly described for each function */ + OMX_Sts_NoErr = 0, /* No error, the function completed successfully */ + OMX_Sts_Err = -2, /* Unknown/unspecified error */ + OMX_Sts_InvalidBitstreamValErr = -182, /* Invalid value detected during bitstream processing */ + OMX_Sts_MemAllocErr = -9, /* Not enough memory allocated for the operation */ + OMX_StsACAAC_GainCtrErr = -159, /* AAC: Unsupported gain control data detected */ + OMX_StsACAAC_PrgNumErr = -167, /* AAC: Invalid number of elements for one program */ + OMX_StsACAAC_CoefValErr = -163, /* AAC: Invalid quantized coefficient value */ + OMX_StsACAAC_MaxSfbErr = -162, /* AAC: Invalid maxSfb value in relation to numSwb */ + OMX_StsACAAC_PlsDataErr = -160, /* AAC: pulse escape sequence data error */ + + /* Optional return codes - use cases are explicitly described for each function*/ + OMX_Sts_BadArgErr = -5, /* Bad Arguments */ + + OMX_StsACAAC_TnsNumFiltErr = -157, /* AAC: Invalid number of TNS filters */ + OMX_StsACAAC_TnsLenErr = -156, /* AAC: Invalid TNS region length */ + OMX_StsACAAC_TnsOrderErr = -155, /* AAC: Invalid order of TNS filter */ + OMX_StsACAAC_TnsCoefResErr = -154, /* AAC: Invalid bit-resolution for TNS filter coefficients */ + OMX_StsACAAC_TnsCoefErr = -153, /* AAC: Invalid TNS filter coefficients */ + OMX_StsACAAC_TnsDirectErr = -152, /* AAC: Invalid TNS filter direction */ + + OMX_StsICJP_JPEGMarkerErr = -183, /* JPEG marker encountered within an entropy-coded block; */ + /* Huffman decoding operation terminated early. */ + OMX_StsICJP_JPEGMarker = -181, /* JPEG marker encountered; Huffman decoding */ + /* operation terminated early. */ + OMX_StsIPPP_ContextMatchErr = -17, /* Context parameter doesn't match to the operation */ + + OMX_StsSP_EvenMedianMaskSizeErr = -180, /* Even size of the Median Filter mask was replaced by the odd one */ + + OMX_Sts_MaximumEnumeration = INT_MAX /*Placeholder, forces enum of size OMX_INT*/ + + } OMXResult; /** Return value or error value returned from a function. Identical to OMX_INT */ + + +/* OMX_U8 */ +#if UCHAR_MAX == 0xff +typedef unsigned char OMX_U8; +#elif USHRT_MAX == 0xff +typedef unsigned short int OMX_U8; +#else +#error OMX_U8 undefined +#endif + + +/* OMX_S8 */ +#if SCHAR_MAX == 0x7f +typedef signed char OMX_S8; +#elif SHRT_MAX == 0x7f +typedef signed short int OMX_S8; +#else +#error OMX_S8 undefined +#endif + + +/* OMX_U16 */ +#if USHRT_MAX == 0xffff +typedef unsigned short int OMX_U16; +#elif UINT_MAX == 0xffff +typedef unsigned int OMX_U16; +#else +#error OMX_U16 undefined +#endif + + +/* OMX_S16 */ +#if SHRT_MAX == 0x7fff +typedef signed short int OMX_S16; +#elif INT_MAX == 0x7fff +typedef signed int OMX_S16; +#else +#error OMX_S16 undefined +#endif + + +/* OMX_U32 */ +#if UINT_MAX == 0xffffffff +typedef unsigned int OMX_U32; +#elif LONG_MAX == 0xffffffff +typedef unsigned long int OMX_U32; +#else +#error OMX_U32 undefined +#endif + + +/* OMX_S32 */ +#if INT_MAX == 0x7fffffff +typedef signed int OMX_S32; +#elif LONG_MAX == 0x7fffffff +typedef long signed int OMX_S32; +#else +#error OMX_S32 undefined +#endif + + +/* OMX_U64 & OMX_S64 */ +#if defined( _WIN32 ) || defined ( _WIN64 ) + typedef __int64 OMX_S64; /** Signed 64-bit integer */ + typedef unsigned __int64 OMX_U64; /** Unsigned 64-bit integer */ + #define OMX_MIN_S64 (0x8000000000000000i64) + #define OMX_MIN_U64 (0x0000000000000000i64) + #define OMX_MAX_S64 (0x7FFFFFFFFFFFFFFFi64) + #define OMX_MAX_U64 (0xFFFFFFFFFFFFFFFFi64) +#else + typedef long long OMX_S64; /** Signed 64-bit integer */ + typedef unsigned long long OMX_U64; /** Unsigned 64-bit integer */ + #define OMX_MIN_S64 (0x8000000000000000LL) + #define OMX_MIN_U64 (0x0000000000000000LL) + #define OMX_MAX_S64 (0x7FFFFFFFFFFFFFFFLL) + #define OMX_MAX_U64 (0xFFFFFFFFFFFFFFFFLL) +#endif + + +/* OMX_SC8 */ +typedef struct +{ + OMX_S8 Re; /** Real part */ + OMX_S8 Im; /** Imaginary part */ + +} OMX_SC8; /** Signed 8-bit complex number */ + + +/* OMX_SC16 */ +typedef struct +{ + OMX_S16 Re; /** Real part */ + OMX_S16 Im; /** Imaginary part */ + +} OMX_SC16; /** Signed 16-bit complex number */ + + +/* OMX_SC32 */ +typedef struct +{ + OMX_S32 Re; /** Real part */ + OMX_S32 Im; /** Imaginary part */ + +} OMX_SC32; /** Signed 32-bit complex number */ + + +/* OMX_SC64 */ +typedef struct +{ + OMX_S64 Re; /** Real part */ + OMX_S64 Im; /** Imaginary part */ + +} OMX_SC64; /** Signed 64-bit complex number */ + + +/* OMX_F32 */ +typedef float OMX_F32; /** Single precision floating point,IEEE 754 */ + + +/* OMX_F64 */ +typedef double OMX_F64; /** Double precision floating point,IEEE 754 */ + + +/* OMX_INT */ +typedef int OMX_INT; /** signed integer corresponding to machine word length, has maximum signed value INT_MAX*/ + + +#define OMX_MIN_S8 (-128) +#define OMX_MIN_U8 0 +#define OMX_MIN_S16 (-32768) +#define OMX_MIN_U16 0 +#define OMX_MIN_S32 (-2147483647-1) +#define OMX_MIN_U32 0 + +#define OMX_MAX_S8 (127) +#define OMX_MAX_U8 (255) +#define OMX_MAX_S16 (32767) +#define OMX_MAX_U16 (0xFFFF) +#define OMX_MAX_S32 (2147483647) +#define OMX_MAX_U32 (0xFFFFFFFF) + +typedef void OMXVoid; + +#ifndef NULL +#define NULL ((void*)0) +#endif + +/** Defines the geometric position and size of a rectangle, + * where x,y defines the coordinates of the top left corner + * of the rectangle, with dimensions width in the x-direction + * and height in the y-direction */ +typedef struct { + OMX_INT x; /** x-coordinate of top left corner of rectangle */ + OMX_INT y; /** y-coordinate of top left corner of rectangle */ + OMX_INT width; /** Width in the x-direction. */ + OMX_INT height; /** Height in the y-direction. */ +}OMXRect; + + +/** Defines the geometric position of a point, */ +typedef struct +{ + OMX_INT x; /** x-coordinate */ + OMX_INT y; /** y-coordinate */ + +} OMXPoint; + + +/** Defines the dimensions of a rectangle, or region of interest in an image */ +typedef struct +{ + OMX_INT width; /** Width of the rectangle, in the x-direction */ + OMX_INT height; /** Height of the rectangle, in the y-direction */ + +} OMXSize; + +#endif /* _OMXTYPES_H_ */ diff --git a/src/video/h264_decoder/opttarget.h b/src/video/h264_decoder/opttarget.h new file mode 100644 index 000000000..e69de29bb diff --git a/src/video/libdragon.mk b/src/video/libdragon.mk new file mode 100644 index 000000000..73f8ff005 --- /dev/null +++ b/src/video/libdragon.mk @@ -0,0 +1,13 @@ +LIBDRAGON_OBJS += \ + $(BUILD_DIR)/video/video.o \ + $(BUILD_DIR)/video/video_sync.o \ + $(BUILD_DIR)/video/fmv.o \ + $(BUILD_DIR)/video/mpeg1.o \ + $(BUILD_DIR)/video/yuv.o \ + $(BUILD_DIR)/video/rsp_yuv.o \ + $(BUILD_DIR)/video/rsp_mpeg1.o \ + $(BUILD_DIR)/video/h264_decoder.o \ + $(BUILD_DIR)/video/h264.o \ + $(BUILD_DIR)/video/rsph264_inter.o \ + $(BUILD_DIR)/video/rsph264_intra.o \ + $(BUILD_DIR)/video/subtitles.o diff --git a/src/video/mpeg1.c b/src/video/mpeg1.c new file mode 100644 index 000000000..a11e2b39f --- /dev/null +++ b/src/video/mpeg1.c @@ -0,0 +1,256 @@ +/** + * @file mpeg1.c + * @author Giovanni Bajo + */ +#include "mpeg1.h" +#include "n64sys.h" +#include "rspq.h" +#include "rdpq.h" +#include "rdpq_rect.h" +#include "rdpq_mode.h" +#include "yuv.h" +#include "debug.h" +#include "profile.h" +#include "utils.h" +#include "video_internal.h" +#include +#include +#include "mpeg1_internal.h" + +typedef struct mpeg1_s { + video_t video; + plm_buffer_t *buf; + plm_video_t *v; + plm_frame_t *f; + int buffered_pics; +} mpeg1_t; + +typedef enum { + PS_MPEG = PROFILE_SLOT_MPEG1, + PS_MPEG_FINDSTART, + PS_MPEG_HASSTART, + PS_MPEG_DECODESLICE, + PS_MPEG_MB, + PS_MPEG_MB_MV, + PS_MPEG_MB_PREDICT, + PS_MPEG_MB_DECODE, + PS_MPEG_MB_DECODE_DC, + PS_MPEG_MB_DECODE_AC, + PS_MPEG_MB_DECODE_AC_VLC, + PS_MPEG_MB_DECODE_AC_CODE, + PS_MPEG_MB_DECODE_AC_DEQUANT, + PS_MPEG_MB_DECODE_BLOCK, + PS_MPEG_MB_DECODE_BLOCK_IDCT, +} MPEG1ProfileSlot; + +DEFINE_RSP_UCODE(rsp_mpeg1); + +static uint32_t ovl_id; +static bool rsp_mpeg1_initialized = false; + +void rsp_mpeg1_init(void) { + if (rsp_mpeg1_initialized) return; + rspq_init(); + ovl_id = rspq_overlay_register(&rsp_mpeg1); + rsp_mpeg1_initialized = true; +} + +void rsp_mpeg1_close(void) { + if (!rsp_mpeg1_initialized) return; + rspq_overlay_unregister(ovl_id); ovl_id = 0; + rsp_mpeg1_initialized = false; +} + +void rsp_mpeg1_load_matrix(int16_t *mtx) { + assert((PhysicalAddr(mtx) & 7) == 0); + data_cache_hit_writeback(mtx, 8*8*2); + rspq_write(ovl_id, 0x0, PhysicalAddr(mtx)); +} + +void rsp_mpeg1_store_matrix(int16_t *mtx) { + assert((PhysicalAddr(mtx) & 7) == 0); + data_cache_hit_writeback_invalidate(mtx, 8*8*2); + rspq_write(ovl_id, 0x7, PhysicalAddr(mtx)); +} + +void rsp_mpeg1_store_pixels(void) { + rspq_write(ovl_id, 0x1); +} + +void rsp_mpeg1_load_pixels(void) { + rspq_write(ovl_id, 0xC); +} + +void rsp_mpeg1_zero_pixels(void) { + rspq_write(ovl_id, 0xD); +} + +void rsp_mpeg1_idct(void) { + rspq_write(ovl_id, 0x2); +} + +void rsp_mpeg1_block_begin(int block, uint8_t *pixels, int pitch) { + assert((PhysicalAddr(pixels) & 7) == 0); + assert((pitch & 7) == 0); + assert(block == RSP_MPEG1_BLOCK_Y0 || block == RSP_MPEG1_BLOCK_CR || block == RSP_MPEG1_BLOCK_CB); + rspq_write(ovl_id, 0x3, block, PhysicalAddr(pixels), pitch); +} + +void rsp_mpeg1_block_switch_partition(int partition) { + rspq_write(ovl_id, 0xB, partition); +} + +void rsp_mpeg1_block_coeff(int idx, int16_t coeff) { + rspq_write(ovl_id, 0x4, ((idx & 0x3F) << 16) | (uint16_t)coeff); +} + +void rsp_mpeg1_block_dequant(bool intra, int scale) { + rspq_write(ovl_id, 0x5, (int)intra | (scale << 8)); +} + +void rsp_mpeg1_block_decode(int ncoeffs, bool intra) { + rspq_write(ovl_id, 0x6, ncoeffs, intra); +} + +void rsp_mpeg1_block_predict(uint8_t *src, int pitch, bool oddh, bool oddv, bool interpolate) { + rspq_write(ovl_id, 0xA, PhysicalAddr(src), pitch, oddv | (oddh<<1) | (interpolate<<2)); +} + +void rsp_mpeg1_set_quant_matrix(bool intra, const uint8_t quant_mtx[64]) { + uint32_t *qmtx = (uint32_t*)quant_mtx; + rspq_write(ovl_id, 0x8, intra, + qmtx[0], qmtx[1], qmtx[2], qmtx[3], + qmtx[4], qmtx[5], qmtx[6], qmtx[7]); + rspq_write(ovl_id, 0x9, intra, + qmtx[8], qmtx[9], qmtx[10], qmtx[11], + qmtx[12], qmtx[13], qmtx[14], qmtx[15]); +} + +#define PL_MPEG_IMPLEMENTATION +#include "pl_mpeg/pl_mpeg.h" + +static video_t *mpeg1_open(const char *fn, const video_parms_t *parms) { + (void)parms; + mpeg1_t *mp1 = malloc(sizeof(mpeg1_t)); + assertf(mp1, "Out of memory"); + memset(mp1, 0, sizeof(mpeg1_t)); + + rsp_mpeg1_init(); + + mp1->buf = plm_buffer_create_with_filename(fn); + assertf(mp1->buf, "error opening file %s: %s\n", fn, strerror(errno)); + + // In the common case of accessing a movie stream + // from the ROM, disable buffering. This will allow + // the data to flow directly from the DMA into the buffers + // without any intervening memcpy. We keep buffering on + // for other supports like SD cards. + if (strncmp(fn, "rom:/", 5) == 0) { + setvbuf(mp1->buf->fh, NULL, _IONBF, 0); + } + + mp1->v = plm_video_create_with_buffer(mp1->buf, true); + assert(mp1->v); + + mp1->buffered_pics = parms ? parms->buffered_pics : 1; + if (mp1->buffered_pics < 1) mp1->buffered_pics = 1; + plm_video_set_buffered_pics(mp1->v, mp1->buffered_pics); + + // Force decoding of header. This would be done lazily but better do it + // now to catch any errors early. + if (!plm_video_has_header(mp1->v)) { + assertf(0, "invalid header in video stream\n"); + } + + // Fill in information structure + mp1->video.info.width = plm_video_get_width(mp1->v); + mp1->video.info.height = plm_video_get_height(mp1->v); + mp1->video.info.framerate = plm_video_get_framerate(mp1->v); + mp1->video.info.aspect_ratio = (float)plm_video_get_aspect_ratio(mp1->v); + + // MPEG1 does not specify colorspace. Historically, the vast amount of MPEG1 + // streams were encoded using BT.601 standard with TV levels, and that's also + // the default for ffmpeg when encoding MPEG1, so we assume that here. + mp1->video.info.colorspace = YUV_BT601_TV; + + return &mp1->video; +} + +static int mpeg1_poll(video_t *v) { + mpeg1_t *mp1 = (mpeg1_t *)v; + if (mp1->buffered_pics <= 0) + return 0; + return plm_video_poll(mp1->v) ? 1 : 0; +} + +static bool mpeg1_next_frame(video_t *v) { + mpeg1_t *mp1 = (mpeg1_t *)v; + + // Release previous frame (if any). Frames extracted from the output queue stay + // locked until explicitly released. + if (mp1->f) { + plm_video_release_output(mp1->v, (plm_frame_t *)mp1->f); + mp1->f = NULL; + } + + PROFILE_START(PS_MPEG); + mp1->f = plm_video_decode(mp1->v); + PROFILE_STOP(PS_MPEG); + return (mp1->f != NULL); +} + +static void mpeg1_rewind(video_t *v) { + mpeg1_t *mp1 = (mpeg1_t *)v; + if (mp1->f) { + plm_video_release_output(mp1->v, (plm_frame_t *)mp1->f); + mp1->f = NULL; + } + plm_video_rewind(mp1->v); +} + +static yuv_frame_t mpeg1_get_frame(video_t *v) { + mpeg1_t *mp1 = (mpeg1_t *)v; + plm_frame_t *frame = (plm_frame_t*)mp1->f; + surface_t yp = surface_make_linear(frame->y.data, FMT_I8, frame->width, frame->height); + surface_t cbp = surface_make_linear(frame->cb.data, FMT_I8, frame->width/2, frame->height/2); + surface_t crp = surface_make_linear(frame->cr.data, FMT_I8, frame->width/2, frame->height/2); + return (yuv_frame_t){ .y = yp, .u = cbp, .v = crp }; +} + +static void mpeg1_close(video_t *v) { + mpeg1_t *mp1 = (mpeg1_t *)v; + if (mp1->f) { + plm_video_release_output(mp1->v, (plm_frame_t *)mp1->f); + mp1->f = NULL; + } + plm_video_destroy(mp1->v); + free(mp1); +} + +void __mpeg1_profile_init(void) { + profile_register(PS_MPEG, "MPEG", 0); + profile_register(PS_MPEG_FINDSTART, "FindStart", 1); + profile_register(PS_MPEG_HASSTART, "HasStart", 1); + profile_register(PS_MPEG_DECODESLICE, "DecodeSlice", 1); + profile_register(PS_MPEG_MB, "MacroB", 1); + profile_register(PS_MPEG_MB_MV, "MV", 2); + profile_register(PS_MPEG_MB_PREDICT, "Predict", 2); + profile_register(PS_MPEG_MB_DECODE, "Decode", 2); + profile_register(PS_MPEG_MB_DECODE_DC, "DecodeDC", 3); + profile_register(PS_MPEG_MB_DECODE_AC, "DecodeAC", 3); + profile_register(PS_MPEG_MB_DECODE_AC_VLC, "DecodeACVLC", 4); + profile_register(PS_MPEG_MB_DECODE_AC_CODE, "DecodeACCode", 4); + profile_register(PS_MPEG_MB_DECODE_AC_DEQUANT, "DecodeACDeQuant", 4); + profile_register(PS_MPEG_MB_DECODE_BLOCK, "DecodeBlock", 3); + profile_register(PS_MPEG_MB_DECODE_BLOCK_IDCT, "DecodeBlockIDCT", 4); +} + +video_codec_t mpeg1_codec = { + .extension = ".m1v", + .open = mpeg1_open, + .close = mpeg1_close, + .poll = mpeg1_poll, + .next_frame = mpeg1_next_frame, + .get_frame = mpeg1_get_frame, + .rewind = mpeg1_rewind, +}; diff --git a/src/video/mpeg1_internal.h b/src/video/mpeg1_internal.h new file mode 100644 index 000000000..d14247dda --- /dev/null +++ b/src/video/mpeg1_internal.h @@ -0,0 +1,59 @@ +/** + * @file mpeg1_internal.h + * @author Giovanni Bajo + */ +#ifndef __LIBDRAGON_MPEG1_INTERNAL_H +#define __LIBDRAGON_MPEG1_INTERNAL_H + +// The IDCT of pl_mpeg requires about 19 bits of precision +// Since RSP has 16-bit vector registers, we need to scale +// input data. This macro decides by how much. +// 3 is the minimum value that does not seem to produce +// artifacts in videos. +#define RSP_IDCT_SCALER 3 + +// Usage of RSP in MPEG-1 player: +// 0: None (full CPU) +// 1: IDCT+Residual +// 2: Dequant+IDCT+Residual +// 3: Dequant+IDCT+Residual+Prediction +#define RSP_MODE 3 + +#define ASSERT_UNDEFINED_BLOCK 0x0001 +#define ASSERT_UNDEFINED_BLOCK2 0x0002 +#define ASSERT_UNDEFINED_BLOCK3 0x0003 +#define ASSERT_UNDEFINED_BLOCK4 0x0004 +#define ASSERT_UNDEFINED_BLOCK5 0x0005 +#define ASSERT_UNDEFINED_BLOCK6 0x0006 +#define ASSERT_PIXELCHECK(n) (0x0010+n) + +#define RSP_MPEG1_BLOCK_Y0 0 +#define RSP_MPEG1_BLOCK_Y1 1 +#define RSP_MPEG1_BLOCK_Y2 2 +#define RSP_MPEG1_BLOCK_Y3 3 +#define RSP_MPEG1_BLOCK_CB 4 +#define RSP_MPEG1_BLOCK_CR 5 + +#ifndef __ASSEMBLER__ +#include "pl_mpeg/pl_mpeg.h" + +void rsp_mpeg1_init(void); +void rsp_mpeg1_close(void); +void rsp_mpeg1_load_matrix(int16_t *mtx); +void rsp_mpeg1_store_matrix(int16_t *mtx); +void rsp_mpeg1_zero_pixels(void); +void rsp_mpeg1_load_pixels(void); +void rsp_mpeg1_store_pixels(void); +void rsp_mpeg1_idct(void); +void rsp_mpeg1_block_begin(int block, uint8_t *pixels, int pitch); +void rsp_mpeg1_block_switch_partition(int partition); +void rsp_mpeg1_block_coeff(int idx, int16_t coeff); +void rsp_mpeg1_block_dequant(bool intra, int scale); +void rsp_mpeg1_block_decode(int ncoeffs, bool intra); +void rsp_mpeg1_set_quant_matrix(bool intra, const uint8_t quant_mtx[64]); +void rsp_mpeg1_block_predict(uint8_t *src, int pitch, bool oddh, bool oddv, bool interpolate); +void rsp_mpeg1_block_split(void); + +#endif + +#endif diff --git a/src/video/pl_mpeg/makevlc.py b/src/video/pl_mpeg/makevlc.py new file mode 100644 index 000000000..8c5d64b86 --- /dev/null +++ b/src/video/pl_mpeg/makevlc.py @@ -0,0 +1,239 @@ +#!/bin/python3 +# This script is kept as documentation, it is not used by the code. +# It shows how the VLC tables are generated for AC coefficient decoding. +# Original pl_mpeg decodes huffman codes via bit-tables, which is hugely +# inefficient. AC decoding is by far the biggest CPU hog in our MPEG1 +# implementation (after much of the rest was moved to RSP), so we optimize +# it by switching that specific table (PLM_VIDEO_DCT_COEFF) into byte tables +# for faster decoding. This script generates the byte tables. + +PLM_VIDEO_DCT_COEFF = [ + [ 1 << 1, 0], [ 0, 0x0001], # 0: x + [ 2 << 1, 0], [ 3 << 1, 0], # 1: 0x + [ 4 << 1, 0], [ 5 << 1, 0], # 2: 00x + [ 6 << 1, 0], [ 0, 0x0101], # 3: 01x + [ 7 << 1, 0], [ 8 << 1, 0], # 4: 000x + [ 9 << 1, 0], [ 10 << 1, 0], # 5: 001x + [ 0, 0x0002], [ 0, 0x0201], # 6: 010x + [ 11 << 1, 0], [ 12 << 1, 0], # 7: 0000x + [ 13 << 1, 0], [ 14 << 1, 0], # 8: 0001x + [ 15 << 1, 0], [ 0, 0x0003], # 9: 0010x + [ 0, 0x0401], [ 0, 0x0301], #10: 0011x + [ 16 << 1, 0], [ 0, 0xffff], #11: 0000 0x + [ 17 << 1, 0], [ 18 << 1, 0], #12: 0000 1x + [ 0, 0x0701], [ 0, 0x0601], #13: 0001 0x + [ 0, 0x0102], [ 0, 0x0501], #14: 0001 1x + [ 19 << 1, 0], [ 20 << 1, 0], #15: 0010 0x + [ 21 << 1, 0], [ 22 << 1, 0], #16: 0000 00x + [ 0, 0x0202], [ 0, 0x0901], #17: 0000 10x + [ 0, 0x0004], [ 0, 0x0801], #18: 0000 11x + [ 23 << 1, 0], [ 24 << 1, 0], #19: 0010 00x + [ 25 << 1, 0], [ 26 << 1, 0], #20: 0010 01x + [ 27 << 1, 0], [ 28 << 1, 0], #21: 0000 000x + [ 29 << 1, 0], [ 30 << 1, 0], #22: 0000 001x + [ 0, 0x0d01], [ 0, 0x0006], #23: 0010 000x + [ 0, 0x0c01], [ 0, 0x0b01], #24: 0010 001x + [ 0, 0x0302], [ 0, 0x0103], #25: 0010 010x + [ 0, 0x0005], [ 0, 0x0a01], #26: 0010 011x + [ 31 << 1, 0], [ 32 << 1, 0], #27: 0000 0000x + [ 33 << 1, 0], [ 34 << 1, 0], #28: 0000 0001x + [ 35 << 1, 0], [ 36 << 1, 0], #29: 0000 0010x + [ 37 << 1, 0], [ 38 << 1, 0], #30: 0000 0011x + [ 39 << 1, 0], [ 40 << 1, 0], #31: 0000 0000 0x + [ 41 << 1, 0], [ 42 << 1, 0], #32: 0000 0000 1x + [ 43 << 1, 0], [ 44 << 1, 0], #33: 0000 0001 0x + [ 45 << 1, 0], [ 46 << 1, 0], #34: 0000 0001 1x + [ 0, 0x1001], [ 0, 0x0502], #35: 0000 0010 0x + [ 0, 0x0007], [ 0, 0x0203], #36: 0000 0010 1x + [ 0, 0x0104], [ 0, 0x0f01], #37: 0000 0011 0x + [ 0, 0x0e01], [ 0, 0x0402], #38: 0000 0011 1x + [ 47 << 1, 0], [ 48 << 1, 0], #39: 0000 0000 00x + [ 49 << 1, 0], [ 50 << 1, 0], #40: 0000 0000 01x + [ 51 << 1, 0], [ 52 << 1, 0], #41: 0000 0000 10x + [ 53 << 1, 0], [ 54 << 1, 0], #42: 0000 0000 11x + [ 55 << 1, 0], [ 56 << 1, 0], #43: 0000 0001 00x + [ 57 << 1, 0], [ 58 << 1, 0], #44: 0000 0001 01x + [ 59 << 1, 0], [ 60 << 1, 0], #45: 0000 0001 10x + [ 61 << 1, 0], [ 62 << 1, 0], #46: 0000 0001 11x + [ -1, 0], [ 63 << 1, 0], #47: 0000 0000 000x + [ 64 << 1, 0], [ 65 << 1, 0], #48: 0000 0000 001x + [ 66 << 1, 0], [ 67 << 1, 0], #49: 0000 0000 010x + [ 68 << 1, 0], [ 69 << 1, 0], #50: 0000 0000 011x + [ 70 << 1, 0], [ 71 << 1, 0], #51: 0000 0000 100x + [ 72 << 1, 0], [ 73 << 1, 0], #52: 0000 0000 101x + [ 74 << 1, 0], [ 75 << 1, 0], #53: 0000 0000 110x + [ 76 << 1, 0], [ 77 << 1, 0], #54: 0000 0000 111x + [ 0, 0x000b], [ 0, 0x0802], #55: 0000 0001 000x + [ 0, 0x0403], [ 0, 0x000a], #56: 0000 0001 001x + [ 0, 0x0204], [ 0, 0x0702], #57: 0000 0001 010x + [ 0, 0x1501], [ 0, 0x1401], #58: 0000 0001 011x + [ 0, 0x0009], [ 0, 0x1301], #59: 0000 0001 100x + [ 0, 0x1201], [ 0, 0x0105], #60: 0000 0001 101x + [ 0, 0x0303], [ 0, 0x0008], #61: 0000 0001 110x + [ 0, 0x0602], [ 0, 0x1101], #62: 0000 0001 111x + [ 78 << 1, 0], [ 79 << 1, 0], #63: 0000 0000 0001x + [ 80 << 1, 0], [ 81 << 1, 0], #64: 0000 0000 0010x + [ 82 << 1, 0], [ 83 << 1, 0], #65: 0000 0000 0011x + [ 84 << 1, 0], [ 85 << 1, 0], #66: 0000 0000 0100x + [ 86 << 1, 0], [ 87 << 1, 0], #67: 0000 0000 0101x + [ 88 << 1, 0], [ 89 << 1, 0], #68: 0000 0000 0110x + [ 90 << 1, 0], [ 91 << 1, 0], #69: 0000 0000 0111x + [ 0, 0x0a02], [ 0, 0x0902], #70: 0000 0000 1000x + [ 0, 0x0503], [ 0, 0x0304], #71: 0000 0000 1001x + [ 0, 0x0205], [ 0, 0x0107], #72: 0000 0000 1010x + [ 0, 0x0106], [ 0, 0x000f], #73: 0000 0000 1011x + [ 0, 0x000e], [ 0, 0x000d], #74: 0000 0000 1100x + [ 0, 0x000c], [ 0, 0x1a01], #75: 0000 0000 1101x + [ 0, 0x1901], [ 0, 0x1801], #76: 0000 0000 1110x + [ 0, 0x1701], [ 0, 0x1601], #77: 0000 0000 1111x + [ 92 << 1, 0], [ 93 << 1, 0], #78: 0000 0000 0001 0x + [ 94 << 1, 0], [ 95 << 1, 0], #79: 0000 0000 0001 1x + [ 96 << 1, 0], [ 97 << 1, 0], #80: 0000 0000 0010 0x + [ 98 << 1, 0], [ 99 << 1, 0], #81: 0000 0000 0010 1x + [100 << 1, 0], [101 << 1, 0], #82: 0000 0000 0011 0x + [102 << 1, 0], [103 << 1, 0], #83: 0000 0000 0011 1x + [ 0, 0x001f], [ 0, 0x001e], #84: 0000 0000 0100 0x + [ 0, 0x001d], [ 0, 0x001c], #85: 0000 0000 0100 1x + [ 0, 0x001b], [ 0, 0x001a], #86: 0000 0000 0101 0x + [ 0, 0x0019], [ 0, 0x0018], #87: 0000 0000 0101 1x + [ 0, 0x0017], [ 0, 0x0016], #88: 0000 0000 0110 0x + [ 0, 0x0015], [ 0, 0x0014], #89: 0000 0000 0110 1x + [ 0, 0x0013], [ 0, 0x0012], #90: 0000 0000 0111 0x + [ 0, 0x0011], [ 0, 0x0010], #91: 0000 0000 0111 1x + [104 << 1, 0], [105 << 1, 0], #92: 0000 0000 0001 00x + [106 << 1, 0], [107 << 1, 0], #93: 0000 0000 0001 01x + [108 << 1, 0], [109 << 1, 0], #94: 0000 0000 0001 10x + [110 << 1, 0], [111 << 1, 0], #95: 0000 0000 0001 11x + [ 0, 0x0028], [ 0, 0x0027], #96: 0000 0000 0010 00x + [ 0, 0x0026], [ 0, 0x0025], #97: 0000 0000 0010 01x + [ 0, 0x0024], [ 0, 0x0023], #98: 0000 0000 0010 10x + [ 0, 0x0022], [ 0, 0x0021], #99: 0000 0000 0010 11x + [ 0, 0x0020], [ 0, 0x010e], #100: 0000 0000 0011 00x + [ 0, 0x010d], [ 0, 0x010c], #101: 0000 0000 0011 01x + [ 0, 0x010b], [ 0, 0x010a], #102: 0000 0000 0011 10x + [ 0, 0x0109], [ 0, 0x0108], #103: 0000 0000 0011 11x + [ 0, 0x0112], [ 0, 0x0111], #104: 0000 0000 0001 000x + [ 0, 0x0110], [ 0, 0x010f], #105: 0000 0000 0001 001x + [ 0, 0x0603], [ 0, 0x1002], #106: 0000 0000 0001 010x + [ 0, 0x0f02], [ 0, 0x0e02], #107: 0000 0000 0001 011x + [ 0, 0x0d02], [ 0, 0x0c02], #108: 0000 0000 0001 100x + [ 0, 0x0b02], [ 0, 0x1f01], #109: 0000 0000 0001 101x + [ 0, 0x1e01], [ 0, 0x1d01], #110: 0000 0000 0001 110x + [ 0, 0x1c01], [ 0, 0x1b01], #111: 0000 0000 0001 111x +] + +# Lookup a code of 8 bits into a bit table. +# Return: +# Positive number: 16-bit value, where the lowest 13 bits are the symbol +# found during the lookup and the highest 3 bits are +# the number of consumed code bits to find the symbol. +# Negative number: Index into the table to continue the lookup, when no +# symbol was found (and 8 code bits were consumed). +# None: Invalid sequence of code bits that should not happen in +# well formed files. +def lookup8(table, code, idx=0): + for i in range(8): + idx += ((code >> 7) & 1) + code <<= 1 + if table[idx][0] == -1: + return None + if table[idx][0] == 0: + sym = table[idx][1] + assert sym & 0xE000 == 0 or sym == 0xFFFF + return sym | (i << 13) + idx = table[idx][0] + return -idx + +qtables = [] + +# Build the quick lookup tables. The tables are stored in qtables, which is +# an array of tables. qtables[0] is the first table, while other tables are +# allocated lazily, as necessary, when code words of more than 8 bits are found. +# Each table is an array of 256 entries, where each entry is either: +# 0: Invalid code +# >0: 16-bit value, where the lowest 13 bits is the symbol, and the top 3 bits +# is the number of code bits consumed to find the symbol (minus 1). +# <0: Index into qtables of the next table to continue lookup. +# +# NOTE: there is a special case for 0xffff (65535). This is the only symbol that +# doesn't fit in 13-bits. In this case, we put 0xffff in the table (positive value), +# and the decoder will have to know that the code word (0b000001) for 0xffff is +# exactly 6 bits long (you can search for it in the above PLM_VIDEO_DCT_COEFF table). +def build_table(table, idx=0): + tidx = len(qtables) + qtables.append([None]*256) + qt = qtables[-1] + for code in range(256): + value = lookup8(table, code, idx) + if value is None: + qt[code] = 0 + elif value > 0: + qt[code] = value + else: + qt[code] = -build_table(table, -value) + return tidx + +# Build the tables for PLM_VIDEO_DCT_COEFF +build_table(PLM_VIDEO_DCT_COEFF) + +# Dump the qtables +#print(len(qtables)) +# for qt in qtables: +# print(qt) + +# Perform a fast lookup of a 16-bit code using the fast byte tables. +# Returns tuple (bitlen, symbol), or None if the code is invalid +def fast_lookup(qtables, code): + assert code < 65536 + code0 = code >> 8 + code1 = code & 0xFF + # Hack #1: an initial 1 bit is the code for symbol "1", which is encoded + # with a 50% probability so it has to be extremely common. Handle it with + # an explicit if check. This also means that we will not need to use + # half of the first qtable. + if code0 >= 128: + return (1, 1) + # Hack #2: even without looking up the tables, we know that codewords requiring + # a second table lookup are *exactly* 0x00 .. 0x03. This was probably designed + # by MPEG so that fast byte lookups could use this shortcut. + if code0 < 4: + # Lookup the second byte into one of the other tables + value = qtables[code0+1][code1] + n = 8 + else: + # Lookup the first byte into the first table + value = qtables[0][code0] + n = 0 + if value == 0: + return None # invalid code + # Hack #3: as explained above, 0xffff is a special case, in which the decoder + # needs to know the exact length. We know it is 6 bits, so we can return it. + if value == 65535: + return (6, 0xffff) + # Return the code + return (n+1+(value>>13), value & 0x1fff) + +# Perform a slow lookup of a 16-bit code using the slow bit tables +# This is basically a Python version of plm_buffer_read_vlc_bits. +# Returns tuple (bitlen, code), or None for an invalid symbol. +def slow_lookup(table, code): + assert code < 65536 + idx = 0 + for i in range(16): + idx += ((code >> 15) & 1) + code <<= 1 + if table[idx][0] == -1: + return None + if table[idx][0] == 0: + return (i+1, table[idx][1]) + idx = table[idx][0] + assert 0 + +# Make sure our fast tables work. Compare slow and fast lookup for all +# 16-bit values. +for code in range(65536): + sym0 = fast_lookup(qtables, code) + sym1 = slow_lookup(PLM_VIDEO_DCT_COEFF, code) + assert sym0 == sym1 + +print("OK") diff --git a/src/video/pl_mpeg/pl_mpeg.h b/src/video/pl_mpeg/pl_mpeg.h new file mode 100644 index 000000000..16410b425 --- /dev/null +++ b/src/video/pl_mpeg/pl_mpeg.h @@ -0,0 +1,4881 @@ +/* +PL_MPEG - MPEG1 Video decoder, MP2 Audio decoder, MPEG-PS demuxer + +Dominic Szablewski - https://phoboslab.org + + +-- LICENSE: The MIT License(MIT) + +Copyright(c) 2019 Dominic Szablewski + +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. + + + + +-- Synopsis + +// Define `PL_MPEG_IMPLEMENTATION` in *one* C/C++ file before including this +// library to create the implementation. + +#define PL_MPEG_IMPLEMENTATION +#include "plmpeg.h" + +// This function gets called for each decoded video frame +void my_video_callback(plm_t *plm, plm_frame_t *frame, void *user) { + // Do something with frame->y.data, frame->cr.data, frame->cb.data +} + +// This function gets called for each decoded audio frame +void my_audio_callback(plm_t *plm, plm_samples_t *frame, void *user) { + // Do something with samples->interleaved +} + +// Load a .mpg (MPEG Program Stream) file +plm_t *plm = plm_create_with_filename("some-file.mpg"); + +// Install the video & audio decode callbacks +plm_set_video_decode_callback(plm, my_video_callback, my_data); +plm_set_audio_decode_callback(plm, my_audio_callback, my_data); + + +// Decode +do { + plm_decode(plm, time_since_last_call); +} while (!plm_has_ended(plm)); + +// All done +plm_destroy(plm); + + + +-- Documentation + +This library provides several interfaces to load, demux and decode MPEG video +and audio data. A high-level API combines the demuxer, video & audio decoders +in an easy to use wrapper. + +Lower-level APIs for accessing the demuxer, video decoder and audio decoder, +as well as providing different data sources are also available. + +Interfaces are written in an object orientet style, meaning you create object +instances via various different constructor functions (plm_*create()), +do some work on them and later dispose them via plm_*destroy(). + +plm_* ......... the high-level interface, combining demuxer and decoders +plm_buffer_* .. the data source used by all interfaces +plm_demux_* ... the MPEG-PS demuxer +plm_video_* ... the MPEG1 Video ("mpeg1") decoder +plm_audio_* ... the MPEG1 Audio Layer II ("mp2") decoder + + +With the high-level interface you have two options to decode video & audio: + + 1. Use plm_decode() and just hand over the delta time since the last call. + It will decode everything needed and call your callbacks (specified through + plm_set_{video|audio}_decode_callback()) any number of times. + + 2. Use plm_decode_video() and plm_decode_audio() to decode exactly one + frame of video or audio data at a time. How you handle the synchronization + of both streams is up to you. + +If you only want to decode video *or* audio through these functions, you should +disable the other stream (plm_set_{video|audio}_enabled(FALSE)) + +Video data is decoded into a struct with all 3 planes (Y, Cr, Cb) stored in +separate buffers. You can either convert this to RGB on the CPU (slow) via the +plm_frame_to_rgb() function or do it on the GPU with the following matrix: + +mat4 bt601 = mat4( + 1.16438, 0.00000, 1.59603, -0.87079, + 1.16438, -0.39176, -0.81297, 0.52959, + 1.16438, 2.01723, 0.00000, -1.08139, + 0, 0, 0, 1 +); +gl_FragColor = vec4(y, cb, cr, 1.0) * bt601; + +Audio data is decoded into a struct with either one single float array with the +samples for the left and right channel interleaved, or if the +PLM_AUDIO_SEPARATE_CHANNELS is defined *before* including this library, into +two separate float arrays - one for each channel. + + +Data can be supplied to the high level interface, the demuxer and the decoders +in three different ways: + + 1. Using plm_create_from_filename() or with a file handle with + plm_create_from_file(). + + 2. Using plm_create_with_memory() and supplying a pointer to memory that + contains the whole file. + + 3. Using plm_create_with_buffer(), supplying your own plm_buffer_t instance and + periodically writing to this buffer. + +When using your own plm_buffer_t instance, you can fill this buffer using +plm_buffer_write(). You can either monitor plm_buffer_get_remaining() and push +data when appropriate, or install a callback on the buffer with +plm_buffer_set_load_callback() that gets called whenever the buffer needs more +data. + +A buffer created with plm_buffer_create_with_capacity() is treated as a ring +buffer, meaning that data that has already been read, will be discarded. In +contrast, a buffer created with plm_buffer_create_for_appending() will keep all +data written to it in memory. This enables seeking in the already loaded data. + + +There should be no need to use the lower level plm_demux_*, plm_video_* and +plm_audio_* functions, if all you want to do is read/decode an MPEG-PS file. +However, if you get raw mpeg1video data or raw mp2 audio data from a different +source, these functions can be used to decode the raw data directly. Similarly, +if you only want to analyze an MPEG-PS file or extract raw video or audio +packets from it, you can use the plm_demux_* functions. + + +This library uses malloc(), realloc() and free() to manage memory. Typically +all allocation happens up-front when creating the interface. However, the +default buffer size may be too small for certain inputs. In these cases plmpeg +will realloc() the buffer with a larger size whenever needed. You can configure +the default buffer size by defining PLM_BUFFER_DEFAULT_SIZE *before* +including this library. + + +See below for detailed the API documentation. + +*/ + + +#ifndef PL_MPEG_H +#define PL_MPEG_H + +#include +#include + +#ifdef N64 +#include "../profile.h" +#include +#else +#define memalign(a, sz) malloc(sz) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// ----------------------------------------------------------------------------- +// Public Data Types + + +// Object types for the various interfaces + +typedef struct plm_t plm_t; +typedef struct plm_buffer_t plm_buffer_t; +typedef struct plm_demux_t plm_demux_t; +typedef struct plm_video_t plm_video_t; +typedef struct plm_audio_t plm_audio_t; + + +// Demuxed MPEG PS packet +// The type maps directly to the various MPEG-PES start codes. PTS is the +// presentation time stamp of the packet in seconds. Note that not all packets +// have a PTS value, indicated by PLM_PACKET_INVALID_TS. + +#define PLM_PACKET_INVALID_TS -1 + +typedef struct { + int type; + double pts; + size_t length; + uint8_t *data; +} plm_packet_t; + + +// Decoded Video Plane +// The byte length of the data is width * height. Note that different planes +// have different sizes: the Luma plane (Y) is double the size of each of +// the two Chroma planes (Cr, Cb) - i.e. 4 times the byte length. +// Also note that the size of the plane does *not* denote the size of the +// displayed frame. The sizes of planes are always rounded up to the nearest +// macroblock (16px). + +typedef struct { + unsigned int width; + unsigned int height; + uint8_t *data; +} plm_plane_t; + + +// Decoded Video Frame +// width and height denote the desired display size of the frame. This may be +// different from the internal size of the 3 planes. + +typedef struct { + double time; + unsigned int width; + unsigned int height; + plm_plane_t y; + plm_plane_t cr; + plm_plane_t cb; +} plm_frame_t; + + +// Callback function type for decoded video frames used by the high-level +// plm_* interface + +typedef void(*plm_video_decode_callback) + (plm_t *self, plm_frame_t *frame, void *user); + + +// Decoded Audio Samples +// Samples are stored as normalized (-1, 1) float either interleaved, or if +// PLM_AUDIO_SEPARATE_CHANNELS is defined, in two separate arrays. +// The `count` is always PLM_AUDIO_SAMPLES_PER_FRAME and just there for +// convenience. + +#define PLM_AUDIO_SAMPLES_PER_FRAME 1152 + +typedef struct { + double time; + unsigned int count; + #ifdef PLM_AUDIO_SEPARATE_CHANNELS + float left[PLM_AUDIO_SAMPLES_PER_FRAME]; + float right[PLM_AUDIO_SAMPLES_PER_FRAME]; + #else + float interleaved[PLM_AUDIO_SAMPLES_PER_FRAME * 2]; + #endif +} plm_samples_t; + + +// Callback function type for decoded audio samples used by the high-level +// plm_* interface + +typedef void(*plm_audio_decode_callback) + (plm_t *self, plm_samples_t *samples, void *user); + + +// Callback function for plm_buffer when it needs more data + +typedef void(*plm_buffer_load_callback)(plm_buffer_t *self, void *user); + + + +// ----------------------------------------------------------------------------- +// plm_* public API +// High-Level API for loading/demuxing/decoding MPEG-PS data + + +// Create a plmpeg instance with a filename. Returns NULL if the file could not +// be opened. + +plm_t *plm_create_with_filename(const char *filename); + + +// Create a plmpeg instance with a file handle. Pass TRUE to close_when_done to +// let plmpeg call fclose() on the handle when plm_destroy() is called. + +plm_t *plm_create_with_file(FILE *fh, int close_when_done); + + +// Create a plmpeg instance with a pointer to memory as source. This assumes the +// whole file is in memory. The memory is not copied. Pass TRUE to +// free_when_done to let plmpeg call free() on the pointer when plm_destroy() +// is called. + +plm_t *plm_create_with_memory(uint8_t *bytes, size_t length, int free_when_done); + + +// Create a plmpeg instance with a plm_buffer as source. Pass TRUE to +// destroy_when_done to let plmpeg call plm_buffer_destroy() on the buffer when +// plm_destroy() is called. + +plm_t *plm_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done); + + +// Destroy a plmpeg instance and free all data. + +void plm_destroy(plm_t *self); + + +// Get whether we have headers on all available streams and we can accurately +// report the number of video/audio streams, video dimensions, framerate and +// audio samplerate. +// This returns FALSE if the file is not an MPEG-PS file or - when not using a +// file as source - when not enough data is available yet. + +int plm_has_headers(plm_t *self); + + +// Get or set whether video decoding is enabled. Default TRUE. + +int plm_get_video_enabled(plm_t *self); +void plm_set_video_enabled(plm_t *self, int enabled); + + +// Get the number of video streams (0--1) reported in the system header. + +int plm_get_num_video_streams(plm_t *self); + + +// Get the display width/height of the video stream. + +int plm_get_width(plm_t *self); +int plm_get_height(plm_t *self); + + +// Get the framerate of the video stream in frames per second. + +double plm_get_framerate(plm_t *self); + + +// Get or set whether audio decoding is enabled. Default TRUE. + +int plm_get_audio_enabled(plm_t *self); +void plm_set_audio_enabled(plm_t *self, int enabled); + + +// Get the number of audio streams (0--4) reported in the system header. + +int plm_get_num_audio_streams(plm_t *self); + + +// Set the desired audio stream (0--3). Default 0. + +void plm_set_audio_stream(plm_t *self, int stream_index); + + +// Get the samplerate of the audio stream in samples per second. + +int plm_get_samplerate(plm_t *self); + + +// Get or set the audio lead time in seconds - the time in which audio samples +// are decoded in advance (or behind) the video decode time. Typically this +// should be set to the duration of the buffer of the audio API that you use +// for output. E.g. for SDL2: (SDL_AudioSpec.samples / samplerate) + +double plm_get_audio_lead_time(plm_t *self); +void plm_set_audio_lead_time(plm_t *self, double lead_time); + + +// Get the current internal time in seconds. + +double plm_get_time(plm_t *self); + + +// Get the video duration of the underlying source in seconds. + +double plm_get_duration(plm_t *self); + + +// Rewind all buffers back to the beginning. + +void plm_rewind(plm_t *self); + + +// Get or set looping. Default FALSE. + +int plm_get_loop(plm_t *self); +void plm_set_loop(plm_t *self, int loop); + + +// Get whether the file has ended. If looping is enabled, this will always +// return FALSE. + +int plm_has_ended(plm_t *self); + + +// Set the callback for decoded video frames used with plm_decode(). If no +// callback is set, video data will be ignored and not be decoded. The *user +// Parameter will be passed to your callback. + +void plm_set_video_decode_callback(plm_t *self, plm_video_decode_callback fp, void *user); + + +// Set the callback for decoded audio samples used with plm_decode(). If no +// callback is set, audio data will be ignored and not be decoded. The *user +// Parameter will be passed to your callback. + +void plm_set_audio_decode_callback(plm_t *self, plm_audio_decode_callback fp, void *user); + + +// Advance the internal timer by seconds and decode video/audio up to this time. +// This will call the video_decode_callback and audio_decode_callback any number +// of times. A frame-skip is not implemented, i.e. everything up to current time +// will be decoded. + +void plm_decode(plm_t *self, double seconds); + + +// Decode and return one video frame. Returns NULL if no frame could be decoded +// (either because the source ended or data is corrupt). If you only want to +// decode video, you should disable audio via plm_set_audio_enabled(). +// The returned plm_frame_t is valid until the next call to plm_decode_video() +// or until plm_destroy() is called. + +plm_frame_t *plm_decode_video(plm_t *self); + + +// Decode and return one audio frame. Returns NULL if no frame could be decoded +// (either because the source ended or data is corrupt). If you only want to +// decode audio, you should disable video via plm_set_video_enabled(). +// The returned plm_samples_t is valid until the next call to plm_decode_audio() +// or until plm_destroy() is called. + +plm_samples_t *plm_decode_audio(plm_t *self); + + +// Seek to the specified time, clamped between 0 -- duration. This can only be +// used when the underlying plm_buffer is seekable, i.e. for files, fixed +// memory buffers or _for_appending buffers. +// If seek_exact is TRUE this will seek to the exact time, otherwise it will +// seek to the last intra frame just before the desired time. Exact seeking can +// be slow, because all frames up to the seeked one have to be decoded on top of +// the previous intra frame. +// If seeking succeeds, this function will call the video_decode_callback +// exactly once with the target frame. If audio is enabled, it will also call +// the audio_decode_callback any number of times, until the audio_lead_time is +// satisfied. +// Returns TRUE if seeking succeeded or FALSE if no frame could be found. + +int plm_seek(plm_t *self, double time, int seek_exact); + + +// Similar to plm_seek(), but will not call the video_decode_callback, +// audio_decode_callback or make any attempts to sync audio. +// Returns the found frame or NULL if no frame could be found. + +plm_frame_t *plm_seek_frame(plm_t *self, double time, int seek_exact); + + + +// ----------------------------------------------------------------------------- +// plm_buffer public API +// Provides the data source for all other plm_* interfaces + + +// The default size for buffers created from files or by the high-level API + +#ifndef PLM_BUFFER_DEFAULT_SIZE +#define PLM_BUFFER_DEFAULT_SIZE (128 * 1024) +#endif + + +// Create a buffer instance with a filename. Returns NULL if the file could not +// be opened. + +plm_buffer_t *plm_buffer_create_with_filename(const char *filename); + + +// Create a buffer instance with a file handle. Pass TRUE to close_when_done +// to let plmpeg call fclose() on the handle when plm_destroy() is called. + +plm_buffer_t *plm_buffer_create_with_file(FILE *fh, int close_when_done); + + +// Create a buffer instance with a pointer to memory as source. This assumes +// the whole file is in memory. The bytes are not copied. Pass 1 to +// free_when_done to let plmpeg call free() on the pointer when plm_destroy() +// is called. + +plm_buffer_t *plm_buffer_create_with_memory(uint8_t *bytes, size_t length, int free_when_done); + + +// Create an empty buffer with an initial capacity. The buffer will grow +// as needed. Data that has already been read, will be discarded. + +plm_buffer_t *plm_buffer_create_with_capacity(size_t capacity); + + +// Create an empty buffer with an initial capacity. The buffer will grow +// as needed. Decoded data will *not* be discarded. This can be used when +// loading a file over the network, without needing to throttle the download. +// It also allows for seeking in the already loaded data. + +plm_buffer_t *plm_buffer_create_for_appending(size_t initial_capacity); + + +// Destroy a buffer instance and free all data + +void plm_buffer_destroy(plm_buffer_t *self); + + +// Copy data into the buffer. If the data to be written is larger than the +// available space, the buffer will realloc() with a larger capacity. +// Returns the number of bytes written. This will always be the same as the +// passed in length, except when the buffer was created _with_memory() for +// which _write() is forbidden. + +size_t plm_buffer_write(plm_buffer_t *self, uint8_t *bytes, size_t length); + + +// Mark the current byte length as the end of this buffer and signal that no +// more data is expected to be written to it. This function should be called +// just after the last plm_buffer_write(). +// For _with_capacity buffers, this is cleared on a plm_buffer_rewind(). + +void plm_buffer_signal_end(plm_buffer_t *self); + + +// Set a callback that is called whenever the buffer needs more data + +void plm_buffer_set_load_callback(plm_buffer_t *self, plm_buffer_load_callback fp, void *user); + + +// Rewind the buffer back to the beginning. When loading from a file handle, +// this also seeks to the beginning of the file. + +void plm_buffer_rewind(plm_buffer_t *self); + + +// Get the total size. For files, this returns the file size. For all other +// types it returns the number of bytes currently in the buffer. + +size_t plm_buffer_get_size(plm_buffer_t *self); + + +// Get the number of remaining (yet unread) bytes in the buffer. This can be +// useful to throttle writing. + +size_t plm_buffer_get_remaining(plm_buffer_t *self); + + +// Get whether the read position of the buffer is at the end and no more data +// is expected. + +int plm_buffer_has_ended(plm_buffer_t *self); + + + +// ----------------------------------------------------------------------------- +// plm_demux public API +// Demux an MPEG Program Stream (PS) data into separate packages + + +// Various Packet Types + +static const int PLM_DEMUX_PACKET_PRIVATE = 0xBD; +static const int PLM_DEMUX_PACKET_AUDIO_1 = 0xC0; +static const int PLM_DEMUX_PACKET_AUDIO_2 = 0xC1; +static const int PLM_DEMUX_PACKET_AUDIO_3 = 0xC2; +static const int PLM_DEMUX_PACKET_AUDIO_4 = 0xC2; +static const int PLM_DEMUX_PACKET_VIDEO_1 = 0xE0; + + +// Create a demuxer with a plm_buffer as source. This will also attempt to read +// the pack and system headers from the buffer. + +plm_demux_t *plm_demux_create(plm_buffer_t *buffer, int destroy_when_done); + + +// Destroy a demuxer and free all data. + +void plm_demux_destroy(plm_demux_t *self); + + +// Returns TRUE/FALSE whether pack and system headers have been found. This will +// attempt to read the headers if non are present yet. + +int plm_demux_has_headers(plm_demux_t *self); + + +// Returns the number of video streams found in the system header. This will +// attempt to read the system header if non is present yet. + +int plm_demux_get_num_video_streams(plm_demux_t *self); + + +// Returns the number of audio streams found in the system header. This will +// attempt to read the system header if non is present yet. + +int plm_demux_get_num_audio_streams(plm_demux_t *self); + + +// Rewind the internal buffer. See plm_buffer_rewind(). + +void plm_demux_rewind(plm_demux_t *self); + + +// Get whether the file has ended. This will be cleared on seeking or rewind. + +int plm_demux_has_ended(plm_demux_t *self); + + +// Seek to a packet of the specified type with a PTS just before specified time. +// If force_intra is TRUE, only packets containing an intra frame will be +// considered - this only makes sense when the type is PLM_DEMUX_PACKET_VIDEO_1. +// Note that the specified time is considered 0-based, regardless of the first +// PTS in the data source. + +plm_packet_t *plm_demux_seek(plm_demux_t *self, double time, int type, int force_intra); + + +// Get the PTS of the first packet of this type. Returns PLM_PACKET_INVALID_TS +// if not packet of this packet type can be found. + +double plm_demux_get_start_time(plm_demux_t *self, int type); + + +// Get the duration for the specified packet type - i.e. the span between the +// the first PTS and the last PTS in the data source. This only makes sense when +// the underlying data source is a file or fixed memory. + +double plm_demux_get_duration(plm_demux_t *self, int type); + + +// Decode and return the next packet. The returned packet_t is valid until +// the next call to plm_demux_decode() or until the demuxer is destroyed. + +plm_packet_t *plm_demux_decode(plm_demux_t *self); + + + +// ----------------------------------------------------------------------------- +// plm_video public API +// Decode MPEG1 Video ("mpeg1") data into raw YCrCb frames + + +// Create a video decoder with a plm_buffer as source. + +plm_video_t *plm_video_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done); + + +// Destroy a video decoder and free all data. + +void plm_video_destroy(plm_video_t *self); + + +// Get whether a sequence header was found and we can accurately report on +// dimensions and framerate. + +int plm_video_has_header(plm_video_t *self); + + +// Get the framerate in frames per second. + +double plm_video_get_framerate(plm_video_t *self); + + +// Get the display width/height. + +int plm_video_get_width(plm_video_t *self); +int plm_video_get_height(plm_video_t *self); + +// Get the display aspect ratio (DAR). +// If a libdragon proprietary SAR (LD_SAR=NUM:DEN) is present as MPEG-1 user_data, +// it is used; otherwise the MPEG-1 sequence header aspect_ratio_information is used. +double plm_video_get_aspect_ratio(plm_video_t *self); + + +// Set "no delay" mode. When enabled, the decoder assumes that the video does +// *not* contain any B-Frames. This is useful for reducing lag when streaming. +// The default is FALSE. + +void plm_video_set_no_delay(plm_video_t *self, int no_delay); + +// ----------------------------------------------------------------------------- +// libdragon extension: buffered output + incremental polling +// +// These APIs allow decoding video in small chunks (eg: one slice at a time) +// while keeping up to N fully decoded frames in an internal output queue. +// This avoids copying full YUV frames while enabling background decoding. + +// Configure how many fully decoded pictures can be buffered in the output queue. +// Must be called before the sequence header is decoded (before frames_data alloc). +void plm_video_set_buffered_pics(plm_video_t *self, int buffered_pics); + +// Do a small chunk of decoding work. Returns 1 if work was done, 0 if no work +// could be done (eg: need more input data, output queue full, or EOF). +int plm_video_poll(plm_video_t *self); + +// Extract next picture from output queue (presentation order). Returned pointer +// stays valid until released or the decoder is destroyed. +plm_frame_t *plm_video_next_output(plm_video_t *self); + +// Release a picture previously obtained via plm_video_next_output(). +void plm_video_release_output(plm_video_t *self, const plm_frame_t *frame); + + +// Get the current internal time in seconds. + +double plm_video_get_time(plm_video_t *self); + + +// Set the current internal time in seconds. This is only useful when you +// manipulate the underlying video buffer and want to enforce a correct +// timestamps. + +void plm_video_set_time(plm_video_t *self, double time); + + +// Rewind the internal buffer. See plm_buffer_rewind(). + +void plm_video_rewind(plm_video_t *self); + + +// Get whether the file has ended. This will be cleared on rewind. + +int plm_video_has_ended(plm_video_t *self); + + +// Decode and return one frame of video and advance the internal time by +// 1/framerate seconds. The returned frame_t is valid until the next call of +// plm_video_decode() or until the video decoder is destroyed. + +plm_frame_t *plm_video_decode(plm_video_t *self); + + +// Convert the YCrCb data of a frame into interleaved R G B data. The stride +// specifies the width in bytes of the destination buffer. I.e. the number of +// bytes from one line to the next. The stride must be at least +// (frame->width * bytes_per_pixel). The buffer pointed to by *dest must have a +// size of at least (stride * frame->height). +// Note that the alpha component of the dest buffer is always left untouched. + +void plm_frame_to_rgb(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_bgr(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_rgba(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_bgra(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_argb(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_abgr(plm_frame_t *frame, uint8_t *dest, int stride); + + +// ----------------------------------------------------------------------------- +// plm_audio public API +// Decode MPEG-1 Audio Layer II ("mp2") data into raw samples + + +// Create an audio decoder with a plm_buffer as source. + +plm_audio_t *plm_audio_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done); + + +// Destroy an audio decoder and free all data. + +void plm_audio_destroy(plm_audio_t *self); + + +// Get whether a frame header was found and we can accurately report on +// samplerate. + +int plm_audio_has_header(plm_audio_t *self); + + +// Get the samplerate in samples per second. + +int plm_audio_get_samplerate(plm_audio_t *self); + + +// Get the current internal time in seconds. + +double plm_audio_get_time(plm_audio_t *self); + + +// Set the current internal time in seconds. This is only useful when you +// manipulate the underlying video buffer and want to enforce a correct +// timestamps. + +void plm_audio_set_time(plm_audio_t *self, double time); + + +// Rewind the internal buffer. See plm_buffer_rewind(). + +void plm_audio_rewind(plm_audio_t *self); + + +// Get whether the file has ended. This will be cleared on rewind. + +int plm_audio_has_ended(plm_audio_t *self); + + +// Decode and return one "frame" of audio and advance the internal time by +// (PLM_AUDIO_SAMPLES_PER_FRAME/samplerate) seconds. The returned samples_t +// is valid until the next call of plm_audio_decode() or until the audio +// decoder is destroyed. + +plm_samples_t *plm_audio_decode(plm_audio_t *self); + + + +#ifdef __cplusplus +} +#endif + +#endif // PL_MPEG_H + + + + + +// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// IMPLEMENTATION + +#ifdef PL_MPEG_IMPLEMENTATION + +#include +#include +#include +#include + +#ifndef ASSERT +#define ASSERT(x) assert(x) +#endif + +#ifndef TRUE +#define TRUE 1 +#define FALSE 0 +#endif + +#define PLM_UNUSED(expr) (void)(expr) + + +// ----------------------------------------------------------------------------- +// plm (high-level interface) implementation + +typedef struct plm_t { + plm_demux_t *demux; + double time; + int has_ended; + int loop; + int has_decoders; + + int video_enabled; + int video_packet_type; + plm_buffer_t *video_buffer; + plm_video_t *video_decoder; + + int audio_enabled; + int audio_stream_index; + int audio_packet_type; + double audio_lead_time; + plm_buffer_t *audio_buffer; + plm_audio_t *audio_decoder; + + plm_video_decode_callback video_decode_callback; + void *video_decode_callback_user_data; + + plm_audio_decode_callback audio_decode_callback; + void *audio_decode_callback_user_data; +} plm_t; + +int plm_init_decoders(plm_t *self); +void plm_handle_end(plm_t *self); +void plm_read_video_packet(plm_buffer_t *buffer, void *user); +void plm_read_audio_packet(plm_buffer_t *buffer, void *user); +void plm_read_packets(plm_t *self, int requested_type); + +plm_t *plm_create_with_filename(const char *filename) { + plm_buffer_t *buffer = plm_buffer_create_with_filename(filename); + if (!buffer) { + return NULL; + } + return plm_create_with_buffer(buffer, TRUE); +} + +plm_t *plm_create_with_file(FILE *fh, int close_when_done) { + plm_buffer_t *buffer = plm_buffer_create_with_file(fh, close_when_done); + return plm_create_with_buffer(buffer, TRUE); +} + +plm_t *plm_create_with_memory(uint8_t *bytes, size_t length, int free_when_done) { + plm_buffer_t *buffer = plm_buffer_create_with_memory(bytes, length, free_when_done); + return plm_create_with_buffer(buffer, TRUE); +} + +plm_t *plm_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done) { + plm_t *self = (plm_t *)malloc(sizeof(plm_t)); + assertf(self, "Out of memory"); + memset(self, 0, sizeof(plm_t)); + + self->demux = plm_demux_create(buffer, destroy_when_done); + self->video_enabled = TRUE; + self->audio_enabled = TRUE; + plm_init_decoders(self); + + return self; +} + +int plm_init_decoders(plm_t *self) { + if (self->has_decoders) { + return TRUE; + } + + if (!plm_demux_has_headers(self->demux)) { + return FALSE; + } + + if (plm_demux_get_num_video_streams(self->demux) > 0) { + if (self->video_enabled) { + self->video_packet_type = PLM_DEMUX_PACKET_VIDEO_1; + } + self->video_buffer = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + plm_buffer_set_load_callback(self->video_buffer, plm_read_video_packet, self); + } + + if (plm_demux_get_num_audio_streams(self->demux) > 0) { + if (self->audio_enabled) { + self->audio_packet_type = PLM_DEMUX_PACKET_AUDIO_1 + self->audio_stream_index; + } + self->audio_buffer = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + plm_buffer_set_load_callback(self->audio_buffer, plm_read_audio_packet, self); + } + + if (self->video_buffer) { + self->video_decoder = plm_video_create_with_buffer(self->video_buffer, TRUE); + } + + if (self->audio_buffer) { + self->audio_decoder = plm_audio_create_with_buffer(self->audio_buffer, TRUE); + } + + self->has_decoders = TRUE; + return TRUE; +} + +void plm_destroy(plm_t *self) { + if (self->video_decoder) { + plm_video_destroy(self->video_decoder); + } + if (self->audio_decoder) { + plm_audio_destroy(self->audio_decoder); + } + + plm_demux_destroy(self->demux); + free(self); +} + +int plm_get_audio_enabled(plm_t *self) { + return self->audio_enabled; +} + +int plm_has_headers(plm_t *self) { + if (!plm_demux_has_headers(self->demux)) { + return FALSE; + } + + if (!plm_init_decoders(self)) { + return FALSE; + } + + if ( + (self->video_decoder && !plm_video_has_header(self->video_decoder)) || + (self->audio_decoder && !plm_audio_has_header(self->audio_decoder)) + ) { + return FALSE; + } + + return TRUE; +} + +void plm_set_audio_enabled(plm_t *self, int enabled) { + self->audio_enabled = enabled; + + if (!enabled) { + self->audio_packet_type = 0; + return; + } + + self->audio_packet_type = (plm_init_decoders(self) && self->audio_decoder) + ? PLM_DEMUX_PACKET_AUDIO_1 + self->audio_stream_index + : 0; +} + +void plm_set_audio_stream(plm_t *self, int stream_index) { + if (stream_index < 0 || stream_index > 3) { + return; + } + self->audio_stream_index = stream_index; + + // Set the correct audio_packet_type + plm_set_audio_enabled(self, self->audio_enabled); +} + +int plm_get_video_enabled(plm_t *self) { + return self->video_enabled; +} + +void plm_set_video_enabled(plm_t *self, int enabled) { + self->video_enabled = enabled; + + if (!enabled) { + self->video_packet_type = 0; + return; + } + + self->video_packet_type = (plm_init_decoders(self) && self->video_decoder) + ? PLM_DEMUX_PACKET_VIDEO_1 + : 0; +} + +int plm_get_num_video_streams(plm_t *self) { + return plm_demux_get_num_video_streams(self->demux); +} + +int plm_get_width(plm_t *self) { + return (plm_init_decoders(self) && self->video_decoder) + ? plm_video_get_width(self->video_decoder) + : 0; +} + +int plm_get_height(plm_t *self) { + return (plm_init_decoders(self) && self->video_decoder) + ? plm_video_get_height(self->video_decoder) + : 0; +} + +double plm_get_framerate(plm_t *self) { + return (plm_init_decoders(self) && self->video_decoder) + ? plm_video_get_framerate(self->video_decoder) + : 0; +} + +int plm_get_num_audio_streams(plm_t *self) { + return plm_demux_get_num_audio_streams(self->demux); +} + +int plm_get_samplerate(plm_t *self) { + return (plm_init_decoders(self) && self->audio_decoder) + ? plm_audio_get_samplerate(self->audio_decoder) + : 0; +} + +double plm_get_audio_lead_time(plm_t *self) { + return self->audio_lead_time; +} + +void plm_set_audio_lead_time(plm_t *self, double lead_time) { + self->audio_lead_time = lead_time; +} + +double plm_get_time(plm_t *self) { + return self->time; +} + +double plm_get_duration(plm_t *self) { + return plm_demux_get_duration(self->demux, PLM_DEMUX_PACKET_VIDEO_1); +} + +void plm_rewind(plm_t *self) { + if (self->video_decoder) { + plm_video_rewind(self->video_decoder); + } + + if (self->audio_decoder) { + plm_audio_rewind(self->audio_decoder); + } + + plm_demux_rewind(self->demux); + self->time = 0; +} + +int plm_get_loop(plm_t *self) { + return self->loop; +} + +void plm_set_loop(plm_t *self, int loop) { + self->loop = loop; +} + +int plm_has_ended(plm_t *self) { + return self->has_ended; +} + +void plm_set_video_decode_callback(plm_t *self, plm_video_decode_callback fp, void *user) { + self->video_decode_callback = fp; + self->video_decode_callback_user_data = user; +} + +void plm_set_audio_decode_callback(plm_t *self, plm_audio_decode_callback fp, void *user) { + self->audio_decode_callback = fp; + self->audio_decode_callback_user_data = user; +} + +void plm_decode(plm_t *self, double tick) { + if (!plm_init_decoders(self)) { + return; + } + + int decode_video = (self->video_decode_callback && self->video_packet_type); + int decode_audio = (self->audio_decode_callback && self->audio_packet_type); + + if (!decode_video && !decode_audio) { + // Nothing to do here + return; + } + + int did_decode = FALSE; + int decode_video_failed = FALSE; + int decode_audio_failed = FALSE; + + double video_target_time = self->time + tick; + double audio_target_time = self->time + tick + self->audio_lead_time; + + do { + did_decode = FALSE; + + if (decode_video && plm_video_get_time(self->video_decoder) < video_target_time) { + plm_frame_t *frame = plm_video_decode(self->video_decoder); + if (frame) { + self->video_decode_callback(self, frame, self->video_decode_callback_user_data); + did_decode = TRUE; + } + else { + decode_video_failed = TRUE; + } + } + + if (decode_audio && plm_audio_get_time(self->audio_decoder) < audio_target_time) { + plm_samples_t *samples = plm_audio_decode(self->audio_decoder); + if (samples) { + self->audio_decode_callback(self, samples, self->audio_decode_callback_user_data); + did_decode = TRUE; + } + else { + decode_audio_failed = TRUE; + } + } + } while (did_decode); + + // Did all sources we wanted to decode fail and the demuxer is at the end? + if ( + (!decode_video || decode_video_failed) && + (!decode_audio || decode_audio_failed) && + plm_demux_has_ended(self->demux) + ) { + plm_handle_end(self); + return; + } + + self->time += tick; +} + +plm_frame_t *plm_decode_video(plm_t *self) { + if (!plm_init_decoders(self)) { + return NULL; + } + + if (!self->video_packet_type) { + return NULL; + } + + plm_frame_t *frame = plm_video_decode(self->video_decoder); + if (frame) { + self->time = frame->time; + } + else if (plm_demux_has_ended(self->demux)) { + plm_handle_end(self); + } + return frame; +} + +plm_samples_t *plm_decode_audio(plm_t *self) { + if (!plm_init_decoders(self)) { + return NULL; + } + + if (!self->audio_packet_type) { + return NULL; + } + + plm_samples_t *samples = plm_audio_decode(self->audio_decoder); + if (samples) { + self->time = samples->time; + } + else if (plm_demux_has_ended(self->demux)) { + plm_handle_end(self); + } + return samples; +} + +void plm_handle_end(plm_t *self) { + if (self->loop) { + plm_rewind(self); + } + else { + self->has_ended = TRUE; + } +} + +void plm_read_video_packet(plm_buffer_t *buffer, void *user) { + PLM_UNUSED(buffer); + plm_t *self = (plm_t *)user; + plm_read_packets(self, self->video_packet_type); +} + +void plm_read_audio_packet(plm_buffer_t *buffer, void *user) { + PLM_UNUSED(buffer); + plm_t *self = (plm_t *)user; + plm_read_packets(self, self->audio_packet_type); +} + +void plm_read_packets(plm_t *self, int requested_type) { + plm_packet_t *packet; + while ((packet = plm_demux_decode(self->demux))) { + if (packet->type == self->video_packet_type) { + plm_buffer_write(self->video_buffer, packet->data, packet->length); + } + else if (packet->type == self->audio_packet_type) { + plm_buffer_write(self->audio_buffer, packet->data, packet->length); + } + + if (packet->type == requested_type) { + return; + } + } + + if (plm_demux_has_ended(self->demux)) { + if (self->video_buffer) { + plm_buffer_signal_end(self->video_buffer); + } + if (self->audio_buffer) { + plm_buffer_signal_end(self->audio_buffer); + } + } +} + +plm_frame_t *plm_seek_frame(plm_t *self, double time, int seek_exact) { + if (!plm_init_decoders(self)) { + return NULL; + } + + if (!self->video_packet_type) { + return NULL; + } + + int type = self->video_packet_type; + + double start_time = plm_demux_get_start_time(self->demux, type); + double duration = plm_demux_get_duration(self->demux, type); + + if (time < 0) { + time = 0; + } + else if (time > duration) { + time = duration; + } + + plm_packet_t *packet = plm_demux_seek(self->demux, time, type, TRUE); + if (!packet) { + return NULL; + } + + // Disable writing to the audio buffer while decoding video + int previous_audio_packet_type = self->audio_packet_type; + self->audio_packet_type = 0; + + // Clear video buffer and decode the found packet + plm_video_rewind(self->video_decoder); + plm_video_set_time(self->video_decoder, packet->pts - start_time); + plm_buffer_write(self->video_buffer, packet->data, packet->length); + plm_frame_t *frame = plm_video_decode(self->video_decoder); + + // If we want to seek to an exact frame, we have to decode all frames + // on top of the intra frame we just jumped to. + if (seek_exact) { + while (frame && frame->time < time) { + frame = plm_video_decode(self->video_decoder); + } + } + + // Enable writing to the audio buffer again? + self->audio_packet_type = previous_audio_packet_type; + + if (frame) { + self->time = frame->time; + } + + self->has_ended = FALSE; + return frame; +} + +int plm_seek(plm_t *self, double time, int seek_exact) { + plm_frame_t *frame = plm_seek_frame(self, time, seek_exact); + + if (!frame) { + return FALSE; + } + + if (self->video_decode_callback) { + self->video_decode_callback(self, frame, self->video_decode_callback_user_data); + } + + // If audio is not enabled we are done here. + if (!self->audio_packet_type) { + return TRUE; + } + + // Sync up Audio. This demuxes more packets until the first audio packet + // with a PTS greater than the current time is found. plm_decode() is then + // called to decode enough audio data to satisfy the audio_lead_time. + + double start_time = plm_demux_get_start_time(self->demux, self->video_packet_type); + plm_audio_rewind(self->audio_decoder); + + plm_packet_t *packet = NULL; + while ((packet = plm_demux_decode(self->demux))) { + if (packet->type == self->video_packet_type) { + plm_buffer_write(self->video_buffer, packet->data, packet->length); + } + else if ( + packet->type == self->audio_packet_type && + packet->pts - start_time > self->time + ) { + plm_audio_set_time(self->audio_decoder, packet->pts - start_time); + plm_buffer_write(self->audio_buffer, packet->data, packet->length); + plm_decode(self, 0); + break; + } + } + + return TRUE; +} + + + +// ----------------------------------------------------------------------------- +// plm_buffer implementation + +enum plm_buffer_mode { + PLM_BUFFER_MODE_FILE, + PLM_BUFFER_MODE_FIXED_MEM, + PLM_BUFFER_MODE_RING, + PLM_BUFFER_MODE_APPEND +}; + +typedef struct plm_buffer_t { + size_t bit_index; + size_t capacity; + size_t length; + size_t total_size; + int discard_read_bytes; + int has_ended; + int free_when_done; + int close_when_done; + FILE *fh; + plm_buffer_load_callback load_callback; + void *load_callback_user_data; + uint8_t *bytes; + enum plm_buffer_mode mode; +} plm_buffer_t; + +typedef struct { + int16_t index; + int16_t value; +} plm_vlc_t; + +typedef struct { + int16_t index; + uint16_t value; +} plm_vlc_uint_t; + + +void plm_buffer_seek(plm_buffer_t *self, size_t pos); +size_t plm_buffer_tell(plm_buffer_t *self); +void plm_buffer_discard_read_bytes(plm_buffer_t *self); +void plm_buffer_load_file_callback(plm_buffer_t *self, void *user); + +int plm_buffer_has(plm_buffer_t *self, size_t count); +int plm_buffer_read(plm_buffer_t *self, int count); +void plm_buffer_align(plm_buffer_t *self); +void plm_buffer_skip(plm_buffer_t *self, size_t count); +int plm_buffer_skip_bytes(plm_buffer_t *self, uint8_t v); +int plm_buffer_next_start_code(plm_buffer_t *self); +int plm_buffer_find_start_code(plm_buffer_t *self, int code); +int plm_buffer_no_start_code(plm_buffer_t *self); +int16_t plm_buffer_read_vlc(plm_buffer_t *self, const plm_vlc_t *table); +uint16_t plm_buffer_read_vlc_uint(plm_buffer_t *self, const plm_vlc_uint_t *table); + +plm_buffer_t *plm_buffer_create_with_filename(const char *filename) { + FILE *fh = fopen(filename, "rb"); + if (!fh) { + return NULL; + } + return plm_buffer_create_with_file(fh, TRUE); +} + +plm_buffer_t *plm_buffer_create_with_file(FILE *fh, int close_when_done) { + plm_buffer_t *self = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + self->fh = fh; + self->close_when_done = close_when_done; + self->mode = PLM_BUFFER_MODE_FILE; + self->discard_read_bytes = TRUE; + + fseek(self->fh, 0, SEEK_END); + self->total_size = ftell(self->fh); + fseek(self->fh, 0, SEEK_SET); plm_buffer_set_load_callback(self, plm_buffer_load_file_callback, NULL); + return self; +} + +plm_buffer_t *plm_buffer_create_with_memory(uint8_t *bytes, size_t length, int free_when_done) { + plm_buffer_t *self = (plm_buffer_t *)malloc(sizeof(plm_buffer_t)); + assertf(self, "Out of memory"); + memset(self, 0, sizeof(plm_buffer_t)); + self->capacity = length; + self->length = length; + self->total_size = length; + self->free_when_done = free_when_done; + self->bytes = bytes; + self->mode = PLM_BUFFER_MODE_FIXED_MEM; + self->discard_read_bytes = FALSE; + return self; +} + +plm_buffer_t *plm_buffer_create_with_capacity(size_t capacity) { + plm_buffer_t *self = (plm_buffer_t *)malloc(sizeof(plm_buffer_t)); + assertf(self, "Out of memory"); + memset(self, 0, sizeof(plm_buffer_t)); + self->capacity = capacity; + self->free_when_done = TRUE; + self->bytes = (uint8_t *)malloc(capacity); + assertf(self->bytes, "Out of memory"); + self->mode = PLM_BUFFER_MODE_RING; + self->discard_read_bytes = TRUE; + return self; +} + +plm_buffer_t *plm_buffer_create_for_appending(size_t initial_capacity) { + plm_buffer_t *self = plm_buffer_create_with_capacity(initial_capacity); + self->mode = PLM_BUFFER_MODE_APPEND; + self->discard_read_bytes = FALSE; + return self; +} + +void plm_buffer_destroy(plm_buffer_t *self) { + if (self->fh && self->close_when_done) { + fclose(self->fh); + } + if (self->free_when_done) { + free(self->bytes); + } + free(self); +} + +size_t plm_buffer_get_size(plm_buffer_t *self) { + return (self->mode == PLM_BUFFER_MODE_FILE) + ? self->total_size + : self->length; +} + +size_t plm_buffer_get_remaining(plm_buffer_t *self) { + return self->length - (self->bit_index >> 3); +} + +size_t plm_buffer_write(plm_buffer_t *self, uint8_t *bytes, size_t length) { + if (self->mode == PLM_BUFFER_MODE_FIXED_MEM) { + return 0; + } + + if (self->discard_read_bytes) { + // This should be a ring buffer, but instead it just shifts all unread + // data to the beginning of the buffer and appends new data at the end. + // Seems to be good enough. + + plm_buffer_discard_read_bytes(self); + if (self->mode == PLM_BUFFER_MODE_RING) { + self->total_size = 0; + } + } + + // Do we have to resize to fit the new data? + size_t bytes_available = self->capacity - self->length; + if (bytes_available < length) { + size_t new_size = self->capacity; + do { + new_size *= 2; + } while (new_size - self->length < length); + self->bytes = (uint8_t *)realloc(self->bytes, new_size); + assertf(self->bytes, "Out of memory"); + self->capacity = new_size; + } + + memcpy(self->bytes + self->length, bytes, length); + self->length += length; + self->has_ended = FALSE; + return length; +} + +void plm_buffer_signal_end(plm_buffer_t *self) { + self->total_size = self->length; +} + +void plm_buffer_set_load_callback(plm_buffer_t *self, plm_buffer_load_callback fp, void *user) { + self->load_callback = fp; + self->load_callback_user_data = user; +} + +void plm_buffer_rewind(plm_buffer_t *self) { + plm_buffer_seek(self, 0); +} + +void plm_buffer_seek(plm_buffer_t *self, size_t pos) { + self->has_ended = FALSE; + + if (self->mode == PLM_BUFFER_MODE_FILE) { + fseek(self->fh, pos, SEEK_SET); + self->bit_index = 0; + self->length = 0; + } + else if (self->mode == PLM_BUFFER_MODE_RING) { + if (pos != 0) { + // Seeking to non-0 is forbidden for dynamic-mem buffers + return; + } + self->bit_index = 0; + self->length = 0; + self->total_size = 0; + } + else if (pos < self->length) { + self->bit_index = pos << 3; + } +} + +size_t plm_buffer_tell(plm_buffer_t *self) { + return self->mode == PLM_BUFFER_MODE_FILE + ? ftell(self->fh) + (self->bit_index >> 3) - self->length + : self->bit_index >> 3; +} + +void plm_buffer_discard_read_bytes(plm_buffer_t *self) { + size_t byte_pos = self->bit_index >> 3; + if (byte_pos == self->length) { + self->bit_index = 0; + self->length = 0; + } + else if (byte_pos > 0) { + memmove(self->bytes, self->bytes + byte_pos, self->length - byte_pos); + self->bit_index -= byte_pos << 3; + self->length -= byte_pos; + } +} + +void plm_buffer_load_file_callback(plm_buffer_t *self, void *user) { + PLM_UNUSED(user); + + if (self->discard_read_bytes) { + plm_buffer_discard_read_bytes(self); + } + + size_t bytes_available = self->capacity - self->length; + size_t bytes_read = fread(self->bytes + self->length, 1, bytes_available, self->fh); + self->length += bytes_read; + + if (bytes_read == 0) { + self->has_ended = TRUE; + } +} + +int plm_buffer_has_ended(plm_buffer_t *self) { + return self->has_ended; +} + +int plm_buffer_has(plm_buffer_t *self, size_t count) { + if (((self->length << 3) - self->bit_index) >= count) { + return TRUE; + } + + if (self->load_callback) { + self->load_callback(self, self->load_callback_user_data); + + if (((self->length << 3) - self->bit_index) >= count) { + return TRUE; + } + } + + if (self->total_size != 0 && self->length == self->total_size) { + self->has_ended = TRUE; + } + return FALSE; +} + +uint64_t plm_buffer_showbits(plm_buffer_t *self) { + if (!plm_buffer_has(self, 64)) + return 0; + + typedef uint64_t u_uint64_t __attribute__((aligned(1))); + + uint64_t bits = *(u_uint64_t*)&self->bytes[self->bit_index >> 3]; + bits <<= self->bit_index & 7; + return bits; +} + +uint64_t plm_buffer_showbits2(plm_buffer_t *self) { + typedef uint64_t u_uint64_t __attribute__((aligned(1))); + uint64_t bits = *(u_uint64_t*)&self->bytes[self->bit_index >> 3]; + bits <<= self->bit_index & 7; + return bits; +} + +int plm_buffer_read(plm_buffer_t *self, int count) { + if (!plm_buffer_has(self, count)) { + return 0; + } + + int value = 0; + while (count) { + int current_byte = self->bytes[self->bit_index >> 3]; + + int remaining = 8 - (self->bit_index & 7); // Remaining bits in byte + int read = remaining < count ? remaining : count; // Bits in self run + int shift = remaining - read; + int mask = (0xff >> (8 - read)); + + value = (value << read) | ((current_byte & (mask << shift)) >> shift); + + self->bit_index += read; + count -= read; + } + + return value; +} + +void plm_buffer_align(plm_buffer_t *self) { + self->bit_index = ((self->bit_index + 7) >> 3) << 3; // Align to next byte +} + +void plm_buffer_skip(plm_buffer_t *self, size_t count) { + if (plm_buffer_has(self, count)) { + self->bit_index += count; + } +} + +int plm_buffer_skip_bytes(plm_buffer_t *self, uint8_t v) { + plm_buffer_align(self); + int skipped = 0; + while (plm_buffer_has(self, 8) && self->bytes[self->bit_index >> 3] == v) { + self->bit_index += 8; + skipped++; + } + return skipped; +} + +int plm_buffer_next_start_code(plm_buffer_t *self) { + plm_buffer_align(self); + + while (plm_buffer_has(self, (5 << 3))) { + size_t byte_index = (self->bit_index) >> 3; + if ( + self->bytes[byte_index] == 0x00 && + self->bytes[byte_index + 1] == 0x00 && + self->bytes[byte_index + 2] == 0x01 + ) { + self->bit_index = (byte_index + 4) << 3; + return self->bytes[byte_index + 3]; + } + self->bit_index += 8; + } + return -1; +} + +int plm_buffer_find_start_code(plm_buffer_t *self, int code) { + int current = 0; + while (TRUE) { + current = plm_buffer_next_start_code(self); + if (current == code || current == -1) { + return current; + } + } + return -1; +} + +int plm_buffer_has_start_code(plm_buffer_t *self, int code) { + size_t previous_bit_index = self->bit_index; + int previous_discard_read_bytes = self->discard_read_bytes; + + self->discard_read_bytes = FALSE; + int current = plm_buffer_find_start_code(self, code); + + self->bit_index = previous_bit_index; + self->discard_read_bytes = previous_discard_read_bytes; + return current; +} + +int plm_buffer_peek_non_zero(plm_buffer_t *self, int bit_count) { + if (!plm_buffer_has(self, bit_count)) { + return FALSE; + } + + int val = plm_buffer_read(self, bit_count); + self->bit_index -= bit_count; + return val != 0; +} + +int16_t plm_buffer_read_vlc_bits(plm_buffer_t *self, const plm_vlc_t *table, uint64_t bits) { + plm_vlc_t state = {0, 0}; + do { + state = table[state.index + (bits >> 63)]; + bits <<= 1; + self->bit_index++; + } while (state.index > 0); + return state.value; +} + + +int16_t plm_buffer_read_vlc(plm_buffer_t *self, const plm_vlc_t *table) { + return plm_buffer_read_vlc_bits(self, table, plm_buffer_showbits(self)); +} + +uint16_t plm_buffer_read_vlc_uint_bits(plm_buffer_t *self, const plm_vlc_uint_t *table, uint64_t bits) { + return (uint16_t)plm_buffer_read_vlc_bits(self, (const plm_vlc_t *)table, bits); +} + +uint16_t plm_buffer_read_vlc_uint(plm_buffer_t *self, const plm_vlc_uint_t *table) { + return (uint16_t)plm_buffer_read_vlc(self, (const plm_vlc_t *)table); +} + + +// ---------------------------------------------------------------------------- +// plm_demux implementation + +static const int PLM_START_PACK = 0xBA; +static const int PLM_START_END = 0xB9; +static const int PLM_START_SYSTEM = 0xBB; + +typedef struct plm_demux_t { + plm_buffer_t *buffer; + int destroy_buffer_when_done; + double system_clock_ref; + + size_t last_file_size; + double last_decoded_pts; + double start_time; + double duration; + + int start_code; + int has_pack_header; + int has_system_header; + int has_headers; + + int num_audio_streams; + int num_video_streams; + plm_packet_t current_packet; + plm_packet_t next_packet; +} plm_demux_t; + + +void plm_demux_buffer_seek(plm_demux_t *self, size_t pos); +double plm_demux_decode_time(plm_demux_t *self); +plm_packet_t *plm_demux_decode_packet(plm_demux_t *self, int type); +plm_packet_t *plm_demux_get_packet(plm_demux_t *self); + +plm_demux_t *plm_demux_create(plm_buffer_t *buffer, int destroy_when_done) { + plm_demux_t *self = (plm_demux_t *)malloc(sizeof(plm_demux_t)); + assertf(self, "Out of memory"); + memset(self, 0, sizeof(plm_demux_t)); + + self->buffer = buffer; + self->destroy_buffer_when_done = destroy_when_done; + + self->start_time = PLM_PACKET_INVALID_TS; + self->duration = PLM_PACKET_INVALID_TS; + self->start_code = -1; + + plm_demux_has_headers(self); + return self; +} + +void plm_demux_destroy(plm_demux_t *self) { + if (self->destroy_buffer_when_done) { + plm_buffer_destroy(self->buffer); + } + free(self); +} + +int plm_demux_has_headers(plm_demux_t *self) { + if (self->has_headers) { + return TRUE; + } + + // Decode pack header + if (!self->has_pack_header) { + if ( + self->start_code != PLM_START_PACK && + plm_buffer_find_start_code(self->buffer, PLM_START_PACK) == -1 + ) { + return FALSE; + } + + self->start_code = PLM_START_PACK; + if (!plm_buffer_has(self->buffer, 64)) { + return FALSE; + } + self->start_code = -1; + + if (plm_buffer_read(self->buffer, 4) != 0x02) { + return FALSE; + } + + self->system_clock_ref = plm_demux_decode_time(self); + plm_buffer_skip(self->buffer, 1); + plm_buffer_skip(self->buffer, 22); // mux_rate * 50 + plm_buffer_skip(self->buffer, 1); + + self->has_pack_header = TRUE; + } + + // Decode system header + if (!self->has_system_header) { + if ( + self->start_code != PLM_START_SYSTEM && + plm_buffer_find_start_code(self->buffer, PLM_START_SYSTEM) == -1 + ) { + return FALSE; + } + + self->start_code = PLM_START_SYSTEM; + if (!plm_buffer_has(self->buffer, 56)) { + return FALSE; + } + self->start_code = -1; + + plm_buffer_skip(self->buffer, 16); // header_length + plm_buffer_skip(self->buffer, 24); // rate bound + self->num_audio_streams = plm_buffer_read(self->buffer, 6); + plm_buffer_skip(self->buffer, 5); // misc flags + self->num_video_streams = plm_buffer_read(self->buffer, 5); + + self->has_system_header = TRUE; + } + + self->has_headers = TRUE; + return TRUE; +} + +int plm_demux_get_num_video_streams(plm_demux_t *self) { + return plm_demux_has_headers(self) + ? self->num_video_streams + : 0; +} + +int plm_demux_get_num_audio_streams(plm_demux_t *self) { + return plm_demux_has_headers(self) + ? self->num_audio_streams + : 0; +} + +void plm_demux_rewind(plm_demux_t *self) { + plm_buffer_rewind(self->buffer); + self->current_packet.length = 0; + self->next_packet.length = 0; + self->start_code = -1; +} + +int plm_demux_has_ended(plm_demux_t *self) { + return plm_buffer_has_ended(self->buffer); +} + +void plm_demux_buffer_seek(plm_demux_t *self, size_t pos) { + plm_buffer_seek(self->buffer, pos); + self->current_packet.length = 0; + self->next_packet.length = 0; + self->start_code = -1; +} + +double plm_demux_get_start_time(plm_demux_t *self, int type) { + if (self->start_time != PLM_PACKET_INVALID_TS) { + return self->start_time; + } + + int previous_pos = plm_buffer_tell(self->buffer); + int previous_start_code = self->start_code; + + // Find first video PTS + plm_demux_rewind(self); + do { + plm_packet_t *packet = plm_demux_decode(self); + if (!packet) { + break; + } + if (packet->type == type) { + self->start_time = packet->pts; + } + } while (self->start_time == PLM_PACKET_INVALID_TS); + + plm_demux_buffer_seek(self, previous_pos); + self->start_code = previous_start_code; + return self->start_time; +} + +double plm_demux_get_duration(plm_demux_t *self, int type) { + size_t file_size = plm_buffer_get_size(self->buffer); + + if ( + self->duration != PLM_PACKET_INVALID_TS && + self->last_file_size == file_size + ) { + return self->duration; + } + + size_t previous_pos = plm_buffer_tell(self->buffer); + int previous_start_code = self->start_code; + + // Find last video PTS. Start searching 64kb from the end and go further + // back if needed. + long start_range = 64 * 1024; + long max_range = 4096 * 1024; + for (long range = start_range; range <= max_range; range *= 2) { + long seek_pos = file_size - range; + if (seek_pos < 0) { + seek_pos = 0; + range = max_range; // Make sure to bail after this round + } + plm_demux_buffer_seek(self, seek_pos); + self->current_packet.length = 0; + + double last_pts = PLM_PACKET_INVALID_TS; + plm_packet_t *packet = NULL; + while ((packet = plm_demux_decode(self))) { + if (packet->pts != PLM_PACKET_INVALID_TS && packet->type == type) { + last_pts = packet->pts; + } + } + if (last_pts != PLM_PACKET_INVALID_TS) { + self->duration = last_pts - plm_demux_get_start_time(self, type); + break; + } + } + + plm_demux_buffer_seek(self, previous_pos); + self->start_code = previous_start_code; + self->last_file_size = file_size; + return self->duration; +} + +plm_packet_t *plm_demux_seek(plm_demux_t *self, double seek_time, int type, int force_intra) { + if (!plm_demux_has_headers(self)) { + return NULL; + } + + // Using the current time, current byte position and the average bytes per + // second for this file, try to jump to a byte position that hopefully has + // packets containing timestamps within one second before to the desired + // seek_time. + + // If we hit close to the seek_time scan through all packets to find the + // last one (just before the seek_time) containing an intra frame. + // Otherwise we should at least be closer than before. Calculate the bytes + // per second for the jumped range and jump again. + + // The number of retries here is hard-limited to a generous amount. Usually + // the correct range is found after 1--5 jumps, even for files with very + // variable bitrates. If significantly more jumps are needed, there's + // probably something wrong with the file and we just avoid getting into an + // infinite loop. 32 retries should be enough for anybody. + + double duration = plm_demux_get_duration(self, type); + long file_size = plm_buffer_get_size(self->buffer); + long byterate = file_size / duration; + + double cur_time = self->last_decoded_pts; + double scan_span = 1; + + if (seek_time > duration) { + seek_time = duration; + } + else if (seek_time < 0) { + seek_time = 0; + } + seek_time += self->start_time; + + for (int retry = 0; retry < 32; retry++) { + int found_packet_with_pts = FALSE; + int found_packet_in_range = FALSE; + long last_valid_packet_start = -1; + double first_packet_time = PLM_PACKET_INVALID_TS; + + long cur_pos = plm_buffer_tell(self->buffer); + + // Estimate byte offset and jump to it. + long offset = (seek_time - cur_time - scan_span) * byterate; + long seek_pos = cur_pos + offset; + if (seek_pos < 0) { + seek_pos = 0; + } + else if (seek_pos > file_size - 256) { + seek_pos = file_size - 256; + } + + plm_demux_buffer_seek(self, seek_pos); + + // Scan through all packets up to the seek_time to find the last packet + // containing an intra frame. + while (plm_buffer_find_start_code(self->buffer, type) != -1) { + long packet_start = plm_buffer_tell(self->buffer); + plm_packet_t *packet = plm_demux_decode_packet(self, type); + + // Skip packet if it has no PTS + if (!packet || packet->pts == PLM_PACKET_INVALID_TS) { + continue; + } + + // Bail scanning through packets if we hit one that is outside + // seek_time - scan_span. + // We also adjust the cur_time and byterate values here so the next + // iteration can be a bit more precise. + if (packet->pts > seek_time || packet->pts < seek_time - scan_span) { + found_packet_with_pts = TRUE; + byterate = (seek_pos - cur_pos) / (packet->pts - cur_time); + cur_time = packet->pts; + break; + } + + // If we are still here, it means this packet is in close range to + // the seek_time. If this is the first packet for this jump position + // record the PTS. If we later have to back off, when there was no + // intra frame in this range, we can lower the seek_time to not scan + // this range again. + if (!found_packet_in_range) { + found_packet_in_range = TRUE; + first_packet_time = packet->pts; + } + + // Check if this is an intra frame packet. If so, record the buffer + // position of the start of this packet. We want to jump back to it + // later, when we know it's the last intra frame before desired + // seek time. + if (force_intra) { + for (size_t i = 0; i < packet->length - 6; i++) { + // Find the START_PICTURE code + if ( + packet->data[i] == 0x00 && + packet->data[i + 1] == 0x00 && + packet->data[i + 2] == 0x01 && + packet->data[i + 3] == 0x00 + ) { + // Bits 11--13 in the picture header contain the frame + // type, where 1=Intra + if ((packet->data[i + 5] & 0x38) == 8) { + last_valid_packet_start = packet_start; + } + break; + } + } + } + + // If we don't want intra frames, just use the last PTS found. + else { + last_valid_packet_start = packet_start; + } + } + + // If there was at least one intra frame in the range scanned above, + // our search is over. Jump back to the packet and decode it again. + if (last_valid_packet_start != -1) { + plm_demux_buffer_seek(self, last_valid_packet_start); + return plm_demux_decode_packet(self, type); + } + + // If we hit the right range, but still found no intra frame, we have + // to increases the scan_span. This is done exponentially to also handle + // video files with very few intra frames. + else if (found_packet_in_range) { + scan_span *= 2; + seek_time = first_packet_time; + } + + // If we didn't find any packet with a PTS, it probably means we reached + // the end of the file. Estimate byterate and cur_time accordingly. + else if (!found_packet_with_pts) { + byterate = (seek_pos - cur_pos) / (duration - cur_time); + cur_time = duration; + } + } + + return NULL; +} + +plm_packet_t *plm_demux_decode(plm_demux_t *self) { + if (!plm_demux_has_headers(self)) { + return NULL; + } + + if (self->current_packet.length) { + size_t bits_till_next_packet = self->current_packet.length << 3; + if (!plm_buffer_has(self->buffer, bits_till_next_packet)) { + return NULL; + } + plm_buffer_skip(self->buffer, bits_till_next_packet); + self->current_packet.length = 0; + } + + // Pending packet waiting for data? + if (self->next_packet.length) { + return plm_demux_get_packet(self); + } + + // Pending packet waiting for header? + if (self->start_code != -1) { + return plm_demux_decode_packet(self, self->start_code); + } + + do { + self->start_code = plm_buffer_next_start_code(self->buffer); + if ( + self->start_code == PLM_DEMUX_PACKET_VIDEO_1 || + self->start_code == PLM_DEMUX_PACKET_PRIVATE || ( + self->start_code >= PLM_DEMUX_PACKET_AUDIO_1 && + self->start_code <= PLM_DEMUX_PACKET_AUDIO_4 + ) + ) { + return plm_demux_decode_packet(self, self->start_code); + } + } while (self->start_code != -1); + + return NULL; +} + +double plm_demux_decode_time(plm_demux_t *self) { + int64_t clock = plm_buffer_read(self->buffer, 3) << 30; + plm_buffer_skip(self->buffer, 1); + clock |= plm_buffer_read(self->buffer, 15) << 15; + plm_buffer_skip(self->buffer, 1); + clock |= plm_buffer_read(self->buffer, 15); + plm_buffer_skip(self->buffer, 1); + return (double)clock / 90000.0; +} + +plm_packet_t *plm_demux_decode_packet(plm_demux_t *self, int type) { + if (!plm_buffer_has(self->buffer, 16 << 3)) { + return NULL; + } + + self->start_code = -1; + + self->next_packet.type = type; + self->next_packet.length = plm_buffer_read(self->buffer, 16); + self->next_packet.length -= plm_buffer_skip_bytes(self->buffer, 0xff); // stuffing + + // skip P-STD + if (plm_buffer_read(self->buffer, 2) == 0x01) { + plm_buffer_skip(self->buffer, 16); + self->next_packet.length -= 2; + } + + int pts_dts_marker = plm_buffer_read(self->buffer, 2); + if (pts_dts_marker == 0x03) { + self->next_packet.pts = plm_demux_decode_time(self); + self->last_decoded_pts = self->next_packet.pts; + plm_buffer_skip(self->buffer, 40); // skip dts + self->next_packet.length -= 10; + } + else if (pts_dts_marker == 0x02) { + self->next_packet.pts = plm_demux_decode_time(self); + self->last_decoded_pts = self->next_packet.pts; + self->next_packet.length -= 5; + } + else if (pts_dts_marker == 0x00) { + self->next_packet.pts = PLM_PACKET_INVALID_TS; + plm_buffer_skip(self->buffer, 4); + self->next_packet.length -= 1; + } + else { + return NULL; // invalid + } + + return plm_demux_get_packet(self); +} + +plm_packet_t *plm_demux_get_packet(plm_demux_t *self) { + if (!plm_buffer_has(self->buffer, self->next_packet.length << 3)) { + return NULL; + } + + self->current_packet.data = self->buffer->bytes + (self->buffer->bit_index >> 3); + self->current_packet.length = self->next_packet.length; + self->current_packet.type = self->next_packet.type; + self->current_packet.pts = self->next_packet.pts; + + self->next_packet.length = 0; + return &self->current_packet; +} + + + +// ----------------------------------------------------------------------------- +// plm_video implementation + +// Inspired by Java MPEG-1 Video Decoder and Player by Zoltan Korandi +// https://sourceforge.net/projects/javampeg1video/ + +static const int PLM_VIDEO_PICTURE_TYPE_INTRA = 1; +static const int PLM_VIDEO_PICTURE_TYPE_PREDICTIVE = 2; +static const int PLM_VIDEO_PICTURE_TYPE_B = 3; + +static const int PLM_START_SEQUENCE = 0xB3; +static const int PLM_START_SLICE_FIRST = 0x01; +static const int PLM_START_SLICE_LAST = 0xAF; +static const int PLM_START_PICTURE = 0x00; +static const int PLM_START_EXTENSION = 0xB5; +static const int PLM_START_USER_DATA = 0xB2; + +#define PLM_START_IS_SLICE(c) \ + (c >= PLM_START_SLICE_FIRST && c <= PLM_START_SLICE_LAST) + +static const double PLM_VIDEO_PICTURE_RATE[] = { + 0.000, 23.976, 24.000, 25.000, 29.970, 30.000, 50.000, 59.940, + 60.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000 +}; + +static const uint8_t PLM_VIDEO_ZIG_ZAG[] = { + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63 +}; + +static const uint8_t PLM_VIDEO_INTRA_QUANT_MATRIX[] = { + 8, 16, 19, 22, 26, 27, 29, 34, + 16, 16, 22, 24, 27, 29, 34, 37, + 19, 22, 26, 27, 29, 34, 34, 38, + 22, 22, 26, 27, 29, 34, 37, 40, + 22, 26, 27, 29, 32, 35, 40, 48, + 26, 27, 29, 32, 35, 40, 48, 58, + 26, 27, 29, 34, 38, 46, 56, 69, + 27, 29, 35, 38, 46, 56, 69, 83 +}; + +static const uint8_t PLM_VIDEO_NON_INTRA_QUANT_MATRIX[] = { + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16 +}; + +static const uint8_t PLM_VIDEO_PREMULTIPLIER_MATRIX[] = { + 32, 44, 42, 38, 32, 25, 17, 9, + 44, 62, 58, 52, 44, 35, 24, 12, + 42, 58, 55, 49, 42, 33, 23, 12, + 38, 52, 49, 44, 38, 30, 20, 10, + 32, 44, 42, 38, 32, 25, 17, 9, + 25, 35, 33, 30, 25, 20, 14, 7, + 17, 24, 23, 20, 17, 14, 9, 5, + 9, 12, 12, 10, 9, 7, 5, 2 +}; + +static const plm_vlc_t PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT[] = { + { 1 << 1, 0}, { 0, 1}, // 0: x + { 2 << 1, 0}, { 3 << 1, 0}, // 1: 0x + { 4 << 1, 0}, { 5 << 1, 0}, // 2: 00x + { 0, 3}, { 0, 2}, // 3: 01x + { 6 << 1, 0}, { 7 << 1, 0}, // 4: 000x + { 0, 5}, { 0, 4}, // 5: 001x + { 8 << 1, 0}, { 9 << 1, 0}, // 6: 0000x + { 0, 7}, { 0, 6}, // 7: 0001x + { 10 << 1, 0}, { 11 << 1, 0}, // 8: 0000 0x + { 12 << 1, 0}, { 13 << 1, 0}, // 9: 0000 1x + { 14 << 1, 0}, { 15 << 1, 0}, // 10: 0000 00x + { 16 << 1, 0}, { 17 << 1, 0}, // 11: 0000 01x + { 18 << 1, 0}, { 19 << 1, 0}, // 12: 0000 10x + { 0, 9}, { 0, 8}, // 13: 0000 11x + { -1, 0}, { 20 << 1, 0}, // 14: 0000 000x + { -1, 0}, { 21 << 1, 0}, // 15: 0000 001x + { 22 << 1, 0}, { 23 << 1, 0}, // 16: 0000 010x + { 0, 15}, { 0, 14}, // 17: 0000 011x + { 0, 13}, { 0, 12}, // 18: 0000 100x + { 0, 11}, { 0, 10}, // 19: 0000 101x + { 24 << 1, 0}, { 25 << 1, 0}, // 20: 0000 0001x + { 26 << 1, 0}, { 27 << 1, 0}, // 21: 0000 0011x + { 28 << 1, 0}, { 29 << 1, 0}, // 22: 0000 0100x + { 30 << 1, 0}, { 31 << 1, 0}, // 23: 0000 0101x + { 32 << 1, 0}, { -1, 0}, // 24: 0000 0001 0x + { -1, 0}, { 33 << 1, 0}, // 25: 0000 0001 1x + { 34 << 1, 0}, { 35 << 1, 0}, // 26: 0000 0011 0x + { 36 << 1, 0}, { 37 << 1, 0}, // 27: 0000 0011 1x + { 38 << 1, 0}, { 39 << 1, 0}, // 28: 0000 0100 0x + { 0, 21}, { 0, 20}, // 29: 0000 0100 1x + { 0, 19}, { 0, 18}, // 30: 0000 0101 0x + { 0, 17}, { 0, 16}, // 31: 0000 0101 1x + { 0, 35}, { -1, 0}, // 32: 0000 0001 00x + { -1, 0}, { 0, 34}, // 33: 0000 0001 11x + { 0, 33}, { 0, 32}, // 34: 0000 0011 00x + { 0, 31}, { 0, 30}, // 35: 0000 0011 01x + { 0, 29}, { 0, 28}, // 36: 0000 0011 10x + { 0, 27}, { 0, 26}, // 37: 0000 0011 11x + { 0, 25}, { 0, 24}, // 38: 0000 0100 00x + { 0, 23}, { 0, 22}, // 39: 0000 0100 01x +}; + +static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_INTRA[] = { + { 1 << 1, 0}, { 0, 0x01}, // 0: x + { -1, 0}, { 0, 0x11}, // 1: 0x +}; + +static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_PREDICTIVE[] = { + { 1 << 1, 0}, { 0, 0x0a}, // 0: x + { 2 << 1, 0}, { 0, 0x02}, // 1: 0x + { 3 << 1, 0}, { 0, 0x08}, // 2: 00x + { 4 << 1, 0}, { 5 << 1, 0}, // 3: 000x + { 6 << 1, 0}, { 0, 0x12}, // 4: 0000x + { 0, 0x1a}, { 0, 0x01}, // 5: 0001x + { -1, 0}, { 0, 0x11}, // 6: 0000 0x +}; + +static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_B[] = { + { 1 << 1, 0}, { 2 << 1, 0}, // 0: x + { 3 << 1, 0}, { 4 << 1, 0}, // 1: 0x + { 0, 0x0c}, { 0, 0x0e}, // 2: 1x + { 5 << 1, 0}, { 6 << 1, 0}, // 3: 00x + { 0, 0x04}, { 0, 0x06}, // 4: 01x + { 7 << 1, 0}, { 8 << 1, 0}, // 5: 000x + { 0, 0x08}, { 0, 0x0a}, // 6: 001x + { 9 << 1, 0}, { 10 << 1, 0}, // 7: 0000x + { 0, 0x1e}, { 0, 0x01}, // 8: 0001x + { -1, 0}, { 0, 0x11}, // 9: 0000 0x + { 0, 0x16}, { 0, 0x1a}, // 10: 0000 1x +}; + +static const plm_vlc_t *PLM_VIDEO_MACROBLOCK_TYPE[] = { + NULL, + PLM_VIDEO_MACROBLOCK_TYPE_INTRA, + PLM_VIDEO_MACROBLOCK_TYPE_PREDICTIVE, + PLM_VIDEO_MACROBLOCK_TYPE_B +}; + +static const plm_vlc_t PLM_VIDEO_CODE_BLOCK_PATTERN[] = { + { 1 << 1, 0}, { 2 << 1, 0}, // 0: x + { 3 << 1, 0}, { 4 << 1, 0}, // 1: 0x + { 5 << 1, 0}, { 6 << 1, 0}, // 2: 1x + { 7 << 1, 0}, { 8 << 1, 0}, // 3: 00x + { 9 << 1, 0}, { 10 << 1, 0}, // 4: 01x + { 11 << 1, 0}, { 12 << 1, 0}, // 5: 10x + { 13 << 1, 0}, { 0, 60}, // 6: 11x + { 14 << 1, 0}, { 15 << 1, 0}, // 7: 000x + { 16 << 1, 0}, { 17 << 1, 0}, // 8: 001x + { 18 << 1, 0}, { 19 << 1, 0}, // 9: 010x + { 20 << 1, 0}, { 21 << 1, 0}, // 10: 011x + { 22 << 1, 0}, { 23 << 1, 0}, // 11: 100x + { 0, 32}, { 0, 16}, // 12: 101x + { 0, 8}, { 0, 4}, // 13: 110x + { 24 << 1, 0}, { 25 << 1, 0}, // 14: 0000x + { 26 << 1, 0}, { 27 << 1, 0}, // 15: 0001x + { 28 << 1, 0}, { 29 << 1, 0}, // 16: 0010x + { 30 << 1, 0}, { 31 << 1, 0}, // 17: 0011x + { 0, 62}, { 0, 2}, // 18: 0100x + { 0, 61}, { 0, 1}, // 19: 0101x + { 0, 56}, { 0, 52}, // 20: 0110x + { 0, 44}, { 0, 28}, // 21: 0111x + { 0, 40}, { 0, 20}, // 22: 1000x + { 0, 48}, { 0, 12}, // 23: 1001x + { 32 << 1, 0}, { 33 << 1, 0}, // 24: 0000 0x + { 34 << 1, 0}, { 35 << 1, 0}, // 25: 0000 1x + { 36 << 1, 0}, { 37 << 1, 0}, // 26: 0001 0x + { 38 << 1, 0}, { 39 << 1, 0}, // 27: 0001 1x + { 40 << 1, 0}, { 41 << 1, 0}, // 28: 0010 0x + { 42 << 1, 0}, { 43 << 1, 0}, // 29: 0010 1x + { 0, 63}, { 0, 3}, // 30: 0011 0x + { 0, 36}, { 0, 24}, // 31: 0011 1x + { 44 << 1, 0}, { 45 << 1, 0}, // 32: 0000 00x + { 46 << 1, 0}, { 47 << 1, 0}, // 33: 0000 01x + { 48 << 1, 0}, { 49 << 1, 0}, // 34: 0000 10x + { 50 << 1, 0}, { 51 << 1, 0}, // 35: 0000 11x + { 52 << 1, 0}, { 53 << 1, 0}, // 36: 0001 00x + { 54 << 1, 0}, { 55 << 1, 0}, // 37: 0001 01x + { 56 << 1, 0}, { 57 << 1, 0}, // 38: 0001 10x + { 58 << 1, 0}, { 59 << 1, 0}, // 39: 0001 11x + { 0, 34}, { 0, 18}, // 40: 0010 00x + { 0, 10}, { 0, 6}, // 41: 0010 01x + { 0, 33}, { 0, 17}, // 42: 0010 10x + { 0, 9}, { 0, 5}, // 43: 0010 11x + { -1, 0}, { 60 << 1, 0}, // 44: 0000 000x + { 61 << 1, 0}, { 62 << 1, 0}, // 45: 0000 001x + { 0, 58}, { 0, 54}, // 46: 0000 010x + { 0, 46}, { 0, 30}, // 47: 0000 011x + { 0, 57}, { 0, 53}, // 48: 0000 100x + { 0, 45}, { 0, 29}, // 49: 0000 101x + { 0, 38}, { 0, 26}, // 50: 0000 110x + { 0, 37}, { 0, 25}, // 51: 0000 111x + { 0, 43}, { 0, 23}, // 52: 0001 000x + { 0, 51}, { 0, 15}, // 53: 0001 001x + { 0, 42}, { 0, 22}, // 54: 0001 010x + { 0, 50}, { 0, 14}, // 55: 0001 011x + { 0, 41}, { 0, 21}, // 56: 0001 100x + { 0, 49}, { 0, 13}, // 57: 0001 101x + { 0, 35}, { 0, 19}, // 58: 0001 110x + { 0, 11}, { 0, 7}, // 59: 0001 111x + { 0, 39}, { 0, 27}, // 60: 0000 0001x + { 0, 59}, { 0, 55}, // 61: 0000 0010x + { 0, 47}, { 0, 31}, // 62: 0000 0011x +}; + +static const plm_vlc_t PLM_VIDEO_MOTION[] = { + { 1 << 1, 0}, { 0, 0}, // 0: x + { 2 << 1, 0}, { 3 << 1, 0}, // 1: 0x + { 4 << 1, 0}, { 5 << 1, 0}, // 2: 00x + { 0, 1}, { 0, -1}, // 3: 01x + { 6 << 1, 0}, { 7 << 1, 0}, // 4: 000x + { 0, 2}, { 0, -2}, // 5: 001x + { 8 << 1, 0}, { 9 << 1, 0}, // 6: 0000x + { 0, 3}, { 0, -3}, // 7: 0001x + { 10 << 1, 0}, { 11 << 1, 0}, // 8: 0000 0x + { 12 << 1, 0}, { 13 << 1, 0}, // 9: 0000 1x + { -1, 0}, { 14 << 1, 0}, // 10: 0000 00x + { 15 << 1, 0}, { 16 << 1, 0}, // 11: 0000 01x + { 17 << 1, 0}, { 18 << 1, 0}, // 12: 0000 10x + { 0, 4}, { 0, -4}, // 13: 0000 11x + { -1, 0}, { 19 << 1, 0}, // 14: 0000 001x + { 20 << 1, 0}, { 21 << 1, 0}, // 15: 0000 010x + { 0, 7}, { 0, -7}, // 16: 0000 011x + { 0, 6}, { 0, -6}, // 17: 0000 100x + { 0, 5}, { 0, -5}, // 18: 0000 101x + { 22 << 1, 0}, { 23 << 1, 0}, // 19: 0000 0011x + { 24 << 1, 0}, { 25 << 1, 0}, // 20: 0000 0100x + { 26 << 1, 0}, { 27 << 1, 0}, // 21: 0000 0101x + { 28 << 1, 0}, { 29 << 1, 0}, // 22: 0000 0011 0x + { 30 << 1, 0}, { 31 << 1, 0}, // 23: 0000 0011 1x + { 32 << 1, 0}, { 33 << 1, 0}, // 24: 0000 0100 0x + { 0, 10}, { 0, -10}, // 25: 0000 0100 1x + { 0, 9}, { 0, -9}, // 26: 0000 0101 0x + { 0, 8}, { 0, -8}, // 27: 0000 0101 1x + { 0, 16}, { 0, -16}, // 28: 0000 0011 00x + { 0, 15}, { 0, -15}, // 29: 0000 0011 01x + { 0, 14}, { 0, -14}, // 30: 0000 0011 10x + { 0, 13}, { 0, -13}, // 31: 0000 0011 11x + { 0, 12}, { 0, -12}, // 32: 0000 0100 00x + { 0, 11}, { 0, -11}, // 33: 0000 0100 01x +}; + +static const plm_vlc_t PLM_VIDEO_DCT_SIZE_LUMINANCE[] = { + { 1 << 1, 0}, { 2 << 1, 0}, // 0: x + { 0, 1}, { 0, 2}, // 1: 0x + { 3 << 1, 0}, { 4 << 1, 0}, // 2: 1x + { 0, 0}, { 0, 3}, // 3: 10x + { 0, 4}, { 5 << 1, 0}, // 4: 11x + { 0, 5}, { 6 << 1, 0}, // 5: 111x + { 0, 6}, { 7 << 1, 0}, // 6: 1111x + { 0, 7}, { 8 << 1, 0}, // 7: 1111 1x + { 0, 8}, { -1, 0}, // 8: 1111 11x +}; + +static const plm_vlc_t PLM_VIDEO_DCT_SIZE_CHROMINANCE[] = { + { 1 << 1, 0}, { 2 << 1, 0}, // 0: x + { 0, 0}, { 0, 1}, // 1: 0x + { 0, 2}, { 3 << 1, 0}, // 2: 1x + { 0, 3}, { 4 << 1, 0}, // 3: 11x + { 0, 4}, { 5 << 1, 0}, // 4: 111x + { 0, 5}, { 6 << 1, 0}, // 5: 1111x + { 0, 6}, { 7 << 1, 0}, // 6: 1111 1x + { 0, 7}, { 8 << 1, 0}, // 7: 1111 11x + { 0, 8}, { -1, 0}, // 8: 1111 111x +}; + +static const plm_vlc_t *PLM_VIDEO_DCT_SIZE[] = { + PLM_VIDEO_DCT_SIZE_LUMINANCE, + PLM_VIDEO_DCT_SIZE_CHROMINANCE, + PLM_VIDEO_DCT_SIZE_CHROMINANCE +}; + + +// dct_coeff bitmap: +// 0xff00 run +// 0x00ff level + +// Decoded values are unsigned. Sign bit follows in the stream. + +static const plm_vlc_uint_t PLM_VIDEO_DCT_COEFF[] = { + { 1 << 1, 0}, { 0, 0x0001}, // 0: x + { 2 << 1, 0}, { 3 << 1, 0}, // 1: 0x + { 4 << 1, 0}, { 5 << 1, 0}, // 2: 00x + { 6 << 1, 0}, { 0, 0x0101}, // 3: 01x + { 7 << 1, 0}, { 8 << 1, 0}, // 4: 000x + { 9 << 1, 0}, { 10 << 1, 0}, // 5: 001x + { 0, 0x0002}, { 0, 0x0201}, // 6: 010x + { 11 << 1, 0}, { 12 << 1, 0}, // 7: 0000x + { 13 << 1, 0}, { 14 << 1, 0}, // 8: 0001x + { 15 << 1, 0}, { 0, 0x0003}, // 9: 0010x + { 0, 0x0401}, { 0, 0x0301}, // 10: 0011x + { 16 << 1, 0}, { 0, 0xffff}, // 11: 0000 0x + { 17 << 1, 0}, { 18 << 1, 0}, // 12: 0000 1x + { 0, 0x0701}, { 0, 0x0601}, // 13: 0001 0x + { 0, 0x0102}, { 0, 0x0501}, // 14: 0001 1x + { 19 << 1, 0}, { 20 << 1, 0}, // 15: 0010 0x + { 21 << 1, 0}, { 22 << 1, 0}, // 16: 0000 00x + { 0, 0x0202}, { 0, 0x0901}, // 17: 0000 10x + { 0, 0x0004}, { 0, 0x0801}, // 18: 0000 11x + { 23 << 1, 0}, { 24 << 1, 0}, // 19: 0010 00x + { 25 << 1, 0}, { 26 << 1, 0}, // 20: 0010 01x + { 27 << 1, 0}, { 28 << 1, 0}, // 21: 0000 000x + { 29 << 1, 0}, { 30 << 1, 0}, // 22: 0000 001x + { 0, 0x0d01}, { 0, 0x0006}, // 23: 0010 000x + { 0, 0x0c01}, { 0, 0x0b01}, // 24: 0010 001x + { 0, 0x0302}, { 0, 0x0103}, // 25: 0010 010x + { 0, 0x0005}, { 0, 0x0a01}, // 26: 0010 011x + { 31 << 1, 0}, { 32 << 1, 0}, // 27: 0000 0000x + { 33 << 1, 0}, { 34 << 1, 0}, // 28: 0000 0001x + { 35 << 1, 0}, { 36 << 1, 0}, // 29: 0000 0010x + { 37 << 1, 0}, { 38 << 1, 0}, // 30: 0000 0011x + { 39 << 1, 0}, { 40 << 1, 0}, // 31: 0000 0000 0x + { 41 << 1, 0}, { 42 << 1, 0}, // 32: 0000 0000 1x + { 43 << 1, 0}, { 44 << 1, 0}, // 33: 0000 0001 0x + { 45 << 1, 0}, { 46 << 1, 0}, // 34: 0000 0001 1x + { 0, 0x1001}, { 0, 0x0502}, // 35: 0000 0010 0x + { 0, 0x0007}, { 0, 0x0203}, // 36: 0000 0010 1x + { 0, 0x0104}, { 0, 0x0f01}, // 37: 0000 0011 0x + { 0, 0x0e01}, { 0, 0x0402}, // 38: 0000 0011 1x + { 47 << 1, 0}, { 48 << 1, 0}, // 39: 0000 0000 00x + { 49 << 1, 0}, { 50 << 1, 0}, // 40: 0000 0000 01x + { 51 << 1, 0}, { 52 << 1, 0}, // 41: 0000 0000 10x + { 53 << 1, 0}, { 54 << 1, 0}, // 42: 0000 0000 11x + { 55 << 1, 0}, { 56 << 1, 0}, // 43: 0000 0001 00x + { 57 << 1, 0}, { 58 << 1, 0}, // 44: 0000 0001 01x + { 59 << 1, 0}, { 60 << 1, 0}, // 45: 0000 0001 10x + { 61 << 1, 0}, { 62 << 1, 0}, // 46: 0000 0001 11x + { -1, 0}, { 63 << 1, 0}, // 47: 0000 0000 000x + { 64 << 1, 0}, { 65 << 1, 0}, // 48: 0000 0000 001x + { 66 << 1, 0}, { 67 << 1, 0}, // 49: 0000 0000 010x + { 68 << 1, 0}, { 69 << 1, 0}, // 50: 0000 0000 011x + { 70 << 1, 0}, { 71 << 1, 0}, // 51: 0000 0000 100x + { 72 << 1, 0}, { 73 << 1, 0}, // 52: 0000 0000 101x + { 74 << 1, 0}, { 75 << 1, 0}, // 53: 0000 0000 110x + { 76 << 1, 0}, { 77 << 1, 0}, // 54: 0000 0000 111x + { 0, 0x000b}, { 0, 0x0802}, // 55: 0000 0001 000x + { 0, 0x0403}, { 0, 0x000a}, // 56: 0000 0001 001x + { 0, 0x0204}, { 0, 0x0702}, // 57: 0000 0001 010x + { 0, 0x1501}, { 0, 0x1401}, // 58: 0000 0001 011x + { 0, 0x0009}, { 0, 0x1301}, // 59: 0000 0001 100x + { 0, 0x1201}, { 0, 0x0105}, // 60: 0000 0001 101x + { 0, 0x0303}, { 0, 0x0008}, // 61: 0000 0001 110x + { 0, 0x0602}, { 0, 0x1101}, // 62: 0000 0001 111x + { 78 << 1, 0}, { 79 << 1, 0}, // 63: 0000 0000 0001x + { 80 << 1, 0}, { 81 << 1, 0}, // 64: 0000 0000 0010x + { 82 << 1, 0}, { 83 << 1, 0}, // 65: 0000 0000 0011x + { 84 << 1, 0}, { 85 << 1, 0}, // 66: 0000 0000 0100x + { 86 << 1, 0}, { 87 << 1, 0}, // 67: 0000 0000 0101x + { 88 << 1, 0}, { 89 << 1, 0}, // 68: 0000 0000 0110x + { 90 << 1, 0}, { 91 << 1, 0}, // 69: 0000 0000 0111x + { 0, 0x0a02}, { 0, 0x0902}, // 70: 0000 0000 1000x + { 0, 0x0503}, { 0, 0x0304}, // 71: 0000 0000 1001x + { 0, 0x0205}, { 0, 0x0107}, // 72: 0000 0000 1010x + { 0, 0x0106}, { 0, 0x000f}, // 73: 0000 0000 1011x + { 0, 0x000e}, { 0, 0x000d}, // 74: 0000 0000 1100x + { 0, 0x000c}, { 0, 0x1a01}, // 75: 0000 0000 1101x + { 0, 0x1901}, { 0, 0x1801}, // 76: 0000 0000 1110x + { 0, 0x1701}, { 0, 0x1601}, // 77: 0000 0000 1111x + { 92 << 1, 0}, { 93 << 1, 0}, // 78: 0000 0000 0001 0x + { 94 << 1, 0}, { 95 << 1, 0}, // 79: 0000 0000 0001 1x + { 96 << 1, 0}, { 97 << 1, 0}, // 80: 0000 0000 0010 0x + { 98 << 1, 0}, { 99 << 1, 0}, // 81: 0000 0000 0010 1x + {100 << 1, 0}, {101 << 1, 0}, // 82: 0000 0000 0011 0x + {102 << 1, 0}, {103 << 1, 0}, // 83: 0000 0000 0011 1x + { 0, 0x001f}, { 0, 0x001e}, // 84: 0000 0000 0100 0x + { 0, 0x001d}, { 0, 0x001c}, // 85: 0000 0000 0100 1x + { 0, 0x001b}, { 0, 0x001a}, // 86: 0000 0000 0101 0x + { 0, 0x0019}, { 0, 0x0018}, // 87: 0000 0000 0101 1x + { 0, 0x0017}, { 0, 0x0016}, // 88: 0000 0000 0110 0x + { 0, 0x0015}, { 0, 0x0014}, // 89: 0000 0000 0110 1x + { 0, 0x0013}, { 0, 0x0012}, // 90: 0000 0000 0111 0x + { 0, 0x0011}, { 0, 0x0010}, // 91: 0000 0000 0111 1x + {104 << 1, 0}, {105 << 1, 0}, // 92: 0000 0000 0001 00x + {106 << 1, 0}, {107 << 1, 0}, // 93: 0000 0000 0001 01x + {108 << 1, 0}, {109 << 1, 0}, // 94: 0000 0000 0001 10x + {110 << 1, 0}, {111 << 1, 0}, // 95: 0000 0000 0001 11x + { 0, 0x0028}, { 0, 0x0027}, // 96: 0000 0000 0010 00x + { 0, 0x0026}, { 0, 0x0025}, // 97: 0000 0000 0010 01x + { 0, 0x0024}, { 0, 0x0023}, // 98: 0000 0000 0010 10x + { 0, 0x0022}, { 0, 0x0021}, // 99: 0000 0000 0010 11x + { 0, 0x0020}, { 0, 0x010e}, // 100: 0000 0000 0011 00x + { 0, 0x010d}, { 0, 0x010c}, // 101: 0000 0000 0011 01x + { 0, 0x010b}, { 0, 0x010a}, // 102: 0000 0000 0011 10x + { 0, 0x0109}, { 0, 0x0108}, // 103: 0000 0000 0011 11x + { 0, 0x0112}, { 0, 0x0111}, // 104: 0000 0000 0001 000x + { 0, 0x0110}, { 0, 0x010f}, // 105: 0000 0000 0001 001x + { 0, 0x0603}, { 0, 0x1002}, // 106: 0000 0000 0001 010x + { 0, 0x0f02}, { 0, 0x0e02}, // 107: 0000 0000 0001 011x + { 0, 0x0d02}, { 0, 0x0c02}, // 108: 0000 0000 0001 100x + { 0, 0x0b02}, { 0, 0x1f01}, // 109: 0000 0000 0001 101x + { 0, 0x1e01}, { 0, 0x1d01}, // 110: 0000 0000 0001 110x + { 0, 0x1c01}, { 0, 0x1b01}, // 111: 0000 0000 0001 111x +}; + +typedef struct { + int full_px; + int is_set; + int r_size; + int h; + int v; +} plm_video_motion_t; + +typedef enum { + PLM_POLL_STATE_IDLE = 0, // no active picture; seek next picture start + PLM_POLL_STATE_HEADER_PARSED, // header parsed; waiting for a free decode buffer + PLM_POLL_STATE_IN_PICTURE, // decoding slices for the current picture +} plm_video_poll_state_t; + +typedef struct plm_video_t { + double framerate; + double time; + int frames_decoded; + int width; + int height; + int mb_width; + int mb_height; + int mb_size; + + int luma_width; + int luma_height; + + int chroma_width; + int chroma_height; + + int start_code; + int picture_type; + + // Sequence header aspect_ratio_information code (4 bits, MPEG-1) + int aspect_ratio_code; + // Libdragon proprietary SAR parsed from MPEG-1 user_data (LD_SAR=NUM:DEN). + // Defaults to 1:1 (square pixels). + int sar_num; + int sar_den; + int sar_parsed; + + plm_video_motion_t motion_forward; + plm_video_motion_t motion_backward; + + int has_sequence_header; + + int quantizer_scale; + int slice_begin; + int macroblock_address; + + int mb_row; + int mb_col; + + int macroblock_type; + int macroblock_intra; + + int dc_predictor[3]; + + plm_buffer_t *buffer; + int destroy_buffer_when_done; + + plm_frame_t frame_current; + plm_frame_t frame_forward; + plm_frame_t frame_backward; + + uint8_t *frames_data; + size_t frame_data_size; + int num_frame_buffers; + uint8_t *frame_locked; // [num_frame_buffers] 1 if underlying buffer is locked (queued or held) + + // libdragon extension: output buffering + incremental polling + int buffered_pics; // requested output queue capacity (>=1) + plm_frame_t *out_queue; // [buffered_pics] frames (by value, pointers into frames_data) + int out_q_head; + int out_q_tail; + int out_q_count; + int out_current_in_use; + plm_frame_t out_current_frame; // returned by plm_video_next_output() + + // Incremental picture decode state (poll) + plm_video_poll_state_t poll_state; + plm_frame_t poll_frame_temp; // saved from picture start (old frame_forward) + int poll_saved_current_valid; // for B pictures only + plm_frame_t poll_saved_current; // original frame_current to restore after B decode + + int16_t block_data[64]; + uint8_t intra_quant_matrix[64]; + uint8_t non_intra_quant_matrix[64]; + + int has_reference_frame; + int assume_no_b_frames; +} plm_video_t; + +static inline uint8_t plm_clamp(int n) { + if (n > 255) { + n = 255; + } + else if (n < 0) { + n = 0; + } + return n; +} + +int plm_video_decode_sequence_header(plm_video_t *self); +void plm_video_init_frame(plm_video_t *self, plm_frame_t *frame, uint8_t *base); +void plm_video_decode_slice(plm_video_t *self, int slice); +void plm_video_decode_macroblock(plm_video_t *self); +void plm_video_decode_motion_vectors(plm_video_t *self); +int plm_video_decode_motion_vector(plm_video_t *self, int r_size, int motion); +void plm_video_predict_macroblock(plm_video_t *self); +void plm_video_copy_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v); +void plm_video_interpolate_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v); +void plm_video_copy_macroblock_rsp(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v); +void plm_video_interpolate_macroblock_rsp(plm_video_t *self, plm_frame_t *s1, int motion_h1, int motion_v1, plm_frame_t *s2, int motion_h2, int motion_v2); +void plm_video_process_macroblock(plm_video_t *self, uint8_t *s, uint8_t *d, int mh, int mb, int bs, int interp); +void plm_video_decode_block(plm_video_t *self, int block); +void plm_video_decode_block_residual(int16_t *s, int si, uint8_t *d, int di, int dw, int n, int intra); +void plm_video_idct(int16_t *block); + +static void plm_video_parse_initial_userdata(plm_video_t *self) { + // Parse libdragon proprietary SAR from an initial user_data block: + // 00 00 01 B2 "LD_SAR=NUM:DEN\n" + // + // This is intentionally done lazily (not in create_with_buffer) to avoid + // triggering I/O during object creation. We only support this scan for + // buffers that can seek to 0 (file / fixed memory / append). + if (self->sar_parsed) return; + self->sar_parsed = TRUE; + + if (self->buffer->mode == PLM_BUFFER_MODE_RING) return; + + size_t prev_pos = plm_buffer_tell(self->buffer); + plm_buffer_seek(self->buffer, 0); + + while (TRUE) { + int code = plm_buffer_next_start_code(self->buffer); + if (code == -1) break; + if (code == PLM_START_SEQUENCE) break; + if (code == PLM_START_USER_DATA) { // user_data + char tmp[128]; + int len = 0; + while (len < (int)sizeof(tmp) - 1 && plm_buffer_has(self->buffer, 8)) { + int c = plm_buffer_read(self->buffer, 8); + tmp[len++] = (char)c; + if (c == '\n') break; + } + tmp[len] = 0; + int n = 0, d = 0; + if (sscanf(tmp, "LD_SAR=%d:%d", &n, &d) == 2 && n > 0 && d > 0) { + self->sar_num = n; + self->sar_den = d; + } + } + } + + plm_buffer_seek(self->buffer, prev_pos); +} + +plm_video_t * plm_video_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done) { + plm_video_t *self = (plm_video_t *)malloc(sizeof(plm_video_t)); + assertf(self, "Out of memory"); + memset(self, 0, sizeof(plm_video_t)); + + self->buffer = buffer; + self->destroy_buffer_when_done = destroy_when_done; + + // Defaults + self->sar_num = 1; + self->sar_den = 1; + self->sar_parsed = FALSE; + self->aspect_ratio_code = 1; + self->buffered_pics = 1; + + // Do not scan/decode here: avoid triggering I/O during object creation. + self->start_code = -1; + return self; +} + +void plm_video_destroy(plm_video_t *self) { + if (self->destroy_buffer_when_done) { + plm_buffer_destroy(self->buffer); + } + + if (self->has_sequence_header) { + free(self->frame_locked); + free(self->out_queue); + if (RSP_MODE >= 2) + free_uncached(self->frames_data); + else + free(self->frames_data); + } + + free(self); +} + +double plm_video_get_framerate(plm_video_t *self) { + return plm_video_has_header(self) + ? self->framerate + : 0; +} + +int plm_video_get_width(plm_video_t *self) { + return plm_video_has_header(self) + ? self->width + : 0; +} + +int plm_video_get_height(plm_video_t *self) { + return plm_video_has_header(self) + ? self->height + : 0; +} + +double plm_video_get_aspect_ratio(plm_video_t *self) { + if (!plm_video_has_header(self)) { + return 0.0; + } + + // Prefer proprietary SAR if present. + if (self->sar_num > 0 && self->sar_den > 0 && (self->sar_num != 1 || self->sar_den != 1)) { + return ((double)self->width * (double)self->sar_num) / ((double)self->height * (double)self->sar_den); + } + + // Fall back to MPEG-1 sequence header aspect code. + switch (self->aspect_ratio_code) { + case 2: return 4.0 / 3.0; + case 3: return 16.0 / 9.0; + case 4: return 2.21; // approximate + case 1: + default: + return (double)self->width / (double)self->height; + } +} + +void plm_video_set_no_delay(plm_video_t *self, int no_delay) { + self->assume_no_b_frames = no_delay; +} + +double plm_video_get_time(plm_video_t *self) { + return self->time; +} + +void plm_video_set_time(plm_video_t *self, double time) { + self->frames_decoded = self->framerate * time; + self->time = time; +} + +void plm_video_rewind(plm_video_t *self) { + plm_buffer_rewind(self->buffer); + self->time = 0; + self->frames_decoded = 0; + self->has_reference_frame = FALSE; + self->start_code = -1; + + // Reset libdragon extension state + self->out_q_head = self->out_q_tail = self->out_q_count = 0; + self->out_current_in_use = 0; + self->poll_state = PLM_POLL_STATE_IDLE; + self->poll_saved_current_valid = 0; + if (self->frame_locked && self->num_frame_buffers > 0) { + memset(self->frame_locked, 0, (size_t)self->num_frame_buffers); + } +} + +void plm_video_set_buffered_pics(plm_video_t *self, int buffered_pics) { + if (!self) return; + // Must be set before sequence header is decoded (before frames_data allocation). + ASSERT(!self->has_sequence_header); + if (buffered_pics < 1) buffered_pics = 1; + self->buffered_pics = buffered_pics; +} + +int plm_video_has_ended(plm_video_t *self) { + return plm_buffer_has_ended(self->buffer); +} + +plm_frame_t *plm_video_decode(plm_video_t *self) { + if (!self) return NULL; + for (;;) { + plm_frame_t *out = plm_video_next_output(self); + if (out) { + return out; + } + if (!plm_video_poll(self)) { + return NULL; + } + } +} + +static inline int plm_video_out_full(plm_video_t *self) { + return self->buffered_pics > 0 && + (self->out_q_count + (self->out_current_in_use ? 1 : 0)) >= self->buffered_pics; +} + +static inline int plm_video_buf_index_from_y(plm_video_t *self, uint8_t *y) { + if (!self->frames_data || self->frame_data_size == 0) return -1; + ptrdiff_t off = (ptrdiff_t)(y - self->frames_data); + if (off < 0) return -1; + int idx = (int)(off / (ptrdiff_t)self->frame_data_size); + if (idx < 0 || idx >= self->num_frame_buffers) return -1; + return idx; +} + +static inline int plm_video_is_reference_index(plm_video_t *self, int idx) { + int cur = plm_video_buf_index_from_y(self, self->frame_current.y.data); + int fwd = plm_video_buf_index_from_y(self, self->frame_forward.y.data); + int bwd = plm_video_buf_index_from_y(self, self->frame_backward.y.data); + return idx == cur || idx == fwd || idx == bwd; +} + +static inline int plm_video_is_frame_locked(plm_video_t *self, const plm_frame_t *frame) { + int idx = plm_video_buf_index_from_y(self, frame->y.data); + if (idx < 0 || !self->frame_locked) return 0; + return self->frame_locked[idx] != 0; +} + +static inline int plm_video_parse_picture_header(plm_video_t *self) { + plm_buffer_skip(self->buffer, 10); // temporalReference + self->picture_type = plm_buffer_read(self->buffer, 3); + plm_buffer_skip(self->buffer, 16); // vbv_delay + + if (self->picture_type <= 0 || self->picture_type > PLM_VIDEO_PICTURE_TYPE_B) { + return 0; + } + + if ( + self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE || + self->picture_type == PLM_VIDEO_PICTURE_TYPE_B + ) { + self->motion_forward.full_px = plm_buffer_read(self->buffer, 1); + int f_code = plm_buffer_read(self->buffer, 3); + if (f_code == 0) { + return 0; + } + self->motion_forward.r_size = f_code - 1; + } + + if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) { + self->motion_backward.full_px = plm_buffer_read(self->buffer, 1); + int f_code = plm_buffer_read(self->buffer, 3); + if (f_code == 0) { + return 0; + } + self->motion_backward.r_size = f_code - 1; + } + + return 1; +} + +static inline plm_frame_t plm_video_begin_picture(plm_video_t *self) { + plm_frame_t frame_temp = self->frame_forward; + if ( + self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA || + self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE + ) { + self->frame_forward = self->frame_backward; + } + + // Find first slice start code; skip extension and user data + do { + self->start_code = plm_buffer_next_start_code(self->buffer); + } while ( + self->start_code == PLM_START_EXTENSION || + self->start_code == PLM_START_USER_DATA + ); + + return frame_temp; +} + +static inline void plm_video_finish_picture(plm_video_t *self, plm_frame_t frame_temp) { + if ( + self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA || + self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE + ) { + self->frame_backward = self->frame_current; + self->frame_current = frame_temp; + } +} + +static void plm_video_lock_frame(plm_video_t *self, const plm_frame_t *frame) { + int idx = plm_video_buf_index_from_y(self, frame->y.data); + if (idx >= 0) { + ASSERT(self->frame_locked); + ASSERT(self->frame_locked[idx] == 0); + self->frame_locked[idx] = 1; + } +} + +static void plm_video_unlock_frame(plm_video_t *self, const plm_frame_t *frame) { + int idx = plm_video_buf_index_from_y(self, frame->y.data); + if (idx >= 0) { + ASSERT(self->frame_locked); + ASSERT(self->frame_locked[idx] == 1); + self->frame_locked[idx] = 0; + } +} + +static int plm_video_find_free_b_buffer(plm_video_t *self) { + if (self->buffered_pics <= 0) return -1; + // Extra buffers are appended after the 3 reference buffers. + for (int i = 3; i < self->num_frame_buffers; i++) { + if (self->frame_locked[i] == 0 && !plm_video_is_reference_index(self, i)) + return i; + } + return -1; +} + +static void plm_video_enqueue_output(plm_video_t *self, const plm_frame_t *frame) { + ASSERT(self->buffered_pics > 0); + ASSERT(self->out_queue); + ASSERT(self->out_q_count < self->buffered_pics); + self->out_queue[self->out_q_tail] = *frame; + self->out_q_tail = (self->out_q_tail + 1) % self->buffered_pics; + self->out_q_count++; + plm_video_lock_frame(self, frame); +} + +static inline void plm_video_emit_output(plm_video_t *self, const plm_frame_t *frame) { + plm_video_enqueue_output(self, frame); + self->out_queue[(self->out_q_tail + self->buffered_pics - 1) % self->buffered_pics].time = self->time; + self->frames_decoded++; + self->time = (double)self->frames_decoded / self->framerate; +} + +plm_frame_t *plm_video_next_output(plm_video_t *self) { + if (!self || self->buffered_pics <= 0) return NULL; + if (self->out_q_count == 0) return NULL; + ASSERT(!self->out_current_in_use); + + self->out_current_frame = self->out_queue[self->out_q_head]; + self->out_q_head = (self->out_q_head + 1) % self->buffered_pics; + self->out_q_count--; + self->out_current_in_use = 1; + return &self->out_current_frame; +} + +void plm_video_release_output(plm_video_t *self, const plm_frame_t *frame) { + if (!self || !frame) return; + // We currently support releasing only the last extracted output frame. + ASSERT(self->out_current_in_use); + ASSERT(frame == &self->out_current_frame); + plm_video_unlock_frame(self, frame); + self->out_current_in_use = 0; +} + +int plm_video_poll(plm_video_t *self) { + if (!self) return 0; + if (self->buffered_pics <= 0) return 0; + if (!plm_video_has_header(self)) return 0; + if (plm_video_out_full(self)) return 0; + + int redirect_b = 0; + int redirect_ip = 0; + int redirect_idx = -1; + + // If not currently decoding a picture, advance to the next picture start. + if (self->poll_state == PLM_POLL_STATE_IDLE) { + if (self->start_code != PLM_START_PICTURE) { + PROFILE_START(PS_MPEG_FINDSTART); + self->start_code = plm_buffer_find_start_code(self->buffer, PLM_START_PICTURE); + PROFILE_STOP(PS_MPEG_FINDSTART); + + if (self->start_code == -1) { + // End-of-stream flush: return last reference frame once, same as plm_video_decode(). + if ( + self->has_reference_frame && + !self->assume_no_b_frames && + plm_buffer_has_ended(self->buffer) && ( + self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA || + self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE + ) + ) { + self->has_reference_frame = FALSE; + plm_video_emit_output(self, &self->frame_backward); + return 1; + } + return 0; + } + } + + // Ensure we have a full picture in the buffer (same strategy as plm_video_decode()). + PROFILE_START(PS_MPEG_HASSTART); + if ( + plm_buffer_has_start_code(self->buffer, PLM_START_PICTURE) == -1 && + !plm_buffer_has_ended(self->buffer) + ) { + PROFILE_STOP(PS_MPEG_HASSTART); + return 0; + } + PROFILE_STOP(PS_MPEG_HASSTART); + + if (!plm_video_parse_picture_header(self)) { + // Skip unsupported picture types + self->start_code = -1; + return 1; + } + self->poll_state = PLM_POLL_STATE_HEADER_PARSED; + } + + if (self->poll_state == PLM_POLL_STATE_HEADER_PARSED) { + // Check buffer availability before consuming/committing picture state. + redirect_b = 0; + redirect_ip = 0; + redirect_idx = -1; + if (!self->assume_no_b_frames && self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) { + redirect_b = 1; + redirect_idx = plm_video_find_free_b_buffer(self); + if (redirect_idx < 0) { + return 0; // no free output buffers + } + } else { + int dst = plm_video_buf_index_from_y(self, self->frame_current.y.data); + if (dst >= 0 && self->frame_locked && self->frame_locked[dst]) { + redirect_ip = 1; + redirect_idx = plm_video_find_free_b_buffer(self); + if (redirect_idx < 0) { + return 0; + } + } + } + + // Begin picture: shuffle references and find first slice. + self->poll_frame_temp = plm_video_begin_picture(self); + + // If this is a B picture, redirect frame_current to a free extra buffer. + self->poll_saved_current_valid = 0; + if (redirect_b) { + self->poll_saved_current = self->frame_current; + self->poll_saved_current_valid = 1; + plm_video_init_frame(self, &self->frame_current, + self->frames_data + self->frame_data_size * (size_t)redirect_idx); + } else if (redirect_ip) { + plm_video_init_frame(self, &self->frame_current, + self->frames_data + self->frame_data_size * (size_t)redirect_idx); + } + + self->poll_state = PLM_POLL_STATE_IN_PICTURE; + } + + // Decode one slice if available. + if (self->poll_state == PLM_POLL_STATE_IN_PICTURE && PLM_START_IS_SLICE(self->start_code)) { + PROFILE_START(PS_MPEG_DECODESLICE); + plm_video_decode_slice(self, self->start_code & 0x000000FF); + self->start_code = plm_buffer_next_start_code(self->buffer); + PROFILE_STOP(PS_MPEG_DECODESLICE); + // If there are more slices, keep the picture open and return after one slice. + if (PLM_START_IS_SLICE(self->start_code)) { + return 1; + } + } + + // Picture completed: restore frame_current if we redirected it for a B picture. + if (self->poll_saved_current_valid) { + plm_frame_t decoded_b = self->frame_current; + self->frame_current = self->poll_saved_current; + self->poll_saved_current_valid = 0; + + // B picture output is the decoded buffer. + plm_video_emit_output(self, &decoded_b); + } else { + // Non-B picture: apply original reference rotation semantics. + plm_video_finish_picture(self, self->poll_frame_temp); + + plm_frame_t *out = NULL; + if (self->assume_no_b_frames) { + out = &self->frame_backward; + } + else if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) { + // Should not happen here (handled above) + out = &self->frame_current; + } + else if (self->has_reference_frame) { + out = &self->frame_forward; + } + else { + self->has_reference_frame = TRUE; + } + + if (out && !plm_video_out_full(self)) { + if (!plm_video_is_frame_locked(self, out)) { + plm_video_emit_output(self, out); + } + } + } + + self->poll_state = PLM_POLL_STATE_IDLE; + return 1; +} + +int plm_video_has_header(plm_video_t *self) { + if (self->has_sequence_header) { + return TRUE; + } + + // Lazily parse initial user_data (LD_SAR) before scanning for the sequence header. + plm_video_parse_initial_userdata(self); + + if (self->start_code != PLM_START_SEQUENCE) { + self->start_code = plm_buffer_find_start_code(self->buffer, PLM_START_SEQUENCE); + } + if (self->start_code == -1) { + return FALSE; + } + + if (!plm_video_decode_sequence_header(self)) { + return FALSE; + } + + return TRUE; +} + +int plm_video_decode_sequence_header(plm_video_t *self) { + int max_header_size = 64 + 2 * 64 * 8; // 64 bit header + 2x 64 byte matrix + if (!plm_buffer_has(self->buffer, max_header_size)) { + return FALSE; + } + + self->width = plm_buffer_read(self->buffer, 12); + self->height = plm_buffer_read(self->buffer, 12); + + if (self->width <= 0 || self->height <= 0) { + return FALSE; + } + + // MPEG-1 aspect_ratio_information (4 bits). Despite the name used historically + // in this file, this is used as a display aspect ratio signal by most tools. + self->aspect_ratio_code = plm_buffer_read(self->buffer, 4); + + self->framerate = PLM_VIDEO_PICTURE_RATE[plm_buffer_read(self->buffer, 4)]; + + // Skip bit_rate, marker, buffer_size and constrained bit + plm_buffer_skip(self->buffer, 18 + 1 + 10 + 1); + + // Load custom intra quant matrix? + if (plm_buffer_read(self->buffer, 1)) { + for (int i = 0; i < 64; i++) { + int idx = PLM_VIDEO_ZIG_ZAG[i]; + self->intra_quant_matrix[idx] = plm_buffer_read(self->buffer, 8); + } + } + else { + memcpy(self->intra_quant_matrix, PLM_VIDEO_INTRA_QUANT_MATRIX, 64); + } + rsp_mpeg1_set_quant_matrix(true, self->intra_quant_matrix); + + // Load custom non intra quant matrix? + if (plm_buffer_read(self->buffer, 1)) { + for (int i = 0; i < 64; i++) { + int idx = PLM_VIDEO_ZIG_ZAG[i]; + self->non_intra_quant_matrix[idx] = plm_buffer_read(self->buffer, 8); + } + } + else { + memcpy(self->non_intra_quant_matrix, PLM_VIDEO_NON_INTRA_QUANT_MATRIX, 64); + } + rsp_mpeg1_set_quant_matrix(false, self->non_intra_quant_matrix); + + self->mb_width = (self->width + 15) >> 4; + self->mb_height = (self->height + 15) >> 4; + self->mb_size = self->mb_width * self->mb_height; + + self->luma_width = self->mb_width << 4; + self->luma_height = self->mb_height << 4; + + self->chroma_width = self->mb_width << 3; + self->chroma_height = self->mb_height << 3; + + + // Allocate one big chunk of data for all 3 frames = 9 planes + size_t luma_plane_size = self->luma_width * self->luma_height; + size_t chroma_plane_size = self->chroma_width * self->chroma_height; + size_t frame_data_size = (luma_plane_size + 2 * chroma_plane_size); + self->frame_data_size = frame_data_size; + self->num_frame_buffers = 3 + (self->buffered_pics > 0 ? self->buffered_pics : 0); + + if (RSP_MODE >= 2) + self->frames_data = (uint8_t*)malloc_uncached(frame_data_size * (size_t)self->num_frame_buffers); + else + self->frames_data = (uint8_t*)memalign(16, frame_data_size * (size_t)self->num_frame_buffers); + assertf(self->frames_data, "Out of memory"); + plm_video_init_frame(self, &self->frame_current, self->frames_data + frame_data_size * 0); + plm_video_init_frame(self, &self->frame_forward, self->frames_data + frame_data_size * 1); + plm_video_init_frame(self, &self->frame_backward, self->frames_data + frame_data_size * 2); + + self->frame_locked = (uint8_t*)calloc((size_t)self->num_frame_buffers, 1); + assertf(self->frame_locked, "Out of memory"); + if (self->buffered_pics > 0) { + self->out_queue = (plm_frame_t*)calloc((size_t)self->buffered_pics, sizeof(plm_frame_t)); + assertf(self->out_queue, "Out of memory"); + } + self->out_q_head = self->out_q_tail = self->out_q_count = 0; + self->out_current_in_use = 0; + self->poll_state = PLM_POLL_STATE_IDLE; + self->poll_saved_current_valid = 0; + + self->has_sequence_header = TRUE; + return TRUE; +} + +void plm_video_init_frame(plm_video_t *self, plm_frame_t *frame, uint8_t *base) { + size_t luma_plane_size = self->luma_width * self->luma_height; + size_t chroma_plane_size = self->chroma_width * self->chroma_height; + + frame->width = self->width; + frame->height = self->height; + frame->y.width = self->luma_width; + frame->y.height = self->luma_height; + frame->y.data = base; + + frame->cr.width = self->chroma_width; + frame->cr.height = self->chroma_height; + frame->cr.data = base + luma_plane_size; + + frame->cb.width = self->chroma_width; + frame->cb.height = self->chroma_height; + frame->cb.data = base + luma_plane_size + chroma_plane_size; +} + +void plm_video_decode_slice(plm_video_t *self, int slice) { + self->slice_begin = TRUE; + self->macroblock_address = (slice - 1) * self->mb_width - 1; + + // Reset motion vectors and DC predictors + self->motion_backward.h = self->motion_forward.h = 0; + self->motion_backward.v = self->motion_forward.v = 0; + self->dc_predictor[0] = 128; + self->dc_predictor[1] = 128; + self->dc_predictor[2] = 128; + + self->quantizer_scale = plm_buffer_read(self->buffer, 5); + + // Skip extra + while (plm_buffer_read(self->buffer, 1)) { + plm_buffer_skip(self->buffer, 8); + } + + do { + PROFILE_START(PS_MPEG_MB); + plm_video_decode_macroblock(self); + PROFILE_STOP(PS_MPEG_MB); + } while ( + self->macroblock_address < self->mb_size - 1 && + plm_buffer_peek_non_zero(self->buffer, 23) + ); +} + +void plm_video_decode_macroblock(plm_video_t *self) { + // Decode increment + int increment = 0; + int t = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT); + + while (t == 34) { + // macroblock_stuffing + t = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT); + } + while (t == 35) { + // macroblock_escape + increment += 33; + t = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT); + } + increment += t; + + // Process any skipped macroblocks + if (self->slice_begin) { + // The first increment of each slice is relative to beginning of the + // preverious row, not the preverious macroblock + self->slice_begin = FALSE; + self->macroblock_address += increment; + } + else { + if (self->macroblock_address + increment >= self->mb_size) { + return; // invalid + } + if (increment > 1) { + // Skipped macroblocks reset DC predictors + self->dc_predictor[0] = 128; + self->dc_predictor[1] = 128; + self->dc_predictor[2] = 128; + + // Skipped macroblocks in P-pictures reset motion vectors + if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE) { + self->motion_forward.h = 0; + self->motion_forward.v = 0; + } + } + + // Predict skipped macroblocks + while (increment > 1) { + self->macroblock_address++; + self->mb_row = self->macroblock_address / self->mb_width; + self->mb_col = self->macroblock_address % self->mb_width; + + plm_video_predict_macroblock(self); + if (RSP_MODE >= 3) { + rsp_mpeg1_block_switch_partition(0); rsp_mpeg1_store_pixels(); + rsp_mpeg1_block_switch_partition(4); rsp_mpeg1_store_pixels(); + rsp_mpeg1_block_switch_partition(5); rsp_mpeg1_store_pixels(); + // rspq_block_run(mpeg_store); + rspq_flush(); + } + + increment--; + } + self->macroblock_address++; + } + + self->mb_row = self->macroblock_address / self->mb_width; + self->mb_col = self->macroblock_address % self->mb_width; + + if (self->mb_col >= self->mb_width || self->mb_row >= self->mb_height) { + return; // corrupt stream; + } + + // Process the current macroblock + const plm_vlc_t *table = PLM_VIDEO_MACROBLOCK_TYPE[self->picture_type]; + self->macroblock_type = plm_buffer_read_vlc(self->buffer, table); + + self->macroblock_intra = (self->macroblock_type & 0x01); + self->motion_forward.is_set = (self->macroblock_type & 0x08); + self->motion_backward.is_set = (self->macroblock_type & 0x04); + + // Quantizer scale + if ((self->macroblock_type & 0x10) != 0) { + self->quantizer_scale = plm_buffer_read(self->buffer, 5); + } + + if (self->macroblock_intra) { + // Intra-coded macroblocks reset motion vectors + self->motion_backward.h = self->motion_forward.h = 0; + self->motion_backward.v = self->motion_forward.v = 0; + } + else { + // Non-intra macroblocks reset DC predictors + self->dc_predictor[0] = 128; + self->dc_predictor[1] = 128; + self->dc_predictor[2] = 128; + + plm_video_decode_motion_vectors(self); + plm_video_predict_macroblock(self); + } + + // Decode blocks + plm_buffer_has(self->buffer, 128*8); + int cbp = ((self->macroblock_type & 0x02) != 0) + ? plm_buffer_read_vlc(self->buffer, PLM_VIDEO_CODE_BLOCK_PATTERN) + : (self->macroblock_intra ? 0x3f : 0); + + for (int block = 0, mask = 0x20; block < 6; block++) { + if ((cbp & mask) != 0) { + plm_video_decode_block(self, block); + } + mask >>= 1; + } + + if (RSP_MODE > 0) { + rsp_mpeg1_block_switch_partition(0); rsp_mpeg1_store_pixels(); + rsp_mpeg1_block_switch_partition(4); rsp_mpeg1_store_pixels(); + rsp_mpeg1_block_switch_partition(5); rsp_mpeg1_store_pixels(); + // rspq_block_run(mpeg_store); + rspq_flush(); + } +} + +void plm_video_decode_motion_vectors(plm_video_t *self) { + PROFILE_START(PS_MPEG_MB_MV); + + // Forward + if (self->motion_forward.is_set) { + int r_size = self->motion_forward.r_size; + self->motion_forward.h = plm_video_decode_motion_vector(self, r_size, self->motion_forward.h); + self->motion_forward.v = plm_video_decode_motion_vector(self, r_size, self->motion_forward.v); + } + else if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE) { + // No motion information in P-picture, reset vectors + self->motion_forward.h = 0; + self->motion_forward.v = 0; + } + + if (self->motion_backward.is_set) { + int r_size = self->motion_backward.r_size; + self->motion_backward.h = plm_video_decode_motion_vector(self, r_size, self->motion_backward.h); + self->motion_backward.v = plm_video_decode_motion_vector(self, r_size, self->motion_backward.v); + } + PROFILE_STOP(PS_MPEG_MB_MV); +} + +int plm_video_decode_motion_vector(plm_video_t *self, int r_size, int motion) { + int fscale = 1 << r_size; + int m_code = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MOTION); + int r = 0; + int d; + + if ((m_code != 0) && (fscale != 1)) { + r = plm_buffer_read(self->buffer, r_size); + d = ((abs(m_code) - 1) << r_size) + r + 1; + if (m_code < 0) { + d = -d; + } + } + else { + d = m_code; + } + + motion += d; + if (motion > (fscale << 4) - 1) { + motion -= fscale << 5; + } + else if (motion < ((-fscale) << 4)) { + motion += fscale << 5; + } + + return motion; +} + +void plm_video_predict_macroblock(plm_video_t *self) { + PROFILE_START(PS_MPEG_MB_PREDICT); + int fw_h = self->motion_forward.h; + int fw_v = self->motion_forward.v; + + if (self->motion_forward.full_px) { + fw_h <<= 1; + fw_v <<= 1; + } + + if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) { + int bw_h = self->motion_backward.h; + int bw_v = self->motion_backward.v; + + if (self->motion_backward.full_px) { + bw_h <<= 1; + bw_v <<= 1; + } + + if (RSP_MODE >= 3) { + if (self->motion_forward.is_set) { + if (self->motion_backward.is_set) { + plm_video_interpolate_macroblock_rsp(self, &self->frame_forward, fw_h, fw_v, &self->frame_backward, bw_h, bw_v); + } else { + plm_video_copy_macroblock_rsp(self, &self->frame_forward, fw_h, fw_v); + } + } else { + plm_video_copy_macroblock_rsp(self, &self->frame_backward, bw_h, bw_v); + } + } else { + if (self->motion_forward.is_set) { + plm_video_copy_macroblock(self, &self->frame_forward, fw_h, fw_v); + if (self->motion_backward.is_set) { + plm_video_interpolate_macroblock(self, &self->frame_backward, bw_h, bw_v); + } + } + else { + plm_video_copy_macroblock(self, &self->frame_backward, bw_h, bw_v); + } + } + } + else { + if (RSP_MODE >= 3) { + plm_video_copy_macroblock_rsp(self, &self->frame_forward, fw_h, fw_v); + } else { + plm_video_copy_macroblock(self, &self->frame_forward, fw_h, fw_v); + } + } + + PROFILE_STOP(PS_MPEG_MB_PREDICT); +} + +void plm_video_copy_macroblock_rsp(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v) { + plm_frame_t *d = &self->frame_current; + + int dw = self->mb_width * 16; + int hp = motion_h >> 1; + int vp = motion_v >> 1; + int odd_h = (motion_h & 1) == 1; + int odd_v = (motion_v & 1) == 1; + + unsigned int si = ((self->mb_row * 16) + vp) * dw + (self->mb_col * 16) + hp; + unsigned int di = (self->mb_row * dw + self->mb_col) * 16; + rsp_mpeg1_block_begin(RSP_MPEG1_BLOCK_Y0, d->y.data+di, dw); + rsp_mpeg1_block_predict(s->y.data+si, dw, odd_h, odd_v, 0); + + dw >>= 1; + odd_h = (hp & 1) == 1; + odd_v = (vp & 1) == 1; + hp >>= 1; + vp >>= 1; + + si = ((self->mb_row * 8) + vp) * dw + (self->mb_col * 8) + hp; + di = (self->mb_row * dw + self->mb_col) * 8; + rsp_mpeg1_block_begin(RSP_MPEG1_BLOCK_CR, d->cr.data+di, dw); + rsp_mpeg1_block_predict(s->cr.data+si, dw, odd_h, odd_v, 0); + rsp_mpeg1_block_begin(RSP_MPEG1_BLOCK_CB, d->cb.data+di, dw); + rsp_mpeg1_block_predict(s->cb.data+si, dw, odd_h, odd_v, 0); + rspq_flush(); +} + +void plm_video_interpolate_macroblock_rsp(plm_video_t *self, plm_frame_t *s1, int motion_h1, int motion_v1, plm_frame_t *s2, int motion_h2, int motion_v2) { + plm_frame_t *d = &self->frame_current; + + int dw = self->mb_width * 16; + int hp1 = motion_h1 >> 1; + int vp1 = motion_v1 >> 1; + int odd_h1 = (motion_h1 & 1) == 1; + int odd_v1 = (motion_v1 & 1) == 1; + int hp2 = motion_h2 >> 1; + int vp2 = motion_v2 >> 1; + int odd_h2 = (motion_h2 & 1) == 1; + int odd_v2 = (motion_v2 & 1) == 1; + + unsigned int si1 = ((self->mb_row * 16) + vp1) * dw + (self->mb_col * 16) + hp1; + unsigned int si2 = ((self->mb_row * 16) + vp2) * dw + (self->mb_col * 16) + hp2; + unsigned int di = (self->mb_row * dw + self->mb_col) * 16; + + rsp_mpeg1_block_begin(RSP_MPEG1_BLOCK_Y0, d->y.data+di, dw); + rsp_mpeg1_block_predict(s1->y.data+si1, dw, odd_h1, odd_v1, 0); + rsp_mpeg1_block_predict(s2->y.data+si2, dw, odd_h2, odd_v2, 1); + + dw >>= 1; + odd_h1 = (hp1 & 1) == 1; + odd_v1 = (vp1 & 1) == 1; + hp1 >>= 1; + vp1 >>= 1; + odd_h2 = (hp2 & 1) == 1; + odd_v2 = (vp2 & 1) == 1; + hp2 >>= 1; + vp2 >>= 1; + + si1 = ((self->mb_row * 8) + vp1) * dw + (self->mb_col * 8) + hp1; + si2 = ((self->mb_row * 8) + vp2) * dw + (self->mb_col * 8) + hp2; + di = (self->mb_row * dw + self->mb_col) * 8; + + rsp_mpeg1_block_begin(RSP_MPEG1_BLOCK_CR, d->cr.data+di, dw); + rsp_mpeg1_block_predict(s1->cr.data+si1, dw, odd_h1, odd_v1, 0); + rsp_mpeg1_block_predict(s2->cr.data+si2, dw, odd_h2, odd_v2, 1); + rsp_mpeg1_block_begin(RSP_MPEG1_BLOCK_CB, d->cb.data+di, dw); + rsp_mpeg1_block_predict(s1->cb.data+si1, dw, odd_h1, odd_v1, 0); + rsp_mpeg1_block_predict(s2->cb.data+si2, dw, odd_h2, odd_v2, 1); + rspq_flush(); +} + +void plm_video_copy_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v) { + plm_frame_t *d = &self->frame_current; + plm_video_process_macroblock(self, s->y.data, d->y.data, motion_h, motion_v, 16, FALSE); + plm_video_process_macroblock(self, s->cr.data, d->cr.data, motion_h / 2, motion_v / 2, 8, FALSE); + plm_video_process_macroblock(self, s->cb.data, d->cb.data, motion_h / 2, motion_v / 2, 8, FALSE); +} + +void plm_video_interpolate_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v) { + plm_frame_t *d = &self->frame_current; + plm_video_process_macroblock(self, s->y.data, d->y.data, motion_h, motion_v, 16, TRUE); + plm_video_process_macroblock(self, s->cr.data, d->cr.data, motion_h / 2, motion_v / 2, 8, TRUE); + plm_video_process_macroblock(self, s->cb.data, d->cb.data, motion_h / 2, motion_v / 2, 8, TRUE); +} + +#define PLM_BLOCK_SET(DEST, DEST_INDEX, DEST_WIDTH, SOURCE_INDEX, SOURCE_WIDTH, BLOCK_SIZE, OP) do { \ + int dest_scan = DEST_WIDTH - BLOCK_SIZE; \ + int source_scan = SOURCE_WIDTH - BLOCK_SIZE; \ + for (int y = 0; y < BLOCK_SIZE; y++) { \ + for (int x = 0; x < BLOCK_SIZE; x++) { \ + DEST[DEST_INDEX] = OP; \ + SOURCE_INDEX++; DEST_INDEX++; \ + } \ + SOURCE_INDEX += source_scan; \ + DEST_INDEX += dest_scan; \ + }} while(FALSE) + +void plm_video_process_macroblock( + plm_video_t *self, uint8_t *s, uint8_t *d, + int motion_h, int motion_v, int block_size, int interpolate +) { + int dw = self->mb_width * block_size; + + int hp = motion_h >> 1; + int vp = motion_v >> 1; + int odd_h = (motion_h & 1) == 1; + int odd_v = (motion_v & 1) == 1; + + unsigned int si = ((self->mb_row * block_size) + vp) * dw + (self->mb_col * block_size) + hp; + unsigned int di = (self->mb_row * dw + self->mb_col) * block_size; + + unsigned int max_address = (dw * (self->mb_height * block_size - block_size + 1) - block_size); + if (si > max_address || di > max_address) { + return; // corrupt video + } + + #define PLM_MB_CASE(INTERPOLATE, ODD_H, ODD_V, OP) \ + case ((INTERPOLATE << 2) | (ODD_H << 1) | (ODD_V)): \ + PLM_BLOCK_SET(d, di, dw, si, dw, block_size, OP); \ + break + + switch ((interpolate << 2) | (odd_h << 1) | (odd_v)) { + PLM_MB_CASE(0, 0, 0, (s[si])); + PLM_MB_CASE(0, 0, 1, (s[si] + s[si + dw] + 1) >> 1); + PLM_MB_CASE(0, 1, 0, (s[si] + s[si + 1] + 1) >> 1); + PLM_MB_CASE(0, 1, 1, (s[si] + s[si + 1] + s[si + dw] + s[si + dw + 1] + 2) >> 2); + + PLM_MB_CASE(1, 0, 0, (d[di] + (s[si]) + 1) >> 1); + PLM_MB_CASE(1, 0, 1, (d[di] + ((s[si] + s[si + dw] + 1) >> 1) + 1) >> 1); + PLM_MB_CASE(1, 1, 0, (d[di] + ((s[si] + s[si + 1] + 1) >> 1) + 1) >> 1); + PLM_MB_CASE(1, 1, 1, (d[di] + ((s[si] + s[si + 1] + s[si + dw] + s[si + dw + 1] + 2) >> 2) + 1) >> 1); + } + + #undef PLM_MB_CASE +} + +uint16_t plm_video_decode_dct_coeff(plm_buffer_t *buf) { + uint64_t bits = plm_buffer_showbits2(buf); + if (bits>>63 == 1) { buf->bit_index += 1; return 0x0001; } + if (bits>>61 == 7) { buf->bit_index += 3; return 0x0101; } + if (bits>>60 == 4) { buf->bit_index += 4; return 0x0002; } + if (bits>>60 == 5) { buf->bit_index += 4; return 0x0201; } + return plm_buffer_read_vlc_uint_bits(buf, PLM_VIDEO_DCT_COEFF, bits); +} + + +void plm_video_decode_block(plm_video_t *self, int block) { + + int n = 0; + uint8_t *quant_matrix; + + PROFILE_START(PS_MPEG_MB_DECODE); + + PROFILE_START(PS_MPEG_MB_DECODE_DC); + // Decode DC coefficient of intra-coded blocks + if (self->macroblock_intra) { + int predictor; + int dct_size; + + // DC prediction + int plane_index = block > 3 ? block - 3 : 0; + predictor = self->dc_predictor[plane_index]; + dct_size = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_DCT_SIZE[plane_index]); + + // Read DC coeff + if (dct_size > 0) { + int differential = plm_buffer_read(self->buffer, dct_size); + if ((differential & (1 << (dct_size - 1))) != 0) { + self->block_data[0] = predictor + differential; + } + else { + self->block_data[0] = predictor + (-(1 << dct_size) | (differential + 1)); + } + } + else { + self->block_data[0] = predictor; + } + + // Save predictor value + self->dc_predictor[plane_index] = self->block_data[0]; + + // Dequantize + premultiply + self->block_data[0] <<= (3 + 5 - RSP_IDCT_SCALER); + + quant_matrix = self->intra_quant_matrix; + n = 1; + } + else { + quant_matrix = self->non_intra_quant_matrix; + } + PROFILE_STOP(PS_MPEG_MB_DECODE_DC); + + // Calculate block position + PROFILE_START(PS_MPEG_MB_DECODE_BLOCK); + uint8_t *d; + int dw; + int di; + + if (block < 4) { + d = self->frame_current.y.data; + dw = self->luma_width; + di = (self->mb_row * self->luma_width + self->mb_col) << 4; + if ((block & 1) != 0) { + di += 8; + } + if ((block & 2) != 0) { + di += self->luma_width << 3; + } + } + else { + d = (block == 4) ? self->frame_current.cb.data : self->frame_current.cr.data; + dw = self->chroma_width; + di = ((self->mb_row * self->luma_width) << 2) + (self->mb_col << 3); + } + PROFILE_STOP(PS_MPEG_MB_DECODE_BLOCK); + + if (RSP_MODE > 0) { + if (RSP_MODE >= 3 && !self->macroblock_intra) { + // If prediction was done in RSP, the blocks are already defined. + // Simply activate the correct partition. + rsp_mpeg1_block_switch_partition(block); + } else { + // Define the current block (aka partition). We don't care exactly which + // one it is as we're not keeping the data in the RSP. So just define + // a 8x8 partition using a chroma channel. + if (block == 0 || block == 4 || block == 5) + rsp_mpeg1_block_begin(block, d+di, dw); + else + rsp_mpeg1_block_switch_partition(block); + } + if (n == 1) { + rsp_mpeg1_block_coeff(0, self->block_data[0]); + } + } + + // Decode AC coefficients (+DC for non-intra) + PROFILE_START(PS_MPEG_MB_DECODE_AC); + plm_buffer_has(self->buffer, 64*24); + while (TRUE) { + static const uint16_t qtable0[128] __attribute__((aligned(16))) = { 0,0,0,0,65535,65535,65535,65535,49666,49666,51457,51457,49156,49156,51201,51201,42753,42753,42753,42753,42497,42497,42497,42497,41218,41218,41218,41218,42241,42241,42241,42241,60673,57350,60417,60161,58114,57603,57349,59905,32771,32771,32771,32771,32771,32771,32771,32771,33793,33793,33793,33793,33793,33793,33793,33793,33537,33537,33537,33537,33537,33537,33537,33537,24578,24578,24578,24578,24578,24578,24578,24578,24578,24578,24578,24578,24578,24578,24578,24578,25089,25089,25089,25089,25089,25089,25089,25089,25089,25089,25089,25089,25089,25089,25089,25089,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641,16641 }; + static const uint16_t qtables1[256] __attribute__((aligned(16))) = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57618, 57617, 57616, 57615, 58883, 61442, 61186, 60930, 60674, 60418, 60162, 65281, 65025, 64769, 64513, 64257, 49192, 49192, 49191, 49191, 49190, 49190, 49189, 49189, 49188, 49188, 49187, 49187, 49186, 49186, 49185, 49185, 49184, 49184, 49422, 49422, 49421, 49421, 49420, 49420, 49419, 49419, 49418, 49418, 49417, 49417, 49416, 49416, 40991, 40991, 40991, 40991, 40990, 40990, 40990, 40990, 40989, 40989, 40989, 40989, 40988, 40988, 40988, 40988, 40987, 40987, 40987, 40987, 40986, 40986, 40986, 40986, 40985, 40985, 40985, 40985, 40984, 40984, 40984, 40984, 40983, 40983, 40983, 40983, 40982, 40982, 40982, 40982, 40981, 40981, 40981, 40981, 40980, 40980, 40980, 40980, 40979, 40979, 40979, 40979, 40978, 40978, 40978, 40978, 40977, 40977, 40977, 40977, 40976, 40976, 40976, 40976, 35330, 35330, 35330, 35330, 35330, 35330, 35330, 35330, 35074, 35074, 35074, 35074, 35074, 35074, 35074, 35074, 34051, 34051, 34051, 34051, 34051, 34051, 34051, 34051, 33540, 33540, 33540, 33540, 33540, 33540, 33540, 33540, 33285, 33285, 33285, 33285, 33285, 33285, 33285, 33285, 33031, 33031, 33031, 33031, 33031, 33031, 33031, 33031, 33030, 33030, 33030, 33030, 33030, 33030, 33030, 33030, 32783, 32783, 32783, 32783, 32783, 32783, 32783, 32783, 32782, 32782, 32782, 32782, 32782, 32782, 32782, 32782, 32781, 32781, 32781, 32781, 32781, 32781, 32781, 32781, 32780, 32780, 32780, 32780, 32780, 32780, 32780, 32780, 39425, 39425, 39425, 39425, 39425, 39425, 39425, 39425, 39169, 39169, 39169, 39169, 39169, 39169, 39169, 39169, 38913, 38913, 38913, 38913, 38913, 38913, 38913, 38913, 38657, 38657, 38657, 38657, 38657, 38657, 38657, 38657, 38401, 38401, 38401, 38401, 38401, 38401, 38401, 38401 }; + static const uint16_t qtables2[16] __attribute__((aligned(16))) = { 24587, 26626, 25603, 24586, 25092, 26370, 29953, 29697, 24585, 29441, 29185, 24837, 25347, 24584, 26114, 28929 }; + static const uint16_t qtables34[2][4] __attribute__((aligned(16))) = { {12289, 9474, 8199, 8707}, {8452, 12033, 11777, 9218} }; + static const uint16_t *tbl[4] = { qtables1, qtables2, qtables34[0], qtables34[1] }; + static const uint8_t shift[4] = { 0,4,6,6 }; + + int level = 0; + int run = 0; + unsigned int coeff; + + uint64_t bits = plm_buffer_showbits2(self->buffer); + #define readbits(n) ({ uint64_t val = bits>>(64-n); bits <<= n; self->buffer->bit_index += n; val; }) + + if (bits>>63 == 1) { + readbits(1); + if ((n > 0) && (readbits(1) == 0)) + break; + run = 0; + level = 1; + if (readbits(1)) { + level = -level; + } + } else { + unsigned int bit0 = (bits>>56) & 0xff; + if (bit0 >= 4) { + coeff = qtable0[bit0]; + } else { + unsigned int bit1 = (bits >> 48) & 0xff; + coeff = tbl[bit0][bit1 >> shift[bit0]]; + readbits(8); + if (coeff == 0xffff) __builtin_unreachable(); + } + if (coeff == 0xffff) { + readbits(6); + run = readbits(6); + level = readbits(8); + if (level == 0) { + level = readbits(8); + } else if (level == 128) { + level = readbits(8) - 256; + } else if (level > 128) { + level = level - 256; + } + } else { + readbits((coeff >> 13) + 1); + coeff &= 0x1fff; + run = coeff >> 8; + level = coeff & 0xff; + if (readbits(1)) { + level = -level; + } + } + } + + n += run; + if (n < 0 || n >= 64) { + fprintf(stderr, "INVALID AC COEFF\n"); + return; // invalid + } + + if (RSP_MODE < 2) { + int de_zig_zagged = PLM_VIDEO_ZIG_ZAG[n]; + + // Dequantize, oddify, clip + level <<= 1; + if (!self->macroblock_intra) { + level += (level < 0 ? -1 : 1); + } + level = (level * self->quantizer_scale * quant_matrix[de_zig_zagged]) >> 4; + if ((level & 1) == 0) { + level += level > 0 ? -1 : 1; + } + if (level > 2047) { + level = 2047; + } + else if (level < -2048) { + level = -2048; + } + + // Save premultiplied coefficient + level = (level * PLM_VIDEO_PREMULTIPLIER_MATRIX[de_zig_zagged]) >> RSP_IDCT_SCALER; + self->block_data[de_zig_zagged] = level; + rsp_mpeg1_block_coeff(n, level); + } else { + rsp_mpeg1_block_coeff(n, level); + } + n++; + // PROFILE_STOP(PS_MPEG_MB_DECODE_AC_DEQUANT); + } + PROFILE_STOP(PS_MPEG_MB_DECODE_AC); + + // Move block to its place + PROFILE_START(PS_MPEG_MB_DECODE_BLOCK, 1); + if (RSP_MODE == 0) { + int16_t *s = self->block_data; + int si = 0; + plm_video_decode_block_residual(s, si, d, di, dw, n, self->macroblock_intra); + } else if (RSP_MODE == 1) { + if (self->macroblock_intra) + rsp_mpeg1_zero_pixels(); + else + rsp_mpeg1_load_pixels(); + rsp_mpeg1_block_decode(n, self->macroblock_intra!=0); + //rsp_mpeg1_store_pixels(); + rspq_flush(); + } else if (RSP_MODE == 2) { + if (self->macroblock_intra) + rsp_mpeg1_zero_pixels(); + else + rsp_mpeg1_load_pixels(); + rsp_mpeg1_block_dequant(self->macroblock_intra, self->quantizer_scale); + rsp_mpeg1_block_decode(n, self->macroblock_intra!=0); + //rsp_mpeg1_store_pixels(); + rspq_flush(); + } else if (RSP_MODE >= 3) { + // if (self->macroblock_intra && (block == 0 || block == 4 || block == 5)) + // rsp_mpeg1_zero_pixels(); + rsp_mpeg1_block_dequant(self->macroblock_intra, self->quantizer_scale); + rsp_mpeg1_block_decode(n, self->macroblock_intra!=0); + //rsp_mpeg1_store_pixels(); + rspq_flush(); + } + + PROFILE_STOP(PS_MPEG_MB_DECODE_BLOCK, 1); + PROFILE_STOP(PS_MPEG_MB_DECODE); +} + +void plm_video_decode_block_residual(int16_t *s, int si, uint8_t *d, int di, int dw, int n, int intra) +{ + if (intra) { + // Overwrite (no prediction) + if (n == 1) { + int clamped = plm_clamp((s[0] + (128 >> RSP_IDCT_SCALER)) >> (8 - RSP_IDCT_SCALER)); + PLM_BLOCK_SET(d, di, dw, si, 8, 8, clamped); + s[0] = 0; + } + else { + plm_video_idct(s); + PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(s[si])); + memset(s, 0, 64*sizeof(int16_t)); + } + } + else { + // Add data to the predicted macroblock + if (n == 1) { + int value = (s[0] + (128 >> RSP_IDCT_SCALER)) >> (8 - RSP_IDCT_SCALER); + PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(d[di] + value)); + s[0] = 0; + } + else { + plm_video_idct(s); + PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(d[di] + s[si])); + memset(s, 0, 64*sizeof(int16_t)); + } + } +} + +void plm_video_idct(int16_t *block) { + int + b1, b3, b4, b6, b7, tmp1, tmp2, m0, + x0, x1, x2, x3, x4, y3, y4, y5, y6, y7; + + PROFILE_START(PS_MPEG_MB_DECODE_BLOCK_IDCT); + + // Transform columns + for (int i = 0; i < 8; ++i) { + b1 = block[4 * 8 + i]; + b3 = block[2 * 8 + i] + block[6 * 8 + i]; + b4 = block[5 * 8 + i] - block[3 * 8 + i]; + tmp1 = block[1 * 8 + i] + block[7 * 8 + i]; + tmp2 = block[3 * 8 + i] + block[5 * 8 + i]; + b6 = block[1 * 8 + i] - block[7 * 8 + i]; + b7 = tmp1 + tmp2; + m0 = block[0 * 8 + i]; + x4 = ((b6 * 473 - b4 * 196 + 128) >> 8) - b7; + x0 = x4 - (((tmp1 - tmp2) * 362 + 128) >> 8); + x1 = m0 - b1; + x2 = (((block[2 * 8 + i] - block[6 * 8 + i]) * 362 + 128) >> 8) - b3; + x3 = m0 + b1; + y3 = x1 + x2; + y4 = x3 + b3; + y5 = x1 - x2; + y6 = x3 - b3; + y7 = -x0 - ((b4 * 473 + b6 * 196 + 128) >> 8); + block[0 * 8 + i] = b7 + y4; + block[1 * 8 + i] = x4 + y3; + block[2 * 8 + i] = y5 - x0; + block[3 * 8 + i] = y6 - y7; + block[4 * 8 + i] = y6 + y7; + block[5 * 8 + i] = x0 + y5; + block[6 * 8 + i] = y3 - x4; + block[7 * 8 + i] = y4 - b7; + } + + // Transform rows + for (int i = 0; i < 64; i += 8) { + b1 = block[4 + i]; + b3 = block[2 + i] + block[6 + i]; + b4 = block[5 + i] - block[3 + i]; + tmp1 = block[1 + i] + block[7 + i]; + tmp2 = block[3 + i] + block[5 + i]; + b6 = block[1 + i] - block[7 + i]; + b7 = tmp1 + tmp2; + m0 = block[0 + i]; + x4 = ((b6 * 473 - b4 * 196 + 128) >> 8) - b7; + x0 = x4 - (((tmp1 - tmp2) * 362 + 128) >> 8); + x1 = m0 - b1; + x2 = (((block[2 + i] - block[6 + i]) * 362 + 128) >> 8) - b3; + x3 = m0 + b1; + y3 = x1 + x2; + y4 = x3 + b3; + y5 = x1 - x2; + y6 = x3 - b3; + y7 = -x0 - ((b4 * 473 + b6 * 196 + 128) >> 8); + block[0 + i] = (b7 + y4 + (128 >> RSP_IDCT_SCALER)) >> (8 - RSP_IDCT_SCALER); + block[1 + i] = (x4 + y3 + (128 >> RSP_IDCT_SCALER)) >> (8 - RSP_IDCT_SCALER); + block[2 + i] = (y5 - x0 + (128 >> RSP_IDCT_SCALER)) >> (8 - RSP_IDCT_SCALER); + block[3 + i] = (y6 - y7 + (128 >> RSP_IDCT_SCALER)) >> (8 - RSP_IDCT_SCALER); + block[4 + i] = (y6 + y7 + (128 >> RSP_IDCT_SCALER)) >> (8 - RSP_IDCT_SCALER); + block[5 + i] = (x0 + y5 + (128 >> RSP_IDCT_SCALER)) >> (8 - RSP_IDCT_SCALER); + block[6 + i] = (y3 - x4 + (128 >> RSP_IDCT_SCALER)) >> (8 - RSP_IDCT_SCALER); + block[7 + i] = (y4 - b7 + (128 >> RSP_IDCT_SCALER)) >> (8 - RSP_IDCT_SCALER); + } + + PROFILE_STOP(PS_MPEG_MB_DECODE_BLOCK_IDCT); +} + +// YCbCr conversion following the BT.601 standard: +// https://infogalactic.com/info/YCbCr#ITU-R_BT.601_conversion + +#define PLM_PUT_PIXEL(RI, GI, BI, Y_OFFSET, DEST_OFFSET) \ + y = ((frame->y.data[y_index + Y_OFFSET]-16) * 76309) >> 16; \ + dest[d_index + DEST_OFFSET + RI] = plm_clamp(y + r); \ + dest[d_index + DEST_OFFSET + GI] = plm_clamp(y - g); \ + dest[d_index + DEST_OFFSET + BI] = plm_clamp(y + b); + +#define PLM_DEFINE_FRAME_CONVERT_FUNCTION(NAME, BYTES_PER_PIXEL, RI, GI, BI) \ + void NAME(plm_frame_t *frame, uint8_t *dest, int stride) { \ + int cols = frame->width >> 1; \ + int rows = frame->height >> 1; \ + int yw = frame->y.width; \ + int cw = frame->cb.width; \ + for (int row = 0; row < rows; row++) { \ + int c_index = row * cw; \ + int y_index = row * 2 * yw; \ + int d_index = row * 2 * stride; \ + for (int col = 0; col < cols; col++) { \ + int y; \ + int cr = frame->cr.data[c_index] - 128; \ + int cb = frame->cb.data[c_index] - 128; \ + int r = (cr * 104597) >> 16; \ + int g = (cb * 25674 + cr * 53278) >> 16; \ + int b = (cb * 132201) >> 16; \ + PLM_PUT_PIXEL(RI, GI, BI, 0, 0); \ + PLM_PUT_PIXEL(RI, GI, BI, 1, BYTES_PER_PIXEL); \ + PLM_PUT_PIXEL(RI, GI, BI, yw, stride); \ + PLM_PUT_PIXEL(RI, GI, BI, yw + 1, stride + BYTES_PER_PIXEL); \ + c_index += 1; \ + y_index += 2; \ + d_index += 2 * BYTES_PER_PIXEL; \ + } \ + } \ + } + +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_rgb, 3, 0, 1, 2) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_bgr, 3, 2, 1, 0) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_rgba, 4, 0, 1, 2) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_bgra, 4, 2, 1, 0) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_argb, 4, 1, 2, 3) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_abgr, 4, 3, 2, 1) + + +#undef PLM_PUT_PIXEL +#undef PLM_DEFINE_FRAME_CONVERT_FUNCTION + + + +// ----------------------------------------------------------------------------- +// plm_audio implementation + +// Based on kjmp2 by Martin J. Fiedler +// http://keyj.emphy.de/kjmp2/ + +static const int PLM_AUDIO_FRAME_SYNC = 0x7ff; + +static const int PLM_AUDIO_MPEG_2_5 = 0x0; +static const int PLM_AUDIO_MPEG_2 = 0x2; +static const int PLM_AUDIO_MPEG_1 = 0x3; + +static const int PLM_AUDIO_LAYER_III = 0x1; +static const int PLM_AUDIO_LAYER_II = 0x2; +static const int PLM_AUDIO_LAYER_I = 0x3; + +static const int PLM_AUDIO_MODE_STEREO = 0x0; +static const int PLM_AUDIO_MODE_JOINT_STEREO = 0x1; +static const int PLM_AUDIO_MODE_DUAL_CHANNEL = 0x2; +static const int PLM_AUDIO_MODE_MONO = 0x3; + +static const unsigned short PLM_AUDIO_SAMPLE_RATE[] = { + 44100, 48000, 32000, 0, // MPEG-1 + 22050, 24000, 16000, 0 // MPEG-2 +}; + +static const short PLM_AUDIO_BIT_RATE[] = { + 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, // MPEG-1 + 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 // MPEG-2 +}; + +static const int PLM_AUDIO_SCALEFACTOR_BASE[] = { + 0x02000000, 0x01965FEA, 0x01428A30 +}; + +static const float PLM_AUDIO_SYNTHESIS_WINDOW[] = { + 0.0, -0.5, -0.5, -0.5, -0.5, -0.5, + -0.5, -1.0, -1.0, -1.0, -1.0, -1.5, + -1.5, -2.0, -2.0, -2.5, -2.5, -3.0, + -3.5, -3.5, -4.0, -4.5, -5.0, -5.5, + -6.5, -7.0, -8.0, -8.5, -9.5, -10.5, + -12.0, -13.0, -14.5, -15.5, -17.5, -19.0, + -20.5, -22.5, -24.5, -26.5, -29.0, -31.5, + -34.0, -36.5, -39.5, -42.5, -45.5, -48.5, + -52.0, -55.5, -58.5, -62.5, -66.0, -69.5, + -73.5, -77.0, -80.5, -84.5, -88.0, -91.5, + -95.0, -98.0, -101.0, -104.0, 106.5, 109.0, + 111.0, 112.5, 113.5, 114.0, 114.0, 113.5, + 112.0, 110.5, 107.5, 104.0, 100.0, 94.5, + 88.5, 81.5, 73.0, 63.5, 53.0, 41.5, + 28.5, 14.5, -1.0, -18.0, -36.0, -55.5, + -76.5, -98.5, -122.0, -147.0, -173.5, -200.5, + -229.5, -259.5, -290.5, -322.5, -355.5, -389.5, + -424.0, -459.5, -495.5, -532.0, -568.5, -605.0, + -641.5, -678.0, -714.0, -749.0, -783.5, -817.0, + -849.0, -879.5, -908.5, -935.0, -959.5, -981.0, + -1000.5, -1016.0, -1028.5, -1037.5, -1042.5, -1043.5, + -1040.0, -1031.5, 1018.5, 1000.0, 976.0, 946.5, + 911.0, 869.5, 822.0, 767.5, 707.0, 640.0, + 565.5, 485.0, 397.0, 302.5, 201.0, 92.5, + -22.5, -144.0, -272.5, -407.0, -547.5, -694.0, + -846.0, -1003.0, -1165.0, -1331.5, -1502.0, -1675.5, + -1852.5, -2031.5, -2212.5, -2394.0, -2576.5, -2758.5, + -2939.5, -3118.5, -3294.5, -3467.5, -3635.5, -3798.5, + -3955.0, -4104.5, -4245.5, -4377.5, -4499.0, -4609.5, + -4708.0, -4792.5, -4863.5, -4919.0, -4958.0, -4979.5, + -4983.0, -4967.5, -4931.5, -4875.0, -4796.0, -4694.5, + -4569.5, -4420.0, -4246.0, -4046.0, -3820.0, -3567.0, + 3287.0, 2979.5, 2644.0, 2280.5, 1888.0, 1467.5, + 1018.5, 541.0, 35.0, -499.0, -1061.0, -1650.0, + -2266.5, -2909.0, -3577.0, -4270.0, -4987.5, -5727.5, + -6490.0, -7274.0, -8077.5, -8899.5, -9739.0, -10594.5, + -11464.5, -12347.0, -13241.0, -14144.5, -15056.0, -15973.5, + -16895.5, -17820.0, -18744.5, -19668.0, -20588.0, -21503.0, + -22410.5, -23308.5, -24195.0, -25068.5, -25926.5, -26767.0, + -27589.0, -28389.0, -29166.5, -29919.0, -30644.5, -31342.0, + -32009.5, -32645.0, -33247.0, -33814.5, -34346.0, -34839.5, + -35295.0, -35710.0, -36084.5, -36417.5, -36707.5, -36954.0, + -37156.5, -37315.0, -37428.0, -37496.0, 37519.0, 37496.0, + 37428.0, 37315.0, 37156.5, 36954.0, 36707.5, 36417.5, + 36084.5, 35710.0, 35295.0, 34839.5, 34346.0, 33814.5, + 33247.0, 32645.0, 32009.5, 31342.0, 30644.5, 29919.0, + 29166.5, 28389.0, 27589.0, 26767.0, 25926.5, 25068.5, + 24195.0, 23308.5, 22410.5, 21503.0, 20588.0, 19668.0, + 18744.5, 17820.0, 16895.5, 15973.5, 15056.0, 14144.5, + 13241.0, 12347.0, 11464.5, 10594.5, 9739.0, 8899.5, + 8077.5, 7274.0, 6490.0, 5727.5, 4987.5, 4270.0, + 3577.0, 2909.0, 2266.5, 1650.0, 1061.0, 499.0, + -35.0, -541.0, -1018.5, -1467.5, -1888.0, -2280.5, + -2644.0, -2979.5, 3287.0, 3567.0, 3820.0, 4046.0, + 4246.0, 4420.0, 4569.5, 4694.5, 4796.0, 4875.0, + 4931.5, 4967.5, 4983.0, 4979.5, 4958.0, 4919.0, + 4863.5, 4792.5, 4708.0, 4609.5, 4499.0, 4377.5, + 4245.5, 4104.5, 3955.0, 3798.5, 3635.5, 3467.5, + 3294.5, 3118.5, 2939.5, 2758.5, 2576.5, 2394.0, + 2212.5, 2031.5, 1852.5, 1675.5, 1502.0, 1331.5, + 1165.0, 1003.0, 846.0, 694.0, 547.5, 407.0, + 272.5, 144.0, 22.5, -92.5, -201.0, -302.5, + -397.0, -485.0, -565.5, -640.0, -707.0, -767.5, + -822.0, -869.5, -911.0, -946.5, -976.0, -1000.0, + 1018.5, 1031.5, 1040.0, 1043.5, 1042.5, 1037.5, + 1028.5, 1016.0, 1000.5, 981.0, 959.5, 935.0, + 908.5, 879.5, 849.0, 817.0, 783.5, 749.0, + 714.0, 678.0, 641.5, 605.0, 568.5, 532.0, + 495.5, 459.5, 424.0, 389.5, 355.5, 322.5, + 290.5, 259.5, 229.5, 200.5, 173.5, 147.0, + 122.0, 98.5, 76.5, 55.5, 36.0, 18.0, + 1.0, -14.5, -28.5, -41.5, -53.0, -63.5, + -73.0, -81.5, -88.5, -94.5, -100.0, -104.0, + -107.5, -110.5, -112.0, -113.5, -114.0, -114.0, + -113.5, -112.5, -111.0, -109.0, 106.5, 104.0, + 101.0, 98.0, 95.0, 91.5, 88.0, 84.5, + 80.5, 77.0, 73.5, 69.5, 66.0, 62.5, + 58.5, 55.5, 52.0, 48.5, 45.5, 42.5, + 39.5, 36.5, 34.0, 31.5, 29.0, 26.5, + 24.5, 22.5, 20.5, 19.0, 17.5, 15.5, + 14.5, 13.0, 12.0, 10.5, 9.5, 8.5, + 8.0, 7.0, 6.5, 5.5, 5.0, 4.5, + 4.0, 3.5, 3.5, 3.0, 2.5, 2.5, + 2.0, 2.0, 1.5, 1.5, 1.0, 1.0, + 1.0, 1.0, 0.5, 0.5, 0.5, 0.5, + 0.5, 0.5 +}; + +// Quantizer lookup, step 1: bitrate classes +static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_1[2][16] = { + // 32, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,384 <- bitrate + { 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2 }, // mono + // 16, 24, 28, 32, 40, 48, 56, 64, 80, 96,112,128,160,192 <- bitrate / chan + { 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2 } // stereo +}; + +// Quantizer lookup, step 2: bitrate class, sample rate -> B2 table idx, sblimit +static const uint8_t PLM_AUDIO_QUANT_TAB_A = (27 | 64); // Table 3-B.2a: high-rate, sblimit = 27 +static const uint8_t PLM_AUDIO_QUANT_TAB_B = (30 | 64); // Table 3-B.2b: high-rate, sblimit = 30 +static const uint8_t PLM_AUDIO_QUANT_TAB_C = 8; // Table 3-B.2c: low-rate, sblimit = 8 +static const uint8_t PLM_AUDIO_QUANT_TAB_D = 12; // Table 3-B.2d: low-rate, sblimit = 12 + +static const uint8_t QUANT_LUT_STEP_2[3][3] = { + //44.1 kHz, 48 kHz, 32 kHz + { PLM_AUDIO_QUANT_TAB_C, PLM_AUDIO_QUANT_TAB_C, PLM_AUDIO_QUANT_TAB_D }, // 32 - 48 kbit/sec/ch + { PLM_AUDIO_QUANT_TAB_A, PLM_AUDIO_QUANT_TAB_A, PLM_AUDIO_QUANT_TAB_A }, // 56 - 80 kbit/sec/ch + { PLM_AUDIO_QUANT_TAB_B, PLM_AUDIO_QUANT_TAB_A, PLM_AUDIO_QUANT_TAB_B } // 96+ kbit/sec/ch +}; + +// Quantizer lookup, step 3: B2 table, subband -> nbal, row index +// (upper 4 bits: nbal, lower 4 bits: row index) +static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_3[3][32] = { + // Low-rate table (3-B.2c and 3-B.2d) + { + 0x44,0x44, + 0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34 + }, + // High-rate table (3-B.2a and 3-B.2b) + { + 0x43,0x43,0x43, + 0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42, + 0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31, + 0x20,0x20,0x20,0x20,0x20,0x20,0x20 + }, + // MPEG-2 LSR table (B.2 in ISO 13818-3) + { + 0x45,0x45,0x45,0x45, + 0x34,0x34,0x34,0x34,0x34,0x34,0x34, + 0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24, + 0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24 + } +}; + +// Quantizer lookup, step 4: table row, allocation[] value -> quant table index +static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_4[6][16] = { + { 0, 1, 2, 17 }, + { 0, 1, 2, 3, 4, 5, 6, 17 }, + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17 }, + { 0, 1, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 }, + { 0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17 }, + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 } +}; + +typedef struct plm_quantizer_spec_t { + unsigned short levels; + unsigned char group; + unsigned char bits; +} plm_quantizer_spec_t; + +static const plm_quantizer_spec_t PLM_AUDIO_QUANT_TAB[] = { + { 3, 1, 5 }, // 1 + { 5, 1, 7 }, // 2 + { 7, 0, 3 }, // 3 + { 9, 1, 10 }, // 4 + { 15, 0, 4 }, // 5 + { 31, 0, 5 }, // 6 + { 63, 0, 6 }, // 7 + { 127, 0, 7 }, // 8 + { 255, 0, 8 }, // 9 + { 511, 0, 9 }, // 10 + { 1023, 0, 10 }, // 11 + { 2047, 0, 11 }, // 12 + { 4095, 0, 12 }, // 13 + { 8191, 0, 13 }, // 14 + { 16383, 0, 14 }, // 15 + { 32767, 0, 15 }, // 16 + { 65535, 0, 16 } // 17 +}; + +typedef struct plm_audio_t { + double time; + int samples_decoded; + int samplerate_index; + int bitrate_index; + int version; + int layer; + int mode; + int bound; + int v_pos; + int next_frame_data_size; + int has_header; + + plm_buffer_t *buffer; + int destroy_buffer_when_done; + + const plm_quantizer_spec_t *allocation[2][32]; + uint8_t scale_factor_info[2][32]; + int scale_factor[2][32][3]; + int sample[2][32][3]; + + plm_samples_t samples; + float D[1024]; + float V[2][1024]; + float U[32]; +} plm_audio_t; + +int plm_audio_find_frame_sync(plm_audio_t *self); +int plm_audio_decode_header(plm_audio_t *self); +void plm_audio_decode_frame(plm_audio_t *self); +const plm_quantizer_spec_t *plm_audio_read_allocation(plm_audio_t *self, int sb, int tab3); +void plm_audio_read_samples(plm_audio_t *self, int ch, int sb, int part); +void plm_audio_idct36(int s[32][3], int ss, float *d, int dp); + +plm_audio_t *plm_audio_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done) { + plm_audio_t *self = (plm_audio_t *)malloc(sizeof(plm_audio_t)); + assertf(self, "Out of memory"); + memset(self, 0, sizeof(plm_audio_t)); + + self->samples.count = PLM_AUDIO_SAMPLES_PER_FRAME; + self->buffer = buffer; + self->destroy_buffer_when_done = destroy_when_done; + self->samplerate_index = 3; // Indicates 0 + + memcpy(self->D, PLM_AUDIO_SYNTHESIS_WINDOW, 512 * sizeof(float)); + memcpy(self->D + 512, PLM_AUDIO_SYNTHESIS_WINDOW, 512 * sizeof(float)); + + // Attempt to decode first header + self->next_frame_data_size = plm_audio_decode_header(self); + + return self; +} + +void plm_audio_destroy(plm_audio_t *self) { + if (self->destroy_buffer_when_done) { + plm_buffer_destroy(self->buffer); + } + free(self); +} + +int plm_audio_has_header(plm_audio_t *self) { + if (self->has_header) { + return TRUE; + } + + self->next_frame_data_size = plm_audio_decode_header(self); + return self->has_header; +} + +int plm_audio_get_samplerate(plm_audio_t *self) { + return plm_audio_has_header(self) + ? PLM_AUDIO_SAMPLE_RATE[self->samplerate_index] + : 0; +} + +double plm_audio_get_time(plm_audio_t *self) { + return self->time; +} + +void plm_audio_set_time(plm_audio_t *self, double time) { + self->samples_decoded = time * + (double)PLM_AUDIO_SAMPLE_RATE[self->samplerate_index]; + self->time = time; +} + +void plm_audio_rewind(plm_audio_t *self) { + plm_buffer_rewind(self->buffer); + self->time = 0; + self->samples_decoded = 0; + self->next_frame_data_size = 0; +} + +int plm_audio_has_ended(plm_audio_t *self) { + return plm_buffer_has_ended(self->buffer); +} + +plm_samples_t *plm_audio_decode(plm_audio_t *self) { + // Do we have at least enough information to decode the frame header? + if (!self->next_frame_data_size) { + if (!plm_buffer_has(self->buffer, 48)) { + return NULL; + } + self->next_frame_data_size = plm_audio_decode_header(self); + } + + if ( + self->next_frame_data_size == 0 || + !plm_buffer_has(self->buffer, self->next_frame_data_size << 3) + ) { + return NULL; + } + + plm_audio_decode_frame(self); + self->next_frame_data_size = 0; + + self->samples.time = self->time; + + self->samples_decoded += PLM_AUDIO_SAMPLES_PER_FRAME; + self->time = (double)self->samples_decoded / + (double)PLM_AUDIO_SAMPLE_RATE[self->samplerate_index]; + + return &self->samples; +} + +int plm_audio_find_frame_sync(plm_audio_t *self) { + size_t i; + for (i = self->buffer->bit_index >> 3; i < self->buffer->length-1; i++) { + if ( + self->buffer->bytes[i] == 0xFF && + (self->buffer->bytes[i+1] & 0xFE) == 0xFC + ) { + self->buffer->bit_index = ((i+1) << 3) + 3; + return TRUE; + } + } + self->buffer->bit_index = (i + 1) << 3; + return FALSE; +} + +int plm_audio_decode_header(plm_audio_t *self) { + if (!plm_buffer_has(self->buffer, 48)) { + return 0; + } + + plm_buffer_skip_bytes(self->buffer, 0x00); + int sync = plm_buffer_read(self->buffer, 11); + + + // Attempt to resync if no syncword was found. This sucks balls. The MP2 + // stream contains a syncword just before every frame (11 bits set to 1). + // However, this syncword is not guaranteed to not occur elswhere in the + // stream. So, if we have to resync, we also have to check if the header + // (samplerate, bitrate) differs from the one we had before. This all + // may still lead to garbage data being decoded :/ + + if (sync != PLM_AUDIO_FRAME_SYNC && !plm_audio_find_frame_sync(self)) { + return 0; + } + + self->version = plm_buffer_read(self->buffer, 2); + self->layer = plm_buffer_read(self->buffer, 2); + int hasCRC = !plm_buffer_read(self->buffer, 1); + + if ( + self->version != PLM_AUDIO_MPEG_1 || + self->layer != PLM_AUDIO_LAYER_II + ) { + return 0; + } + + int bitrate_index = plm_buffer_read(self->buffer, 4) - 1; + if (bitrate_index > 13) { + return 0; + } + + int samplerate_index = plm_buffer_read(self->buffer, 2); + if (samplerate_index == 3) { + return 0; + } + + int padding = plm_buffer_read(self->buffer, 1); + plm_buffer_skip(self->buffer, 1); // f_private + int mode = plm_buffer_read(self->buffer, 2); + + // If we already have a header, make sure the samplerate, bitrate and mode + // are still the same, otherwise we might have missed sync. + if ( + self->has_header && ( + self->bitrate_index != bitrate_index || + self->samplerate_index != samplerate_index || + self->mode != mode + ) + ) { + return 0; + } + + self->bitrate_index = bitrate_index; + self->samplerate_index = samplerate_index; + self->mode = mode; + self->has_header = TRUE; + + // Parse the mode_extension, set up the stereo bound + if (mode == PLM_AUDIO_MODE_JOINT_STEREO) { + self->bound = (plm_buffer_read(self->buffer, 2) + 1) << 2; + } + else { + plm_buffer_skip(self->buffer, 2); + self->bound = (mode == PLM_AUDIO_MODE_MONO) ? 0 : 32; + } + + // Discard the last 4 bits of the header and the CRC value, if present + plm_buffer_skip(self->buffer, 4); // copyright(1), original(1), emphasis(2) + if (hasCRC) { + plm_buffer_skip(self->buffer, 16); + } + + // Compute frame size, check if we have enough data to decode the whole + // frame. + int bitrate = PLM_AUDIO_BIT_RATE[self->bitrate_index]; + int samplerate = PLM_AUDIO_SAMPLE_RATE[self->samplerate_index]; + int frame_size = (144000 * bitrate / samplerate) + padding; + return frame_size - (hasCRC ? 6 : 4); +} + +void plm_audio_decode_frame(plm_audio_t *self) { + // Prepare the quantizer table lookups + int tab3 = 0; + int sblimit = 0; + + int tab1 = (self->mode == PLM_AUDIO_MODE_MONO) ? 0 : 1; + int tab2 = PLM_AUDIO_QUANT_LUT_STEP_1[tab1][self->bitrate_index]; + tab3 = QUANT_LUT_STEP_2[tab2][self->samplerate_index]; + sblimit = tab3 & 63; + tab3 >>= 6; + + if (self->bound > sblimit) { + self->bound = sblimit; + } + + // Read the allocation information + for (int sb = 0; sb < self->bound; sb++) { + self->allocation[0][sb] = plm_audio_read_allocation(self, sb, tab3); + self->allocation[1][sb] = plm_audio_read_allocation(self, sb, tab3); + } + + for (int sb = self->bound; sb < sblimit; sb++) { + self->allocation[0][sb] = + self->allocation[1][sb] = + plm_audio_read_allocation(self, sb, tab3); + } + + // Read scale factor selector information + int channels = (self->mode == PLM_AUDIO_MODE_MONO) ? 1 : 2; + for (int sb = 0; sb < sblimit; sb++) { + for (int ch = 0; ch < channels; ch++) { + if (self->allocation[ch][sb]) { + self->scale_factor_info[ch][sb] = plm_buffer_read(self->buffer, 2); + } + } + if (self->mode == PLM_AUDIO_MODE_MONO) { + self->scale_factor_info[1][sb] = self->scale_factor_info[0][sb]; + } + } + + // Read scale factors + for (int sb = 0; sb < sblimit; sb++) { + for (int ch = 0; ch < channels; ch++) { + if (self->allocation[ch][sb]) { + int *sf = self->scale_factor[ch][sb]; + switch (self->scale_factor_info[ch][sb]) { + case 0: + sf[0] = plm_buffer_read(self->buffer, 6); + sf[1] = plm_buffer_read(self->buffer, 6); + sf[2] = plm_buffer_read(self->buffer, 6); + break; + case 1: + sf[0] = + sf[1] = plm_buffer_read(self->buffer, 6); + sf[2] = plm_buffer_read(self->buffer, 6); + break; + case 2: + sf[0] = + sf[1] = + sf[2] = plm_buffer_read(self->buffer, 6); + break; + case 3: + sf[0] = plm_buffer_read(self->buffer, 6); + sf[1] = + sf[2] = plm_buffer_read(self->buffer, 6); + break; + } + } + } + if (self->mode == PLM_AUDIO_MODE_MONO) { + self->scale_factor[1][sb][0] = self->scale_factor[0][sb][0]; + self->scale_factor[1][sb][1] = self->scale_factor[0][sb][1]; + self->scale_factor[1][sb][2] = self->scale_factor[0][sb][2]; + } + } + + // Coefficient input and reconstruction + int out_pos = 0; + for (int part = 0; part < 3; part++) { + for (int granule = 0; granule < 4; granule++) { + + // Read the samples + for (int sb = 0; sb < self->bound; sb++) { + plm_audio_read_samples(self, 0, sb, part); + plm_audio_read_samples(self, 1, sb, part); + } + for (int sb = self->bound; sb < sblimit; sb++) { + plm_audio_read_samples(self, 0, sb, part); + self->sample[1][sb][0] = self->sample[0][sb][0]; + self->sample[1][sb][1] = self->sample[0][sb][1]; + self->sample[1][sb][2] = self->sample[0][sb][2]; + } + for (int sb = sblimit; sb < 32; sb++) { + self->sample[0][sb][0] = 0; + self->sample[0][sb][1] = 0; + self->sample[0][sb][2] = 0; + self->sample[1][sb][0] = 0; + self->sample[1][sb][1] = 0; + self->sample[1][sb][2] = 0; + } + + // Synthesis loop + for (int p = 0; p < 3; p++) { + // Shifting step + self->v_pos = (self->v_pos - 64) & 1023; + + for (int ch = 0; ch < 2; ch++) { + plm_audio_idct36(self->sample[ch], p, self->V[ch], self->v_pos); + + // Build U, windowing, calculate output + memset(self->U, 0, sizeof(self->U)); + + int d_index = 512 - (self->v_pos >> 1); + int v_index = (self->v_pos % 128) >> 1; + while (v_index < 1024) { + for (int i = 0; i < 32; ++i) { + self->U[i] += self->D[d_index++] * self->V[ch][v_index++]; + } + + v_index += 128 - 32; + d_index += 64 - 32; + } + + d_index -= (512 - 32); + v_index = (128 - 32 + 1024) - v_index; + while (v_index < 1024) { + for (int i = 0; i < 32; ++i) { + self->U[i] += self->D[d_index++] * self->V[ch][v_index++]; + } + + v_index += 128 - 32; + d_index += 64 - 32; + } + + // Output samples + #ifdef PLM_AUDIO_SEPARATE_CHANNELS + float *out_channel = ch == 0 + ? self->samples.left + : self->samples.right; + for (int j = 0; j < 32; j++) { + out_channel[out_pos + j] = self->U[j] / 2147418112.0f; + } + #else + for (int j = 0; j < 32; j++) { + self->samples.interleaved[((out_pos + j) << 1) + ch] = + self->U[j] / 2147418112.0f; + } + #endif + } // End of synthesis channel loop + out_pos += 32; + } // End of synthesis sub-block loop + + } // Decoding of the granule finished + } + + plm_buffer_align(self->buffer); +} + +const plm_quantizer_spec_t *plm_audio_read_allocation(plm_audio_t *self, int sb, int tab3) { + int tab4 = PLM_AUDIO_QUANT_LUT_STEP_3[tab3][sb]; + int qtab = PLM_AUDIO_QUANT_LUT_STEP_4[tab4 & 15][plm_buffer_read(self->buffer, tab4 >> 4)]; + return qtab ? (&PLM_AUDIO_QUANT_TAB[qtab - 1]) : 0; +} + +void plm_audio_read_samples(plm_audio_t *self, int ch, int sb, int part) { + const plm_quantizer_spec_t *q = self->allocation[ch][sb]; + int sf = self->scale_factor[ch][sb][part]; + int *sample = self->sample[ch][sb]; + int val = 0; + + if (!q) { + // No bits allocated for this subband + sample[0] = sample[1] = sample[2] = 0; + return; + } + + // Resolve scalefactor + if (sf == 63) { + sf = 0; + } + else { + int shift = (sf / 3) | 0; + sf = (PLM_AUDIO_SCALEFACTOR_BASE[sf % 3] + ((1 << shift) >> 1)) >> shift; + } + + // Decode samples + int adj = q->levels; + if (q->group) { + // Decode grouped samples + val = plm_buffer_read(self->buffer, q->bits); + sample[0] = val % adj; + val /= adj; + sample[1] = val % adj; + sample[2] = val / adj; + } + else { + // Decode direct samples + sample[0] = plm_buffer_read(self->buffer, q->bits); + sample[1] = plm_buffer_read(self->buffer, q->bits); + sample[2] = plm_buffer_read(self->buffer, q->bits); + } + + // Postmultiply samples + int scale = 65536 / (adj + 1); + adj = ((adj + 1) >> 1) - 1; + + val = (adj - sample[0]) * scale; + sample[0] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12; + + val = (adj - sample[1]) * scale; + sample[1] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12; + + val = (adj - sample[2]) * scale; + sample[2] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12; +} + +void plm_audio_idct36(int s[32][3], int ss, float *d, int dp) { + float t01, t02, t03, t04, t05, t06, t07, t08, t09, t10, t11, t12, + t13, t14, t15, t16, t17, t18, t19, t20, t21, t22, t23, t24, + t25, t26, t27, t28, t29, t30, t31, t32, t33; + + t01 = (float)(s[0][ss] + s[31][ss]); t02 = (float)(s[0][ss] - s[31][ss]) * 0.500602998235f; + t03 = (float)(s[1][ss] + s[30][ss]); t04 = (float)(s[1][ss] - s[30][ss]) * 0.505470959898f; + t05 = (float)(s[2][ss] + s[29][ss]); t06 = (float)(s[2][ss] - s[29][ss]) * 0.515447309923f; + t07 = (float)(s[3][ss] + s[28][ss]); t08 = (float)(s[3][ss] - s[28][ss]) * 0.53104259109f; + t09 = (float)(s[4][ss] + s[27][ss]); t10 = (float)(s[4][ss] - s[27][ss]) * 0.553103896034f; + t11 = (float)(s[5][ss] + s[26][ss]); t12 = (float)(s[5][ss] - s[26][ss]) * 0.582934968206f; + t13 = (float)(s[6][ss] + s[25][ss]); t14 = (float)(s[6][ss] - s[25][ss]) * 0.622504123036f; + t15 = (float)(s[7][ss] + s[24][ss]); t16 = (float)(s[7][ss] - s[24][ss]) * 0.674808341455f; + t17 = (float)(s[8][ss] + s[23][ss]); t18 = (float)(s[8][ss] - s[23][ss]) * 0.744536271002f; + t19 = (float)(s[9][ss] + s[22][ss]); t20 = (float)(s[9][ss] - s[22][ss]) * 0.839349645416f; + t21 = (float)(s[10][ss] + s[21][ss]); t22 = (float)(s[10][ss] - s[21][ss]) * 0.972568237862f; + t23 = (float)(s[11][ss] + s[20][ss]); t24 = (float)(s[11][ss] - s[20][ss]) * 1.16943993343f; + t25 = (float)(s[12][ss] + s[19][ss]); t26 = (float)(s[12][ss] - s[19][ss]) * 1.48416461631f; + t27 = (float)(s[13][ss] + s[18][ss]); t28 = (float)(s[13][ss] - s[18][ss]) * 2.05778100995f; + t29 = (float)(s[14][ss] + s[17][ss]); t30 = (float)(s[14][ss] - s[17][ss]) * 3.40760841847f; + t31 = (float)(s[15][ss] + s[16][ss]); t32 = (float)(s[15][ss] - s[16][ss]) * 10.1900081235f; + + t33 = t01 + t31; t31 = (t01 - t31) * 0.502419286188f; + t01 = t03 + t29; t29 = (t03 - t29) * 0.52249861494f; + t03 = t05 + t27; t27 = (t05 - t27) * 0.566944034816f; + t05 = t07 + t25; t25 = (t07 - t25) * 0.64682178336f; + t07 = t09 + t23; t23 = (t09 - t23) * 0.788154623451f; + t09 = t11 + t21; t21 = (t11 - t21) * 1.06067768599f; + t11 = t13 + t19; t19 = (t13 - t19) * 1.72244709824f; + t13 = t15 + t17; t17 = (t15 - t17) * 5.10114861869f; + t15 = t33 + t13; t13 = (t33 - t13) * 0.509795579104f; + t33 = t01 + t11; t01 = (t01 - t11) * 0.601344886935f; + t11 = t03 + t09; t09 = (t03 - t09) * 0.899976223136f; + t03 = t05 + t07; t07 = (t05 - t07) * 2.56291544774f; + t05 = t15 + t03; t15 = (t15 - t03) * 0.541196100146f; + t03 = t33 + t11; t11 = (t33 - t11) * 1.30656296488f; + t33 = t05 + t03; t05 = (t05 - t03) * 0.707106781187f; + t03 = t15 + t11; t15 = (t15 - t11) * 0.707106781187f; + t03 += t15; + t11 = t13 + t07; t13 = (t13 - t07) * 0.541196100146f; + t07 = t01 + t09; t09 = (t01 - t09) * 1.30656296488f; + t01 = t11 + t07; t07 = (t11 - t07) * 0.707106781187f; + t11 = t13 + t09; t13 = (t13 - t09) * 0.707106781187f; + t11 += t13; t01 += t11; + t11 += t07; t07 += t13; + t09 = t31 + t17; t31 = (t31 - t17) * 0.509795579104f; + t17 = t29 + t19; t29 = (t29 - t19) * 0.601344886935f; + t19 = t27 + t21; t21 = (t27 - t21) * 0.899976223136f; + t27 = t25 + t23; t23 = (t25 - t23) * 2.56291544774f; + t25 = t09 + t27; t09 = (t09 - t27) * 0.541196100146f; + t27 = t17 + t19; t19 = (t17 - t19) * 1.30656296488f; + t17 = t25 + t27; t27 = (t25 - t27) * 0.707106781187f; + t25 = t09 + t19; t19 = (t09 - t19) * 0.707106781187f; + t25 += t19; + t09 = t31 + t23; t31 = (t31 - t23) * 0.541196100146f; + t23 = t29 + t21; t21 = (t29 - t21) * 1.30656296488f; + t29 = t09 + t23; t23 = (t09 - t23) * 0.707106781187f; + t09 = t31 + t21; t31 = (t31 - t21) * 0.707106781187f; + t09 += t31; t29 += t09; t09 += t23; t23 += t31; + t17 += t29; t29 += t25; t25 += t09; t09 += t27; + t27 += t23; t23 += t19; t19 += t31; + t21 = t02 + t32; t02 = (t02 - t32) * 0.502419286188f; + t32 = t04 + t30; t04 = (t04 - t30) * 0.52249861494f; + t30 = t06 + t28; t28 = (t06 - t28) * 0.566944034816f; + t06 = t08 + t26; t08 = (t08 - t26) * 0.64682178336f; + t26 = t10 + t24; t10 = (t10 - t24) * 0.788154623451f; + t24 = t12 + t22; t22 = (t12 - t22) * 1.06067768599f; + t12 = t14 + t20; t20 = (t14 - t20) * 1.72244709824f; + t14 = t16 + t18; t16 = (t16 - t18) * 5.10114861869f; + t18 = t21 + t14; t14 = (t21 - t14) * 0.509795579104f; + t21 = t32 + t12; t32 = (t32 - t12) * 0.601344886935f; + t12 = t30 + t24; t24 = (t30 - t24) * 0.899976223136f; + t30 = t06 + t26; t26 = (t06 - t26) * 2.56291544774f; + t06 = t18 + t30; t18 = (t18 - t30) * 0.541196100146f; + t30 = t21 + t12; t12 = (t21 - t12) * 1.30656296488f; + t21 = t06 + t30; t30 = (t06 - t30) * 0.707106781187f; + t06 = t18 + t12; t12 = (t18 - t12) * 0.707106781187f; + t06 += t12; + t18 = t14 + t26; t26 = (t14 - t26) * 0.541196100146f; + t14 = t32 + t24; t24 = (t32 - t24) * 1.30656296488f; + t32 = t18 + t14; t14 = (t18 - t14) * 0.707106781187f; + t18 = t26 + t24; t24 = (t26 - t24) * 0.707106781187f; + t18 += t24; t32 += t18; + t18 += t14; t26 = t14 + t24; + t14 = t02 + t16; t02 = (t02 - t16) * 0.509795579104f; + t16 = t04 + t20; t04 = (t04 - t20) * 0.601344886935f; + t20 = t28 + t22; t22 = (t28 - t22) * 0.899976223136f; + t28 = t08 + t10; t10 = (t08 - t10) * 2.56291544774f; + t08 = t14 + t28; t14 = (t14 - t28) * 0.541196100146f; + t28 = t16 + t20; t20 = (t16 - t20) * 1.30656296488f; + t16 = t08 + t28; t28 = (t08 - t28) * 0.707106781187f; + t08 = t14 + t20; t20 = (t14 - t20) * 0.707106781187f; + t08 += t20; + t14 = t02 + t10; t02 = (t02 - t10) * 0.541196100146f; + t10 = t04 + t22; t22 = (t04 - t22) * 1.30656296488f; + t04 = t14 + t10; t10 = (t14 - t10) * 0.707106781187f; + t14 = t02 + t22; t02 = (t02 - t22) * 0.707106781187f; + t14 += t02; t04 += t14; t14 += t10; t10 += t02; + t16 += t04; t04 += t08; t08 += t14; t14 += t28; + t28 += t10; t10 += t20; t20 += t02; t21 += t16; + t16 += t32; t32 += t04; t04 += t06; t06 += t08; + t08 += t18; t18 += t14; t14 += t30; t30 += t28; + t28 += t26; t26 += t10; t10 += t12; t12 += t20; + t20 += t24; t24 += t02; + + d[dp + 48] = -t33; + d[dp + 49] = d[dp + 47] = -t21; + d[dp + 50] = d[dp + 46] = -t17; + d[dp + 51] = d[dp + 45] = -t16; + d[dp + 52] = d[dp + 44] = -t01; + d[dp + 53] = d[dp + 43] = -t32; + d[dp + 54] = d[dp + 42] = -t29; + d[dp + 55] = d[dp + 41] = -t04; + d[dp + 56] = d[dp + 40] = -t03; + d[dp + 57] = d[dp + 39] = -t06; + d[dp + 58] = d[dp + 38] = -t25; + d[dp + 59] = d[dp + 37] = -t08; + d[dp + 60] = d[dp + 36] = -t11; + d[dp + 61] = d[dp + 35] = -t18; + d[dp + 62] = d[dp + 34] = -t09; + d[dp + 63] = d[dp + 33] = -t14; + d[dp + 32] = -t05; + d[dp + 0] = t05; d[dp + 31] = -t30; + d[dp + 1] = t30; d[dp + 30] = -t27; + d[dp + 2] = t27; d[dp + 29] = -t28; + d[dp + 3] = t28; d[dp + 28] = -t07; + d[dp + 4] = t07; d[dp + 27] = -t26; + d[dp + 5] = t26; d[dp + 26] = -t23; + d[dp + 6] = t23; d[dp + 25] = -t10; + d[dp + 7] = t10; d[dp + 24] = -t15; + d[dp + 8] = t15; d[dp + 23] = -t12; + d[dp + 9] = t12; d[dp + 22] = -t19; + d[dp + 10] = t19; d[dp + 21] = -t20; + d[dp + 11] = t20; d[dp + 20] = -t13; + d[dp + 12] = t13; d[dp + 19] = -t24; + d[dp + 13] = t24; d[dp + 18] = -t31; + d[dp + 14] = t31; d[dp + 17] = -t02; + d[dp + 15] = t02; d[dp + 16] = 0.0; +} + + +#endif // PL_MPEG_IMPLEMENTATION diff --git a/src/video/rsp_mpeg1.S b/src/video/rsp_mpeg1.S new file mode 100644 index 000000000..986606dfe --- /dev/null +++ b/src/video/rsp_mpeg1.S @@ -0,0 +1,1420 @@ +############################################################ +# rsp_mpeg1.S +# +# Authors: Giovanni Bajo +############################################################ +#include +#include "mpeg1_internal.h" + + .data + + RSPQ_BeginOverlayHeader + RSPQ_DefineCommand cmd_mpeg1_load_matrix 4 # 0x50 + RSPQ_DefineCommand cmd_mpeg1_store_pixels 4 # 0x51 + RSPQ_DefineCommand cmd_mpeg1_idct 4 # 0x52 + RSPQ_DefineCommand cmd_mpeg1_block_begin 12 # 0x53 + RSPQ_DefineCommand cmd_mpeg1_block_coeff 4 # 0x54 + RSPQ_DefineCommand cmd_mpeg1_block_dequant 4 # 0x55 + RSPQ_DefineCommand cmd_mpeg1_block_decode 8 # 0x56 + RSPQ_DefineCommand cmd_mpeg1_store_matrix 4 # 0x57 + RSPQ_DefineCommand cmd_mpeg1_set_quant_mtx1 36 # 0x58 + RSPQ_DefineCommand cmd_mpeg1_set_quant_mtx2 36 # 0x59 + RSPQ_DefineCommand cmd_mpeg1_block_predict 12 # 0x5A + RSPQ_DefineCommand cmd_mpeg1_block_switch 4 # 0x5B + RSPQ_DefineCommand cmd_mpeg1_load_pixels 4 # 0x5C + RSPQ_DefineCommand cmd_mpeg1_zero_pixels 4 # 0x5D + .dcb.w 16-14 + RSPQ_EndOverlayHeader + + .align 4 + .ascii "Dragon RSP MPEG1" + .ascii " Coded by Rasky " + + .align 4 + RSPQ_BeginSavedState +IDCT_MATRIX: .dcb.w 8*8 # 8x8 coefficient matrix +COEFF_MASK: .dcb.b 8 + +INTER_QUANT_MATRIX: .dcb.b 64 # 8x8 quantization matrix for inter frames +INTRA_QUANT_MATRIX: .dcb.b 64 # 8x8 quantization matrix for intra frames + +RDRAM_BLOCK: .long 0 # Current block in RDRAM +RDRAM_BLOCK_SIZE: .long 0 # Current block size in RDRAM (DMA_SIZE format) +RDRAM_BLOCK_PITCH: .long 0 # Pitch of the block/frame in RDRAM + +RDRAM_BLOCKS: .long 0,0,0,0,0,0 +RDRAM_PITCHS: .long 0,0,0,0,0,0 +CUR_PIXELS: .long 0 + + .align 4 +PIXELS: .dcb.b (16*16 + 8*8 + 8*8) +PIXELCHECK: .long 0xBADC0DE + RSPQ_EndSavedState + +PIXELS_OFFSET: .half 0, 8, 16*8, 16*8+8, 16*16, 16*16+8*8 + + .align 4 + +IDCT_PREMULT: + #define PMSH (0) + .half 32<quantizer_scale * quant_matrix[] + vmudl $v01, $v01, v_scale,e(0) + + # Inverse quantization + # C: level * scale >> 4. + # + # NOTE: >>4 is not done here. The 4 additional bits are kept in the + # accumulator. + # + # NOTE: VMULQ has a behavior that, as far as I can tell, differs from + # published MPEG1 standard: when the number is negative, it adds a + # rounding value of 31 (!). This does not match official PDFs and other + # implementations. To be fully accurate, we need to revert this by + # subtracting 31 (via VRNDN16). + vmulq $v00, $v00, $v01 + vrndn16 $v00, km31 + + # Oddification and clamping + # + # C: if ((level & 1) == 0) { level += level > 0 ? -1 : 1; } + # C: if (level > 2047) { level = 2047; } + # C: if (level < -2048) { level = -2048; } + # + # The final result is <<4, but VMACQ returns a clamped value whose last + # 4 bits have been masked out, so we can safely use it anyway. + vmacq $v00 + + # Apply pre-multiplier. + # C: level = (level * PLM_VIDEO_PREMULTIPLIER_MATRIX[]) >> RSP_IDCT_SCALER; + # + # The final result doesn't fit in 16-bit, which is why we introduced + # a scaling by RSP_IDCT_SCALER. We fetch the high part from the accumulator + # and do a 32-bit shift. We take the chance to finally remove the <<4 + # left by the dequantization steps. + # + vmudn $v00, $v02, $v00 + vsar $v03, $v03, $v03,e(1) + vsrl $v00, $v00, (RSP_IDCT_SCALER+4) + vsll8 $v03, $v03, 16-(RSP_IDCT_SCALER+4) + vor $v00, $v00, $v03 + + # Keep only the values that contain actual coefficients. The others are + # forced to zero as the above sequence could have produced non-zero + # results. + vmrg $v00, $v00, kzero + + # Store the output and increment the loop counters + sqv $v00,0, 0,s0 + addi s0, 16 + addi s1, 8 + addi s2, 16 + addi s3, 1 + bnez loop_idx, dequant_loop + addi loop_idx, -1 + + # Restore initial DC coefficient + beqz intra, end_dequant + li s0, %lo(IDCT_MATRIX) + sh dc, 0(s0) + +end_dequant: + lw t0, %lo(PIXELCHECK) + assert_eq t0, 0xBADC0DE, ASSERT_PIXELCHECK(6) + + j RSPQ_Loop + nop + + #undef intra + #undef v_const2 + #undef kp1 + #undef km1 + .endfunc + + + .func cmd_mpeg1_load_matrix +cmd_mpeg1_load_matrix: + move s0, a0 + li t0, DMA_SIZE(8*8*2, 1) + j DMAIn + li s4, %lo(IDCT_MATRIX) + .endfunc + + .func cmd_mpeg1_store_matrix +cmd_mpeg1_store_matrix: + move s0, a0 + li t0, DMA_SIZE(8*8*2, 1) + j DMAOut + li s4, %lo(IDCT_MATRIX) + .endfunc + + .func cmd_mpeg1_zero_pixels +cmd_mpeg1_zero_pixels: + lw s4, %lo(CUR_PIXELS) + assert_ne s4, zero, ASSERT_UNDEFINED_BLOCK + + # Wait DMA idle before zeroing pixels, in case the + # previous macroblock was still being DMA'd to RDRAM. + jal DMAWaitIdle + vxor $v00, $v00, $v00 + sqv $v00,0, 0*16,s4 + sqv $v00,0, 1*16,s4 + sqv $v00,0, 2*16,s4 + sqv $v00,0, 3*16,s4 + j RSPQ_Loop + nop + .endfunc + + .func cmd_mpeg1_load_pixels +cmd_mpeg1_load_pixels: + lw t0, %lo(PIXELCHECK) + assert_eq t0, 0xBADC0DE, ASSERT_PIXELCHECK(7) + + lw s0, %lo(RDRAM_BLOCK) + assert_ne s0, zero, ASSERT_UNDEFINED_BLOCK + lw s4, %lo(CUR_PIXELS) + assert_ne s4, zero, ASSERT_UNDEFINED_BLOCK + lw t1, %lo(RDRAM_BLOCK_PITCH) + jal DMAIn + li t0, DMA_SIZE(8,8) + + lw t4, %lo(PIXELCHECK) + assert_eq t4, 0xBADC0DE, ASSERT_PIXELCHECK(8) + + j RSPQ_Loop + nop + + .endfunc + + .func cmd_mpeg1_store_pixels +cmd_mpeg1_store_pixels: + lw s0, %lo(RDRAM_BLOCK) + assert_ne s0, zero, ASSERT_UNDEFINED_BLOCK + lw s4, %lo(CUR_PIXELS) + assert_ne s4, zero, ASSERT_UNDEFINED_BLOCK + lw t1, %lo(RDRAM_BLOCK_PITCH) + j DMAOutAsync + lw t0, %lo(RDRAM_BLOCK_SIZE) + .endfunc + + .func load_matrix +load_matrix: + li s0, %lo(IDCT_MATRIX) + lqv $v00,0, 0*16,s0 + lqv $v01,0, 1*16,s0 + lqv $v02,0, 2*16,s0 + lqv $v03,0, 3*16,s0 + lqv $v04,0, 4*16,s0 + lqv $v05,0, 5*16,s0 + lqv $v06,0, 6*16,s0 + jr ra + lqv $v07,0, 7*16,s0 + .endfunc + + .func idct +idct: + move ra2, ra + + # Transform columns + jal mtx_idct_half + nop + + jal mtx_transpose + nop + + # Transform rows + jal mtx_idct_half + nop + + jal mtx_transpose + nop + + jr ra2 + nop + .endfunc + + .func add_pred +add_pred: + # Add prediction to residual + # The exact formula, assuming fixed 16.16, is: + # clamp_unsigned((PRED + RES + 0x8000) >> 16) + # + # where clamp unsigned is clamping the resulting pixel in both + # directions (so to both 0 and 255). + # + # This sequence VMULU+VMACU is used to perform the addition with rounding + # *and* clamping to 0 at the same time. The VMULU moves the PRED into the + # higher part of the accumulator and adds the rounding (0x8000), + # while the second VMACU moves the RES (residual/pixel) value into the + # higher part of the accumulator, does the addition, and perform + # the unsigned clamping in range [0, FFFF]. Obviously the higher + # range is useless (our pixels are [0..FF]) but at least we get + # the clamp towards 0 done, which is very annoying to do with + # RSP otherwise. + # + # The two coefficients (k1u and k2) are basically shift values used + # to align both PRED and RES into bits 16..31 of the accumulator. We need + # to align them there because that allows us to get the rounding for free + # since VMULU adds 0x8000 (bit 15). + vmulu pred0, pred0, k2 + vmacu $v00, $v00, k1u + vmulu pred1, pred1, k2 + vmacu $v01, $v01, k1u + vmulu pred2, pred2, k2 + vmacu $v02, $v02, k1u + vmulu pred3, pred3, k2 + vmacu $v03, $v03, k1u + vmulu pred4, pred4, k2 + vmacu $v04, $v04, k1u + vmulu pred5, pred5, k2 + vmacu $v05, $v05, k1u + vmulu pred6, pred6, k2 + vmacu $v06, $v06, k1u + vmulu pred7, pred7, k2 + vmacu $v07, $v07, k1u + + # Perform clamping towards 0xFF. This one is easy to do with VCH. + vch $v00, $v00, k255 + vch $v01, $v01, k255 + vch $v02, $v02, k255 + vch $v03, $v03, k255 + vch $v04, $v04, k255 + vch $v05, $v05, k255 + vch $v06, $v06, k255 + vch $v07, $v07, k255 + + # Shift back pixels into the correct bits to be stored in memory with SUV + vsll $v00, $v00, 7 + vsll $v01, $v01, 7 + vsll $v02, $v02, 7 + vsll $v03, $v03, 7 + vsll $v04, $v04, 7 + vsll $v05, $v05, 7 + vsll $v06, $v06, 7 + vsll $v07, $v07, 7 + +store_pixels: + # Store as pixels + lw s4, %lo(CUR_PIXELS) + lbu t0, %lo(RDRAM_BLOCK_SIZE)+3 + beq t0, 0xF, store_addpred_16 + nop + suv $v00,0, 0*8,s4 + suv $v01,0, 1*8,s4 + suv $v02,0, 2*8,s4 + suv $v03,0, 3*8,s4 + suv $v04,0, 4*8,s4 + suv $v05,0, 5*8,s4 + suv $v06,0, 6*8,s4 + jr ra + suv $v07,0, 7*8,s4 +store_addpred_16: + suv $v00,0, 0*16,s4 + suv $v01,0, 1*16,s4 + suv $v02,0, 2*16,s4 + suv $v03,0, 3*16,s4 + suv $v04,0, 4*16,s4 + suv $v05,0, 5*16,s4 + suv $v06,0, 6*16,s4 + jr ra + suv $v07,0, 7*16,s4 + .endfunc + + .func zero_pred +zero_pred: + vxor pred0, pred0, pred0 + vxor pred1, pred1, pred1 + vxor pred2, pred2, pred2 + vxor pred3, pred3, pred3 + vxor pred4, pred4, pred4 + vxor pred5, pred5, pred5 + vxor pred6, pred6, pred6 + jr ra + vxor pred7, pred7, pred7 + .endfunc + + .func cmd_mpeg1_idct +cmd_mpeg1_idct: + jal load_idct_consts + nop + jal load_matrix + nop + jal idct + nop + + #if RSP_IDCT_SCALER != 0 + vsll $v00, $v00, RSP_IDCT_SCALER + vsll $v01, $v01, RSP_IDCT_SCALER + vsll $v02, $v02, RSP_IDCT_SCALER + vsll $v03, $v03, RSP_IDCT_SCALER + vsll $v04, $v04, RSP_IDCT_SCALER + vsll $v05, $v05, RSP_IDCT_SCALER + vsll $v06, $v06, RSP_IDCT_SCALER + vsll $v07, $v07, RSP_IDCT_SCALER + #endif + + vaddc $v00, $v00, k128 + vaddc $v01, $v01, k128 + vaddc $v02, $v02, k128 + vaddc $v03, $v03, k128 + vaddc $v04, $v04, k128 + vaddc $v05, $v05, k128 + vaddc $v06, $v06, k128 + vaddc $v07, $v07, k128 + + # Store as pixels + lw s4, %lo(CUR_PIXELS) + lbu t0, %lo(RDRAM_BLOCK_SIZE)+3 + beq t0, 0xF, store_pred_16 + nop + spv $v00,0, 0*8,s4 + spv $v01,0, 1*8,s4 + spv $v02,0, 2*8,s4 + spv $v03,0, 3*8,s4 + spv $v04,0, 4*8,s4 + spv $v05,0, 5*8,s4 + spv $v06,0, 6*8,s4 + j RSPQ_Loop + spv $v07,0, 7*8,s4 +store_pred_16: + spv $v00,0, 0*16,s4 + spv $v01,0, 1*16,s4 + spv $v02,0, 2*16,s4 + spv $v03,0, 3*16,s4 + spv $v04,0, 4*16,s4 + spv $v05,0, 5*16,s4 + spv $v06,0, 6*16,s4 + j RSPQ_Loop + spv $v07,0, 7*16,s4 + .endfunc + + .func cmd_mpeg1_block_decode +cmd_mpeg1_block_decode: + # a0 = ncoeffs in matrix (low bytes) + # a1 = 1=intra, 0=inter + lw t0, %lo(PIXELCHECK) + assert_eq t0, 0xBADC0DE, ASSERT_PIXELCHECK(1) + + jal load_idct_consts + nop + jal load_matrix + nop + + beqz a1, load_pred + nop + jal_and_j zero_pred, decode_step2 + +load_pred: + # Load prediction. This must have been already + # loaded into the PIXELS block. + lw s4, %lo(CUR_PIXELS) + assert_ne s4, zero, ASSERT_UNDEFINED_BLOCK3 + + lbu t0, %lo(RDRAM_BLOCK_SIZE)+3 + beq t0, 0xF, load_pred_16 +load_pred_8: + luv pred0,0, 0*8,s4 + luv pred1,0, 1*8,s4 + luv pred2,0, 2*8,s4 + luv pred3,0, 3*8,s4 + luv pred4,0, 4*8,s4 + luv pred5,0, 5*8,s4 + luv pred6,0, 6*8,s4 + j decode_step2 + luv pred7,0, 7*8,s4 + +load_pred_16: + luv pred0,0, 0*16,s4 + luv pred1,0, 1*16,s4 + luv pred2,0, 2*16,s4 + luv pred3,0, 3*16,s4 + luv pred4,0, 4*16,s4 + luv pred5,0, 5*16,s4 + luv pred6,0, 6*16,s4 + luv pred7,0, 7*16,s4 + +decode_step2: + andi a0, 0xFF + addi a0, -1 + beqz a0, decode_dc_only + nop + +decode_ac: + jal idct + nop + li s0, %lo(IDCT_MATRIX) + sqv $v00,0, 0*16,s0 + sqv $v01,0, 1*16,s0 + sqv $v02,0, 2*16,s0 + sqv $v03,0, 3*16,s0 + sqv $v04,0, 4*16,s0 + sqv $v05,0, 5*16,s0 + sqv $v06,0, 6*16,s0 + sqv $v07,0, 7*16,s0 + jal_and_j add_pred, decode_finish + +decode_dc_only: + li s4, %lo(IDCT_MATRIX) + vxor $v07, $v07, $v07 + lqv $v00,0, 0,s4 + vor $v00, $v07, $v00,e(0) + vor $v01, $v07, $v00,e(0) + vor $v02, $v07, $v00,e(0) + vor $v03, $v07, $v00,e(0) + vor $v04, $v07, $v00,e(0) + vor $v05, $v07, $v00,e(0) + vor $v06, $v07, $v00,e(0) + vor $v07, $v07, $v00,e(0) + jal add_pred + nop + +decode_finish: + j RSPQ_Loop + nop + + .endfunc + + + .func mtx_transpose +mtx_transpose: + li s0, %lo(IDCT_MATRIX) + stv $v00,0, 0*16,s0 + stv $v00,2, 1*16,s0 + stv $v00,4, 2*16,s0 + stv $v00,6, 3*16,s0 + stv $v00,8, 4*16,s0 + stv $v00,10, 5*16,s0 + stv $v00,12, 6*16,s0 + stv $v00,14, 7*16,s0 + + ltv $v00,14, 1*16,s0 + ltv $v00,12, 2*16,s0 + ltv $v00,10, 3*16,s0 + ltv $v00,8, 4*16,s0 + ltv $v00,6, 5*16,s0 + ltv $v00,4, 6*16,s0 + ltv $v00,2, 7*16,s0 + + jr ra + nop + .endfunc + + .func mtx_idct_half +mtx_idct_half: +#define b1 $v04 +#define b3 $v08 +#define b4 $v09 +#define tmp1 $v10 +#define tmp2 $v11 +#define b6 $v12 +#define b7 $v13 +#define m0 $v00 +#define x4 $v14 +#define x0 $v15 +#define x1 $v10 // recycle tmp0 +#define x2 $v11 // recycle tmp1 +#define x3 $v16 +#define y3 $v17 +#define y4 $v18 +#define y5 $v19 +#define y6 $v20 +#define y7 $v10 // recycle x1 + + # b3 = v2+v6 + vaddc b3, $v02, $v06 + # b4 = v5-v3 + vsubc b4, $v05, $v03 + vsll b4, b4, 2 + # tmp1 = v1+v7 + vaddc tmp1, $v01, $v07 + # tmp2 = v03 + v05 + vaddc tmp2, $v03, $v05 + # b6 = v1 - v7 + vsubc b6, $v01, $v07 + vsll b6, b6, 2 + # b7 = tmp1 + tmp2 + vaddc b7, tmp1, tmp2 + # x4 = ((b6 * 473 - b4 * 196 + 128) >> 8) - b7 + vmulf x4, b6, k473 + vmacf x4, b4, km196 + vsubc x4, x4, b7 + # x0 = x4 - (((tmp1 - tmp2) * 362 + 128) >> 8); + vsubc x0, tmp1, tmp2 + vsll x0, x0, 2 + vmulf x0, x0, k362 + vsubc x0, x4, x0 + # x1 = m0 - b1 + vsubc x1, m0, b1 + # x2 = (((v2 - v6) * 362 + 128) >> 8) - b3 + vsubc x2, $v02, $v06 + vsll x2, x2, 2 + vmulf x2, x2, k362 + vsubc x2, x2, b3 + # x3 = m0 + b1 + vaddc x3, m0, b1 + # y3 = x1 + x2 + vaddc y3, x1, x2 + # y4 = x3 + b3 + vaddc y4, x3, b3 + # y5 = x1 - x2 + vsubc y5, x1, x2 + # y6 = x3 - b3 + vsubc y6, x3, b3 + # y7 = -x0 - ((b4 * 473 + b6 * 196 + 128) >> 8) + vmulf y7, b4, k473 + vmacf y7, b6, k196 + vaddc y7, y7, x0 + vxor $v00, $v00, $v00 + vsubc y7, $v00, y7 + + vaddc $v00, b7, y4 + vaddc $v01, x4, y3 + vsubc $v02, y5, x0 + vsubc $v03, y6, y7 + vaddc $v04, y6, y7 + vaddc $v05, x0, y5 + vsubc $v06, y3, x4 + vsubc $v07, y4, b7 + + jr ra + nop + + #undef b1 + #undef b3 + #undef b4 + #undef tmp1 + #undef tmp2 + #undef b6 + #undef b7 + #undef m0 + #undef x4 + #undef x0 + #undef x1 + #undef x2 + #undef x3 + #undef y3 + #undef y4 + #undef y5 + #undef y6 + #undef y7 + .endfunc + + +######################################################### +######################################################### +# +# Prediction +# +######################################################### +######################################################### + + #define dmem_16x16_pitch 24 + #define dmem_8x8_pitch 16 + #define kp1 vshift,e(7) + #define kp1e7 vshift,e(0) + #define kp1e6 vshift,e(1) + #define kp1e5 vshift,e(2) + #define kp1e4 vshift,e(3) + #define kp1e15 vshift8,e(0) + #define kp1e14 vshift8,e(1) + #define kp1e13 vshift8,e(2) + #define block_size t8 + + + .func block_copy_8x8 +block_copy_8x8: + # s0: source buffer (pitch = dmem_8x8_pitch) + # s4: dest buffer (pitch = 8) + beq block_size, 16, block_copy_16x16 + + addi t0, block_size, -2 +1: + add s3, s4, block_size + luv $v00,0, 0*dmem_8x8_pitch,s0 + luv $v01,0, 1*dmem_8x8_pitch,s0 + suv $v00,0, 0,s4 + suv $v01,0, 0,s3 + addi s0, 2*dmem_8x8_pitch + add s4, s3, block_size + bgtz t0, 1b + addi t0, -2 + + jr ra + nop + .endfunc + + .func block_copy_16x16 +block_copy_16x16: + # s0: source buffer (pitch = dmem_16x16_pitch) + # s4: dest buffer (pitch = 8) + li t1, 8 + addi t0, block_size, -2 +1: + add s3, s4, block_size + luv $v00,0, 0*dmem_16x16_pitch,s0 + luv $v01,0, 1*dmem_16x16_pitch,s0 + suv $v00,0, 0,s4 + suv $v01,0, 0,s3 + luv $v00,0, 0*dmem_16x16_pitch+8,s0 + luv $v01,0, 1*dmem_16x16_pitch+8,s0 + suv $v00,0, 8,s4 + suv $v01,0, 8,s3 + addi s0, 2*dmem_16x16_pitch + add s4, s3, block_size + bgtz t0, 1b + addi t0, -2 + + jr ra + nop + .endfunc + + .func block_interp_8x8 +block_interp_8x8: + # s0: source buffer (pitch = dmem_8x8_pitch) + # s4: dest buffer (pitch = 8) + #define line t1 + + beq block_size, 16, block_interp_16x16 + + li t0, 8-2 +1: + luv $v00,0, 0*dmem_8x8_pitch,s0 + luv $v01,0, 1*dmem_8x8_pitch,s0 + luv $v02,0, 0*8,s4 + luv $v03,0, 1*8,s4 + + vaddc $v04,$v00,$v02,0 + vaddc $v05,$v01,$v03,0 + + vaddc $v04,$v04,kp1e7 + vaddc $v05,$v05,kp1e7 + + spv $v04,0, 0*8,s4 + spv $v05,0, 1*8,s4 + + addi s0, 2*dmem_8x8_pitch + addi s4, 2*8 + bgtz t0, 1b + addi t0, -2 + + jr ra + nop + #undef line + .endfunc + + .func block_interp_16x16 +block_interp_16x16: + # s0: source buffer (pitch = dmem_16x16_pitch) + # s4: dest buffer (pitch = 8) + #define line t1 + + li t0, 16-1 +1: + luv $v00,0, 0*8,s0 + luv $v01,0, 1*8,s0 + luv $v02,0, 0*8,s4 + luv $v03,0, 1*8,s4 + + vaddc $v04,$v00,$v02 + vaddc $v05,$v01,$v03 + + vaddc $v04,$v04,kp1e7 + vaddc $v05,$v05,kp1e7 + + spv $v04,0, 0*8,s4 + spv $v05,0, 1*8,s4 + + addi s0, dmem_16x16_pitch + addi s4, 2*8 + bgtz t0, 1b + addi t0, -1 + + jr ra + nop + #undef line + .endfunc + + + .func block_copy_8x8_filter2 +block_copy_8x8_filter2: + # s0: source buffer (pitch = dmem_8x8_pitch) + # s1: second pointer into source buffer (for interpolation) + # s4: dest buffer (pitch = 8) + #define line t1 + + beq block_size, 16, block_copy_16x16_filter2 + + # We calculate two lines at a time, to be faster + li line, 8-2 +1: + luv $v00,0, 0,s0 + luv $v01,0, 0,s1 + luv $v02,0, dmem_8x8_pitch,s0 + luv $v03,0, dmem_8x8_pitch,s1 + + vaddc $v04,$v00,$v01,0 + vaddc $v05,$v02,$v03,0 + + vaddc $v04,$v04,kp1e7 + vaddc $v05,$v05,kp1e7 + + spv $v04,0, 0*8,s4 + spv $v05,0, 1*8,s4 + + addi s0, dmem_8x8_pitch*2 + addi s1, dmem_8x8_pitch*2 + addi s4, 2*8 + bgtz line, 1b + addi line, -2 + + jr ra + nop + + #undef line + .endfunc + + .func block_interp_8x8_filter2 +block_interp_8x8_filter2: + # s0: source buffer (pitch = dmem_8x8_pitch) + # s1: second pointer into source buffer (for interpolation) + # s4: dest buffer (pitch = 8) + #define line t1 + + beq block_size, 16, block_interp_16x16_filter2 + + # We calculate two lines at a time, to be faster + li line, 8-2 +1: + luv $v00,0, 0,s0 + luv $v01,0, 0,s1 + luv $v02,0, dmem_8x8_pitch,s0 + luv $v03,0, dmem_8x8_pitch,s1 + luv $v08,0, 0*8,s4 + luv $v09,0, 1*8,s4 + + vaddc $v04,$v00,$v01 + vaddc $v05,$v02,$v03 + + vaddc $v04,$v04,kp1e7 + vaddc $v05,$v05,kp1e7 + + vsrl $v04, $v04, 1 + vsrl $v05, $v05, 1 + + vaddc $v04,$v04,$v08 + vaddc $v05,$v05,$v09 + + vaddc $v04,$v04,kp1e7 + vaddc $v05,$v05,kp1e7 + + spv $v04,0, 0*8,s4 + spv $v05,0, 1*8,s4 + + addi s0, dmem_8x8_pitch*2 + addi s1, dmem_8x8_pitch*2 + addi s4, 2*8 + bgtz line, 1b + addi line, -2 + + jr ra + nop + + #undef line + .endfunc + + .func block_copy_16x16_filter2 +block_copy_16x16_filter2: + # s0: source buffer (pitch = dmem_16x16_pitch) + # s1: second pointer into source buffer (for interpolation) + # s4: dest buffer (pitch = 8) + #define line t1 + + li line, 16-1 +1: + luv $v00,0, 0,s0 + luv $v01,0, 0,s1 + luv $v02,0, 8,s0 + luv $v03,0, 8,s1 + + vaddc $v04,$v00,$v01,0 + vaddc $v05,$v02,$v03,0 + + vaddc $v04,$v04,kp1e7 + vaddc $v05,$v05,kp1e7 + + spv $v04,0, 0*8,s4 + spv $v05,0, 1*8,s4 + + addi s0, dmem_16x16_pitch + addi s1, dmem_16x16_pitch + addi s4, 16 + bgtz line, 1b + addi line, -1 + + jr ra + nop + + #undef line + .endfunc + + .func block_interp_16x16_filter2 +block_interp_16x16_filter2: + # s0: source buffer (pitch = dmem_16x16_pitch) + # s1: second pointer into source buffer (for interpolation) + # s4: dest buffer (pitch = 8) + #define line t1 + + li line, 16-1 +1: + luv $v00,0, 0,s0 + luv $v01,0, 0,s1 + luv $v02,0, 8,s0 + luv $v03,0, 8,s1 + luv $v08,0, 0,s4 + luv $v09,0, 8,s4 + + vaddc $v04,$v00,$v01,0 + vaddc $v05,$v02,$v03,0 + vaddc $v04,$v04,kp1e7 + vaddc $v05,$v05,kp1e7 + vsrl $v04,$v04,1 + vsrl $v05,$v05,1 + vaddc $v04,$v04,$v08 + vaddc $v05,$v05,$v09 + vaddc $v04,$v04,kp1e7 + vaddc $v05,$v05,kp1e7 + + spv $v04,0, 0*8,s4 + spv $v05,0, 1*8,s4 + + addi s0, dmem_16x16_pitch + addi s1, dmem_16x16_pitch + addi s4, 16 + bgtz line, 1b + addi line, -1 + + jr ra + nop + + #undef line + .endfunc + + .func block_copy_8x8_filter4 +block_copy_8x8_filter4: + # s0: source buffer (pitch = dmem_8x8_pitch) + # s4: dest buffer (pitch = 8) + #define line t1 + + beq block_size, 16, block_copy_16x16_filter4 + + addi s1, s0, 1 + addi s2, s0, dmem_8x8_pitch + addi s3, s2, 1 + li line, 7 + +copy_loop_4: + luv $v00,0, 0,s0 + luv $v01,0, 0,s1 + luv $v02,0, 0,s2 + luv $v03,0, 0,s3 + + vmudl $v04,$v00,kp1e14 + vmadl $v04,$v01,kp1e14 + vmadl $v04,$v02,kp1e14 + vmadl $v04,$v03,kp1e14 + vaddc $v04,$v04,kp1e6 + + suv $v04,0, 0,s4 + add s0, dmem_8x8_pitch + add s1, dmem_8x8_pitch + add s2, dmem_8x8_pitch + add s3, dmem_8x8_pitch + add s4, 8 + bgtz line, copy_loop_4 + addi line, -1 + + jr ra + nop + + #undef line + .endfunc + + .func block_copy_16x16_filter4 +block_copy_16x16_filter4: + # s0: source buffer (pitch = dmem_16x16_pitch) + # s4: dest buffer (pitch = 8) + #define line t1 + + addi s1, s0, 1 + addi s2, s0, dmem_16x16_pitch + addi s3, s2, 1 + li line, 15 + +1: + luv $v00,0, 0,s0 + luv $v01,0, 0,s1 + luv $v02,0, 0,s2 + luv $v03,0, 0,s3 + + luv $v04,0, 8,s0 + luv $v05,0, 8,s1 + luv $v06,0, 8,s2 + luv $v07,0, 8,s3 + + vmudl $v16,$v00,kp1e14 + vmadl $v16,$v01,kp1e14 + vmadl $v16,$v02,kp1e14 + vmadl $v16,$v03,kp1e14 + + vmudl $v17,$v04,kp1e14 + vmadl $v17,$v05,kp1e14 + vmadl $v17,$v06,kp1e14 + vmadl $v17,$v07,kp1e14 + + vaddc $v16,$v16,kp1e6 + vaddc $v17,$v17,kp1e6 + + suv $v16,0, 0,s4 + suv $v17,0, 8,s4 + add s0, dmem_16x16_pitch + add s1, dmem_16x16_pitch + add s2, dmem_16x16_pitch + add s3, dmem_16x16_pitch + add s4, 16 + bgtz line, 1b + addi line, -1 + + jr ra + nop + + #undef line + .endfunc + + + .func block_interp_8x8_filter4 +block_interp_8x8_filter4: + # s0: source buffer (pitch = dmem_8x8_pitch) + # s4: dest buffer (pitch = 8) + #define line t1 + #define kp1e7p1e6 $v10,e(0) + + li t0, 0xC0 + mtc2 t0, $v10,0 + + beq block_size, 16, block_interp_16x16_filter4 + addi s1, s0, 1 + + li line, 7 + + luv $v00,0, 0,s0 + luv $v01,0, 0,s1 + luv $v02,0, dmem_8x8_pitch,s0 + luv $v03,0, dmem_8x8_pitch,s1 + lpv $v05,0, -8,s4 + .align 3 +1: + vmudl $v04,$v00,kp1e14; luv $v08,0, 0,s4 + vmadl $v04,$v01,kp1e14; add s0, dmem_8x8_pitch + vmadl $v04,$v02,kp1e14; add s1, dmem_8x8_pitch + vmadl $v04,$v03,kp1e14; spv $v05,0, -8,s4 + luv $v00,0, 0,s0 + luv $v01,0, 0,s1 + vaddc $v04,$v04,kp1e7p1e6; luv $v02,0, dmem_8x8_pitch,s0 + luv $v03,0, dmem_8x8_pitch,s1 + add s4, 8 + vaddc $v05,$v04,$v08; bgtz line, 1b + addi line, -1 + + jr ra + spv $v05,0, -8,s4 + + .endfunc + .func block_interp_16x16_filter4 +block_interp_16x16_filter4: + # s0: source buffer (pitch = dmem_16x16_pitch) + # s4: dest buffer (pitch = 8) + #define line t1 + #define kp1e7p1e6 $v10,e(0) + + li line, 16 + + luv $v00,0, 0,s0 + luv $v01,0, 0,s1 + luv $v02,0, dmem_16x16_pitch,s0 + luv $v03,0, dmem_16x16_pitch,s1 + .align 3 +1: + vmudl $v16,$v00,kp1e14; luv $v04,0, 8,s0 + vmadl $v16,$v01,kp1e14; luv $v05,0, 8,s1 + vmadl $v16,$v02,kp1e14; luv $v06,0, dmem_16x16_pitch+8,s0 + vmadl $v16,$v03,kp1e14; luv $v07,0, dmem_16x16_pitch+8,s1 + + vmudl $v17,$v04,kp1e14; luv $v08,0, 0,s4 + vmadl $v17,$v05,kp1e14; luv $v09,0, 8,s4 + vmadl $v17,$v06,kp1e14; add s0, dmem_16x16_pitch + vmadl $v17,$v07,kp1e14; add s1, dmem_16x16_pitch + + luv $v00,0, 0,s0 + vaddc $v16,$v16,kp1e7p1e6; luv $v01,0, 0,s1 + vaddc $v17,$v17,kp1e7p1e6; luv $v02,0, dmem_16x16_pitch,s0 + luv $v03,0, dmem_16x16_pitch,s1 + + vaddc $v16,$v16,$v08; addi line, -1 + vaddc $v17,$v17,$v09; add s4, 16 + + spv $v16,0, -16,s4 + bgtz line, 1b + spv $v17,0, -8,s4 + jr ra + nop + + #undef kp1e7p1e6 + #undef line + .endfunc + + .func cmd_mpeg1_block_predict +cmd_mpeg1_block_predict: + # a0: source + # a1: source pitch + # a2: oddh/oddv + + #define src_pitch a1 + + # Calculate DMA size. In general, for filtering, we need to + # DMA one pixel more both horizontally and vertically. Given the + # 8-byte constraint on RSP DMA, this means 24x17 for a 16x16 block + # and 16x9 for a 8x8 block. To calculate it, it's sufficient to + # add 1 to both W and H in the block size, and the RSP will round up + # to 8 automatically. + lw t0, %lo(RDRAM_BLOCK_SIZE) + addi t0, DMA_SIZE(2,2) + andi block_size, t0, 0xFF + + li s4, %lo(SOURCE_PIXELS) + move s0, a0 + jal DMAIn + move t1, a1 + + move s0, s4 + lw s4, %lo(CUR_PIXELS) + + andi t0, a2, 0x4 + bnez t0, predict_interpolate + xor a2, t0 + +predict_copy: + beqz a2, copy + addi a2, -1 + beqz a2, copy_odd_v + addi a2, -1 + beqz a2, copy_odd_h + nop + + jal_and_j block_copy_8x8_filter4, RSPQ_Loop + +copy_odd_h: + addi s1, s0, 1 + jal_and_j block_copy_8x8_filter2, RSPQ_Loop + +copy_odd_v: + add s1, s0, block_size + addi s1, 8 + jal_and_j block_copy_8x8_filter2, RSPQ_Loop + +copy: + jal_and_j block_copy_8x8, RSPQ_Loop + +predict_interpolate: + beqz a2, interpolate + addi a2, -1 + beqz a2, interpolate_odd_v + addi a2, -1 + beqz a2, interpolate_odd_h + nop + jal_and_j block_interp_8x8_filter4, RSPQ_Loop + +interpolate_odd_h: + addi s1, s0, 1 + jal_and_j block_interp_8x8_filter2, RSPQ_Loop + +interpolate_odd_v: + add s1, s0, block_size + addi s1, 8 + jal_and_j block_interp_8x8_filter2, RSPQ_Loop + +interpolate: + jal_and_j block_interp_8x8, RSPQ_Loop + + diff --git a/src/video/rsp_yuv.S b/src/video/rsp_yuv.S new file mode 100644 index 000000000..e315845d3 --- /dev/null +++ b/src/video/rsp_yuv.S @@ -0,0 +1,376 @@ +############################################################ +# rsp_yuv.S +# +# Authors: Giovanni Bajo +############################################################ +#include +#include "yuv_internal.h" + + .data + + RSPQ_BeginOverlayHeader + RSPQ_DefineCommand cmd_yuv_set_input 16 + RSPQ_DefineCommand cmd_yuv_set_output 8 + RSPQ_DefineCommand cmd_yuv_interleave4_block_32x16, 4 + RSPQ_DefineCommand cmd_yuv_interleave2_block_32x16, 4 + RSPQ_EndOverlayHeader + + .align 4 + .ascii " Dragon RSP YUV " + .ascii " Coded by Rasky " + + RSPQ_BeginSavedState +RDRAM_YBUF_STRIDE: .half 0 +RDRAM_CBUF_STRIDE: .half 0 +RDRAM_OUTBUF_STRIDE: .half 0 +RDRAM_YBUF: .long 0 +RDRAM_CBBUF: .long 0 +RDRAM_CRBUF: .long 0 +RDRAM_OUTBUF: .long 0 + RSPQ_EndSavedState + + # FIXME? + #.section bss.overlay + + .align 4 + +V1TEMP: .quad 0,0,0,0 +V2TEMP: .quad 0,0,0,0 + + .align 3 + +YBUF: .ds.b BLOCK_W * BLOCK_H +CBBUF: .ds.b (BLOCK_W/2) * (BLOCK_H/2) +CRBUF: .ds.b (BLOCK_W/2) * (BLOCK_H/2) +OUTBUF: .ds.b BLOCK_W * BLOCK_H * 2 + + .text + + .func cmd_yuv_set_input +cmd_yuv_set_input: + sw a0, %lo(RDRAM_YBUF) + sw a1, %lo(RDRAM_CBBUF) + sw a2, %lo(RDRAM_CRBUF) + sh a3, %lo(RDRAM_YBUF_STRIDE) + srl a3, 1 + jr ra + sh a3, %lo(RDRAM_CBUF_STRIDE) + .endfunc + + .func cmd_yuv_set_output +cmd_yuv_set_output: + sw a0, %lo(RDRAM_OUTBUF) + jr ra + sh a1, %lo(RDRAM_OUTBUF_STRIDE) + .endfunc + + + + .func cmd_yuv_interleave4_block_32x16 +cmd_yuv_interleave4_block_32x16: + #define x0y0 a0 + #define ybuf_off t5 + #define cbuf_off t6 + #define outbuf_off t7 + #define y t8 + + #define ybuf s0 + #define cbbuf s1 + #define crbuf s2 + #define outbuf s3 + + # Calculate y0*stride+x0 for both input and output buffers + li s0, %lo(RDRAM_YBUF_STRIDE) + lqv $v01,0, 0,s0 + + andi t0, x0y0, 0xFFF + mtc2 t0, $v00,0*2 # ybuf => y + mtc2 t0, $v00,2*2 # outbuf => y + srl t0, 1 + mtc2 t0, $v00,1*2 # cbuf => y/2 + + vmudn $v00, $v01, $v00 + srl t3, x0y0, 12 + andi t3, 0xFFF + + mfc2 ybuf_off, $v00,0*2 + mfc2 cbuf_off, $v00,1*2 + mfc2 outbuf_off, $v00,2*2 + + vsar $v01, $v01, $v01,9 + mfc2 t0, $v01,0*2 + mfc2 t1, $v01,1*2 + mfc2 t2, $v01,2*2 + + andi ybuf_off, 0xFFFF + andi cbuf_off, 0xFFFF + andi outbuf_off, 0xFFFF + + sll t0, 16 + sll t1, 16 + sll t2, 16 + + add ybuf_off, t0 + add cbuf_off, t1 + add outbuf_off, t2 + + add ybuf_off, t3 + sll t3, 1 + add outbuf_off, t3 + srl t3, 2 + add cbuf_off, t3 + + + # Fetch Y plane + lh t1, %lo(RDRAM_YBUF_STRIDE) + lw s0, %lo(RDRAM_YBUF) + assert_ne s0, 0, ASSERT_INVALID_INPUT_Y + add s0, ybuf_off + li s4, %lo(YBUF) + jal DMAInAsync + li t0, DMA_SIZE(BLOCK_W, BLOCK_H) + + # Fetch CB plane + lh t1, %lo(RDRAM_CBUF_STRIDE) + lw s0, %lo(RDRAM_CBBUF) + assert_ne s0, 0, ASSERT_INVALID_INPUT_CB + add s0, cbuf_off + li s4, %lo(CBBUF) + jal DMAInAsync + li t0, DMA_SIZE(BLOCK_W/2, BLOCK_H/2) + + # Fetch CR plane + lw s0, %lo(RDRAM_CRBUF) + assert_ne s0, 0, ASSERT_INVALID_INPUT_CR + add s0, cbuf_off + li s4, %lo(CRBUF) + jal DMAIn + li t0, DMA_SIZE(BLOCK_W/2, BLOCK_H/2) + + li y, BLOCK_H/2-1 + li t1, %lo(V1TEMP) + li t2, %lo(V2TEMP) + li ybuf, %lo(YBUF) + li cbbuf, %lo(CBBUF) + li crbuf, %lo(CRBUF) + li outbuf, %lo(OUTBUF) + +#if BLOCK_W != 32 + break +#endif + + # Interleave 2 lines of 32 horizontal pixels each loop. The code + # has been laid out to take maximum advantage of parallel VU/SU execution + # of opcodes, taking also into account instruction latency to avoid + # pipeline stalls (4 ops for memory accesses and 3 ops for each + # vector instruction). + # + # This optimized loop takes 34 cycles to interleave 64 pixels. + .align 3 + InterleaveLoopRow2: + # Read CB/CR elements + lpv $v15,0, 0*8,crbuf # CR<<8 (first half) # 1 + lpv $v17,0, 1*8,crbuf # CR<<8 (second half) # 2 + lpv $v14,0, 0*8,cbbuf # CB<<8 (first half) # 3 + lpv $v16,0, 1*8,cbbuf # CB<<8 (second half) # 4 + + # Calculate (CB<<8)|CR # Read Y buffer elements + vsrl $v15, $v15, 8; lpv $v00,0, 0*8,ybuf # row 0, first quarter # 5 + vsrl $v17, $v17, 8; lpv $v01,0, 1*8,ybuf # row 0, second quarter # 6 + + lpv $v08,0, 4*8,ybuf # row 1, first quarter # 7 + vor $v15, $v15, $v14,0; lpv $v09,0, 5*8,ybuf # row 1, second quarter # 8 + vor $v17, $v17, $v16,0; lpv $v02,0, 2*8,ybuf # row 0, third quarter # 9 + lpv $v03,0, 3*8,ybuf # row 0, fourth quarter # 10 + + # Shift Y buffer elements right # Store interleaved CB/CR into t1/t2 + # to go into the lower byte of # and finish reading Y buffer + # each lane of vregs. + vsrl $v00, $v00, 8; sqv $v15,0, 0*16,t1 # 11,12 (stall: store 2 cycles after load) + vsrl $v01, $v01, 8; sqv $v17,0, 0*16,t2 # 13 + vsrl $v08, $v08, 8; lpv $v10,0, 6*8,ybuf # row 1, third quarter # 14 + vsrl $v09, $v09, 8; lpv $v11,0, 7*8,ybuf # row 1, fourth quarter # 15 + + # Read back CB/CR interleaved values + vsrl $v02, $v02, 8; lpv $v14,0, 0*8,t1 # 16 + vsrl $v03, $v03, 8; lpv $v15,0, 1*8,t1 # 17 + vsrl $v10, $v10, 8; lpv $v16,0, 0*8,t2 # 18 + vsrl $v11, $v11, 8; lpv $v17,0, 1*8,t2 # 19 + + # Compose Y with CB/Cr # Prepare pointers for next loop + vor $v00, $v00, $v14,0; addi crbuf, 8*2 # 20 + vor $v01, $v01, $v15,0; addi cbbuf, 8*2 # 21 + vor $v08, $v08, $v14,0; addi ybuf, 32*2 # 22 + vor $v09, $v09, $v15,0; addi outbuf, 64*2 # 23 + + # Write interleaved pixels in output + # buffer. + vor $v02, $v02, $v16,0; sqv $v00,0, -8*16,outbuf # 24 + vor $v03, $v03, $v17,0; sqv $v01,0, -7*16,outbuf # 25 + vor $v10, $v10, $v16,0; sqv $v08,0, -4*16,outbuf # 26 + vor $v11, $v11, $v17,0; sqv $v09,0, -3*16,outbuf # 27 + + sqv $v02,0, -6*16,outbuf # 28 + sqv $v03,0, -5*16,outbuf # 29 + sqv $v10,0, -2*16,outbuf # 30 + sqv $v11,0, -1*16,outbuf # 31 + + bnez y, InterleaveLoopRow2 # 32 + addi y, -1 # 33,34 (stall: taken branch) + + + # DMA output buffer + lh t1, %lo(RDRAM_OUTBUF_STRIDE) + lw s0, %lo(RDRAM_OUTBUF) + assert_ne s0, 0, ASSERT_INVALID_OUTPUT + add s0, outbuf_off + li s4, %lo(OUTBUF) + li t0, DMA_SIZE(BLOCK_W*2, BLOCK_H) + jal_and_j DMAOut, RSPQ_Loop + + #undef x0y0 + #undef ybuf_off + #undef cbuf_off + #undef outbuf_off + #undef y + + #undef ybuf + #undef cbbuf + #undef crbuf + #undef outbuf + #undef outbuf2 + .endfunc + + .func cmd_yuv_interleave2_block_32x16 +cmd_yuv_interleave2_block_32x16: + #define x0y0 a0 + #define ybuf_off t5 + #define cbuf_off t6 + #define outbuf_off t7 + #define y t8 + + #define ybuf s0 + #define cbbuf s1 + #define crbuf s2 + #define outbuf s3 + #define outbuf2 s4 + # Calculate y0*stride+x0 for both input and output buffers + li s0, %lo(RDRAM_YBUF_STRIDE) + lqv $v01,0, 0,s0 + + andi t0, x0y0, 0xFFF + mtc2 t0, $v00,0*2 # ybuf => y + srl t0, 1 + mtc2 t0, $v00,2*2 # outbuf => y/2 + mtc2 t0, $v00,1*2 # cbuf => y/2 + + vmudn $v00, $v01, $v00 + srl t3, x0y0, 12 + andi t3, 0xFFF + + mfc2 ybuf_off, $v00,0*2 + mfc2 cbuf_off, $v00,1*2 + mfc2 outbuf_off, $v00,2*2 + + vsar $v01, $v01, $v01,9 + mfc2 t0, $v01,0*2 + mfc2 t1, $v01,1*2 + mfc2 t2, $v01,2*2 + + andi ybuf_off, 0xFFFF + andi cbuf_off, 0xFFFF + andi outbuf_off, 0xFFFF + + sll t0, 16 + sll t1, 16 + sll t2, 16 + + add ybuf_off, t0 + add cbuf_off, t1 + add outbuf_off, t2 + + add ybuf_off, t3 + add outbuf_off, t3 + srl t3, 1 + add cbuf_off, t3 + + + # Fetch CB plane + lh t1, %lo(RDRAM_CBUF_STRIDE) + lw s0, %lo(RDRAM_CBBUF) + assert_ne s0, 0, ASSERT_INVALID_INPUT_CB + add s0, cbuf_off + li s4, %lo(CBBUF) + jal DMAInAsync + li t0, DMA_SIZE(BLOCK_W/2, BLOCK_H/2) + + # Fetch CR plane + lw s0, %lo(RDRAM_CRBUF) + assert_ne s0, 0, ASSERT_INVALID_INPUT_CR + add s0, cbuf_off + li s4, %lo(CRBUF) + jal DMAIn + li t0, DMA_SIZE(BLOCK_W/2, BLOCK_H/2) + + li y, BLOCK_H/2-1 + li t1, %lo(V1TEMP) + li t2, %lo(V2TEMP) + li cbbuf, %lo(CBBUF) + li crbuf, %lo(CRBUF) + li outbuf, %lo(OUTBUF) + addi outbuf2, outbuf, 1 + +#if BLOCK_W != 32 + break +#endif + +1: + luv $v00,0, 0*8,cbbuf + luv $v02,0, 1*8,cbbuf + luv $v04,0, 2*8,cbbuf + luv $v06,0, 3*8,cbbuf + + luv $v01,0, 0*8,crbuf + luv $v03,0, 1*8,crbuf + luv $v05,0, 2*8,crbuf + luv $v07,0, 3*8,crbuf + + shv $v00,0, 0*16,outbuf + shv $v01,0, 0*16,outbuf2 + shv $v02,0, 1*16,outbuf + shv $v03,0, 1*16,outbuf2 + shv $v04,0, 2*16,outbuf + shv $v05,0, 2*16,outbuf2 + shv $v06,0, 3*16,outbuf + shv $v07,0, 3*16,outbuf2 + + addi cbbuf, 32 + addi crbuf, 32 + addi outbuf, 64 + addi outbuf2, 64 + + bgtz y, 1b + addi y, -2 + + # DMA output buffer + lh t1, %lo(RDRAM_OUTBUF_STRIDE) + lw s0, %lo(RDRAM_OUTBUF) + assert_ne s0, 0, ASSERT_INVALID_OUTPUT + add s0, outbuf_off + li s4, %lo(OUTBUF) + li t0, DMA_SIZE(BLOCK_W, BLOCK_H/2) + jal_and_j DMAOut, RSPQ_Loop + + #undef x0y0 + #undef ybuf_off + #undef cbuf_off + #undef outbuf_off + #undef y + + #undef ybuf + #undef cbbuf + #undef crbuf + #undef outbuf + #undef outbuf2 + .endfunc + diff --git a/src/video/rsph264.c b/src/video/rsph264.c new file mode 100644 index 000000000..d6ae51cf7 --- /dev/null +++ b/src/video/rsph264.c @@ -0,0 +1,513 @@ +/** + * @file rsph264.c + * @author Giovanni Bajo + * + * The H264 RSP code does not fit in IMEM, so it is split into two rspq + * overlays: rsph264_inter (motion compensation and inter residuals) and + * rsph264_intra (intra prediction and intra residuals). Both are full: 4040 + * and 4096 bytes out of 4096, with roughly 1.8 KB of shared code from + * rsph264_common.inc duplicated into each. Merging them is not an option, and + * rspq keeps only its own engine resident, so there is no way to have a third + * always-loaded section for the common part. + * + * An overlay switch is therefore expensive: rspq reloads the whole + * overlay-specific text and data segments and saves/restores the shared state, + * which is about 6.3 KB of DMA to enter intra and 7.3 KB to enter inter (the + * latter also pulls in the shared state as an extra segment, since it is the + * destination of rspq_overlay_share_state). At a few hundred macroblocks per + * frame, even a couple of switches per macroblock are enough to make the + * decoder memory bandwidth bound rather than compute bound, so the number of + * switches matters more than the cost of the tasks themselves. Both the + * switches and the resulting DMA traffic can be measured by building with + * RSPH264_PROFILE (see rsph264_internal.h). + * + * The command stream is thus arranged to stay on one overlay for as long as + * possible. A macroblock is served end to end by a single overlay, so a run of + * macroblocks of the same kind costs no switch at all, and only a transition + * between an inter and an intra macroblock does. Two commands make this + * possible: Task_SetPackedDeltaBuffer and Task_ProcessChromaResidual, which are + * assembled into both overlays from the same source and only touch state that + * the two share. The CPU sends them to whichever overlay the RSP already has + * loaded, tracked here by cur_ovl_id. + */ +#include "rsph264_internal.h" +#include "rspq.h" +#include "fastcache.h" +#include +#include +#if RSPH264_PROFILE +#include "emux.h" +#endif + +DEFINE_RSP_UCODE(rsph264_inter); +DEFINE_RSP_UCODE(rsph264_intra); +static uint32_t rsph264_inter_ovl_id = 0; +static uint32_t rsph264_intra_ovl_id = 0; + +enum { + // Tasks in rsph264_inter + TASK_OMX_DEQUANT_TRANSFORM_RESIDUAL = 0, + // Slot 1 was OMX_TransformDequantLumaDC; moved to the intra overlay + // because TransformDequantLumaDC needs full 32-bit signed math (~324 + // bytes of IMEM) that this overlay can't spare. The slot is held as + // an RSPQ_Loop placeholder so the remaining inter command IDs don't + // shift. + TASK_OMX_TRANSFORM_DEQUANT_CHROMADC = 2, + TASK_PROCESS_LUMA_INTER_RESIDUAL = 3, + TASK_PROCESS_CHROMA_RESIDUAL = 4, + TASK_OMX_INTERPOLATE_LUMA_OVERFILL = 5, + TASK_OMX_INTERPOLATE_CHROMA_OVERFILL = 6, + TASK_SET_WEIGHTS = 7, + + //TASK_SET_PACKED_DELTA_BUFFER = 15, +}; + +enum { + // Tasks in rsph264_intra + TASK_OMX_INTRAPREDICT_CHROMA_8 = 0, + TASK_PROCESS_LUMA_INTRA4_RESIDUAL = 1, + TASK_PROCESS_LUMA_INTRA16_RESIDUAL = 2, + // Same implementation as TASK_PROCESS_CHROMA_RESIDUAL in the inter + // overlay, assembled here as well so that an intra macroblock never has + // to leave this overlay. + TASK_PROCESS_CHROMA_RESIDUAL_INTRA = 3, + + TASK_SET_PACKED_DELTA_BUFFER = 15, +}; + +// Overlay that the RSP will have loaded when it reaches the point of the +// command stream we are currently writing. rspq saves and restores the overlay +// state around highpri preemptions, so this stays accurate even if another +// subsystem interrupts us between two H264 commands. It only goes stale if +// somebody else writes into the same lowpri queue, and in that case the worst +// that happens is one extra overlay switch. +static uint32_t cur_ovl_id = 0; + +#if RSPH264_PROFILE +static rsph264_stats_t stats; +#define STAT_INC(field) (stats.field++) +#else +#define STAT_INC(field) ((void)0) +#endif + +static inline uint32_t track_ovl(uint32_t ovl_id) +{ + if (ovl_id != cur_ovl_id) { + cur_ovl_id = ovl_id; + STAT_INC(ovl_switches); + STAT_INC(switches_to[ovl_id == rsph264_intra_ovl_id]); + } + STAT_INC(commands); + STAT_INC(cmds[ovl_id == rsph264_intra_ovl_id]); + return ovl_id; +} + +// Wrapper over rspq_write that keeps cur_ovl_id and the statistics up to date. +// rspq_write evaluates its ovl_id argument exactly once, so the side effects of +// track_ovl() happen once per command. +#define rsph264_write(ovl_id, cmd_id, ...) \ + rspq_write(track_ovl(ovl_id), cmd_id, ##__VA_ARGS__) + +static const uint8_t *last_packed_delta_buf = NULL; + +// Weightp coefficients currently configured in RSP DMEM. They survive across +// frames (the RSP keeps them in the overlay saved state), so this mirrors the +// initial content of WEIGHT_TAB in rsph264_common.inc. +static uint32_t last_weights[3] = { + RSPH264_WEIGHT_IDENTITY, RSPH264_WEIGHT_IDENTITY, RSPH264_WEIGHT_IDENTITY, +}; + +inline void rsph264_init(void) +{ + static bool initialized = false; + if (initialized) return; + initialized = true; + rspq_init(); + rsph264_inter_ovl_id = rspq_overlay_register(&rsph264_inter); + rsph264_intra_ovl_id = rspq_overlay_register(&rsph264_intra); + rspq_overlay_share_state(&rsph264_inter, &rsph264_intra); + cur_ovl_id = rsph264_intra_ovl_id; +} + +#if RSPH264_PROFILE +inline void rsph264_stats_reset(void) +{ + stats = (rsph264_stats_t){0}; +} + +inline void rsph264_stats_get(rsph264_stats_t *st) +{ + *st = stats; +} + +uint32_t rsph264_measure_overlay_switch_cost(void) +{ + // rsph264_queue_debug_load_overlay writes an RSPQ_Loop command, so a run of + // alternating ones does nothing but switch back and forth: apart from the + // 4 bytes of each command, all the DMA the RSP does in the meantime is the + // cost of the switches. The two directions are not symmetric (entering + // inter also pulls in the shared state, of which it is the owner), but + // switches necessarily alternate, so their average is what matters. + enum { N = 256 }; + + if (!(emux_detect(1) & EMUX_FEAT1_PROFILER)) + return 0; + + rsph264_queue_debug_load_overlay("inter"); + rspq_wait(); + + uint64_t dma0 = emux_prof_read(-1, EMUX_PROF_RAM_RSPDMA_BYTES); + for (int i = 0; i < N; i++) { + rsph264_queue_debug_load_overlay("intra"); + rsph264_queue_debug_load_overlay("inter"); + } + rspq_wait(); + uint64_t dma1 = emux_prof_read(-1, EMUX_PROF_RAM_RSPDMA_BYTES); + + rsph264_stats_reset(); + return (dma1 - dma0) / (2 * N); +} +#endif + +inline void rsph264_begin_frame(void) +{ + last_packed_delta_buf = NULL; +} + +inline void rsph264_sync(void) +{ + rspq_wait(); +} + +inline void rsph264_queue_debug_random_status(void) +{ + // TODO: implement +} + +inline void rsph264_queue_debug_load_overlay(const char *ovlname) +{ + // Slot 14 is an RSPQ_Loop placeholder in both overlays, so this is a no-op + // command whose only effect is the overlay switch it forces. + enum { TASK_NOP = 14 }; + + bool intra = !strcmp(ovlname, "intra"); + assertf(intra || !strcmp(ovlname, "inter"), "unknown overlay: %s", ovlname); + rsph264_write(intra ? rsph264_intra_ovl_id : rsph264_inter_ovl_id, TASK_NOP); +} + +inline void rsph264_queue_interpolate_luma_overfill( + int cache_flags, + const uint8_t *frame, uint32_t frame_pitch, + uint8_t *dst, uint32_t dst_pitch, + uint32_t frame_size, uint32_t block_size, uint32_t mv, uint32_t pos +) { + // Our RSP implementation assumes that dst is aligned to + // the block size, as it should be a macroblock partition. + int block_width = block_size >> 16; + // int block_height = block_size & 0xFFFF; + + assert(((uint32_t)dst & (block_width-1)) == 0); +#if 0 + assert((cache_flags & RSPH264_CACHE_SKIP_SOURCE) != 0); + + if (!(cache_flags & RSPH264_CACHE_SKIP_DEST)) { + for (int i=0;i> 16; + // int block_height = block_size & 0xFFFF; + + assert(((uint32_t)dst & (block_width-1)) == 0); +#if 0 + assert((cache_flags & RSPH264_CACHE_SKIP_SOURCE) != 0); + if (!(cache_flags & RSPH264_CACHE_SKIP_DEST)) { + for (int i=0;i> 16; + uint32_t frame_height = frame_size & 0xFFFF; + + assert(cache_flags == RSPH264_CACHE_SKIP_ALL); + + rsph264_write(rsph264_inter_ovl_id, TASK_OMX_INTERPOLATE_LUMA_OVERFILL, + PhysicalAddr(frame_luma), frame_pitch, + PhysicalAddr(dst_luma), dst_pitch, + frame_size, block_size, + mv, pos, + frame_width*frame_height, + PhysicalAddr(dst_chroma1), + PhysicalAddr(dst_chroma2)); +} diff --git a/src/video/rsph264_common.inc b/src/video/rsph264_common.inc new file mode 100644 index 000000000..a5e25bc92 --- /dev/null +++ b/src/video/rsph264_common.inc @@ -0,0 +1,1044 @@ +########################################################################## +# RSPH264 - Common functions +# +# Authors: Giovanni Bajo +########################################################################## +# +# This library contains the common code used by the RSPH264 overlays. +# +########################################################################## + +.macro SpinWait len + .set noat + li $1,\len +1: bnez $1,1b + addi $1,-1 + .set at +.endm + +# Asertion codes +#define ASSERT_DELTA_BUFFER_TOO_SMALL 0xAF00 + +################################################################################ +# DATA +################################################################################ + + .data +#if 0 +TQ_ADDR: .long 0 # Task queue address in RDRAM +TQ_SKIP: .long 0 # Skip task queue for a single command + + .align 3 +OMXDL_FUNC: .long 0 # OpenMAX command to execute +OMXDL_ARGS: .long 0,0,0,0,0,0,0,0,0,0,0 # space for 11 arguments +#endif + + .align 4 +BANNER0: .ascii "RSPH264 by Rasky" +BANNER1: .ascii "Mod: MB Decoder " +#RIDX_WSTAT: .byte 0x55, 0x56, 0x59, 0x5a, 0x65, 0x66, 0x69, 0x6a, 0x95, 0x96, 0x99, 0x9a, 0xa5, 0xa6, 0xa9, 0xaa + + #vsll_data # define constants for VSLL pseudo-opcode + #vsll8_data # define constants for VSLL8 pseudo-opcode + + .align 4 + # Coefficients of FIR filter used in Luma interpolation. + # Pre-shifted by 3 to help achieving the correct final precision + # (see the comments in the interpolation functions). +LUMA_FIR_COEFFS: + .half 1<<3 # [e8] = 1<<3 + .half -5<<3 # [e9] = -5<<3 + .half 20<<3 # [e10] = 20<<3 + .half 0 + .half 0 + .half 0x1 # [e13] = 1 + .half 0xFFFF # [e14] = -1 (for negating) + .half 0xFF # [e15] = 255 (for clamping) + + .align 4 +IQUANT_TABLE: + #define IQV 0 + .byte 10<>1", which means that the LSB is + # truncated away; if we simply multiply by 0.5, there is a risk that + # the LSB accumulates during the fixed point operations, and affects the result. + # To simulate a real ">>1", we need to mask out the LSB before + # multiplying by 0.5. + # + # Matrix multiplication on RSP + # **************************** + # + # Given that we have two rows in each vector register, + # we can manage to do the multiplication with two MACs + # sequences to calculate the matrix product. + # + # We use the RSP "scalar half" access to each + # register ("Xh" in SGI syntax, "element" 4-7 in opcode), + # so that we replicate each cell four times to multiply it + # in parallel with each column of coefficients (which is + # already duplicated in DMEM). For instance: + # + # ┌─ ─┐ + # v0[0h] = $v00,4 = │ a0 a0 a0 a0 a4 a4 a4 a4 │ + # └─ ─┘ + # x + # ┌─ ─┐ + # $v08 = │ 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 │ + # └─ ─┘ + # + # + + # + # ┌─ ─┐ + # v0[1h] = $v00,5 = │ a1 a1 a1 a1 a5 a5 a5 a5 │ + # └─ ─┘ + # X + # ┌─ ─┐ + # $v09 = │ 1.0 0.5 -0.5 1.0 1.0 0.5 -0.5 1.0 │ + # └─ ─┘ + # + # This computes exactly the partial result of the first two + # row of the resulting matrix (after multiplication with the + # first two rows of each column): + # + # ┌─ ─┐ + # │ a0+a1 a0+a1*.5 a0-a1*.5 a0+a1 │ + # │ a4+a5 a4+a5*.5 a4-a5*.5 a4+a5 │ + # │ ... ... ... ... │ + # │ ... ... ... ... │ + # └─ ─┘ + # + # By adding the following two MACs ($v00,6 * $v10 + $v00,7 * $v11), + # we complete the calculation of the first two rows of the result. + # + # We then repeat the same sequence on $v01 to calculate the second + # two rows. + # + # For the precision, we choose to use VMULF and to encode the coefficients + # in the range [-1, 1] in 2.14 fixed point (see IDCT_COEFF definition), and + # thus we need to pre-shift the input coefficients by 1 to achieve the + # correct result. + + vsll v_res_row01, v_res_row01, 1 + vand $v18, v_a_row1_05mask, v_res_row01,5 # Mask out LSB where will be multiplied by 0.5 + vand $v19, v_a_row3_05mask, v_res_row01,7 # Mask out LSB where will be multiplied by 0.5 + + vmulf $v04, v_a_row0, v_res_row01,4 + vmacf $v04, v_a_row1, $v18,0 + vmacf $v04, v_a_row2, v_res_row01,6 + vmacf $v04, v_a_row3, $v19,0 + + vsll v_res_row23, v_res_row23, 1 + vand $v18, v_a_row1_05mask, v_res_row23,5 # Mask out LSB where will be multiplied by 0.5 + vand $v19, v_a_row3_05mask, v_res_row23,7 # Mask out LSB where will be multiplied by 0.5 + + vmulf $v06, v_a_row0, v_res_row23,4 + vmacf $v06, v_a_row1, $v18,0 + vmacf $v06, v_a_row2, v_res_row23,6 + vmacf $v06, v_a_row3, $v19,0 + + # Matrix pre-multiplication on RSP + # ******************************** + # + # To calculate A' x (YxA), we use a somewhat similar approach. This time, + # the transposed transform matrix A' ($v12 & $v13) will use the RSP + # "scalar half" access, while the coefficients matrix (YxA, as resulted + # by previous operations in $v04/$v06) needs to be duplicated horizontally: + # + # ┌─ ─┐ + # v12[0h] = $v12,4 = │ 1.0 1.0 1.0 1.0 0.5 0.5 0.5 0.5 │ + # └─ ─┘ + # x + # ┌─ ─┐ + # vXX = │ a0 a1 a2 a3 a0 a1 a2 a3 │ + # └─ ─┘ + # + # Given that we currently have: + # + # ┌─ ─┐ + # v04 = │ a0 a1 a2 a3 a4 a5 a6 a7 │ + # └─ ─┘ + # ┌─ ─┐ + # v06 = │ b0 b1 b2 b3 b4 b5 b6 b7 │ + # └─ ─┘ + # + # How do we build all the 4 required vXX above, with each matrix row + # duplicated two times into each vector? We use a simple trick: first, + # we use a unaligned store to memory with SQV that rotates vectors to + # create a 4-position rotated versions of v04 and v06 in v05 and v07 + # respectively: + # + # ┌─ ─┐ + # v05 = │ a4 a5 a6 a7 a0 a1 a2 a3 │ + # └─ ─┘ + # ┌─ ─┐ + # v07 = │ b4 b5 b6 b7 b0 b1 b2 b3 │ + # └─ ─┘ + # + # And now, remembering that we are doing sequences of multiplication and + # adds vertically on each lane, we don't need to exactly build vXX: if you + # look at v04-v07, each column contains exactly the required terms, though + # not in the original order; it is thus sufficient to swap the terms of A' + # accordingly, so that each term gets multiplied to the right coefficient. + # + + # Create rotated vectors + sqv $v04,8, 0*16,t1 + sqv $v06,8, 1*16,t1 + lqv $v05,0, 0*16,t1 + lqv $v07,0, 1*16,t1 + + # In addition to IDCT, we also take the opportunity of this MAC + # sequence to add the predicted pixels (destination buffer) to do the + # final blending. We need to do this in the middle + # of the MAC sequence (rather than after it) because we + # want the last instruction to be a VMACU, the only one + # in the RSP instruction set that clamps to 0, so that + # we get that clamping step for free. + # Obviously we need to shift the pixel data to the required + # precision: since the pixel data was loaded with LUV as <<7, + # we need to shift it back >>7; we can't use the VSRL macro-opcode + # because it generates a VMUDM and we need a VMADM to be part + # of the MAC sequence; so we just call VMADM manually. + + vand $v18, $v04, v_at_05mask,6 # Mask out LSB on second half (will be * 0.5) + vand $v19, $v07, v_at_05mask,5 # Mask out LSB on first half (will be * 0.5) + + vmulf v_res_row01, $v18, v_at_row01,4 + vmacf v_res_row01, $v05, v_at_row01,5 + vmacf v_res_row01, $v06, v_at_row01,6 + vmadm v_res_row01, v_pix_row01, vshift8,14 # Add predicted pixel data (after >>7) + vmacu v_res_row01, $v19, v_at_row01,7 # Complete MAC & clamp to 0 + + vand $v18, $v05, v_at_05mask,5 # Mask out LSB on second half (will be * 0.5) + vand $v19, $v06, v_at_05mask,6 # Mask out LSB on first half (will be * 0.5) + + vmulf v_res_row23, $v04, v_at_row23,4 + vmacf v_res_row23, $v18, v_at_row23,5 + vmacf v_res_row23, $v19, v_at_row23,6 + vmadm v_res_row23, v_pix_row23, vshift8,14 # Add predicted pixel data (after >>7) + vmacu v_res_row23, $v07, v_at_row23,7 # Complete MAC & clamp to 0 + + # Clamp pixel values to 255 + # NOTE: we use vcl instead of vch because it doesn't change VCC (that we need for vmrg). + # vcl works almost by "chance", because VCO has been cleared by a previous vmrg. + # If it stops working, it's better to switch to vch, and reload VCC before the next vmrg. + vcl v_res_row01, v_res_row01, $v15,15 + vcl v_res_row23, v_res_row23, $v15,15 + + # Shift them back with <<7 + vsll v_res_row01, v_res_row01, 7 + vsll v_res_row23, v_res_row23, 7 + + # Merge the calculated values into the original register, + # preserving pixels that are not within our 4x4 area. + vmrg v_pix_row0, v_res_row01, v_pix_row0,0 + vmrg v_pix_row1, v_pix_row1, v_res_row01,0 + vmrg v_pix_row2, v_res_row23, v_pix_row2,0 + vmrg v_pix_row3, v_pix_row3, v_res_row23,0 + + # Write back + suv v_pix_row2,0, 0,s2 + suv v_pix_row3,0, 0,s1 + suv v_pix_row0,0, 0,s4 + jr ra + suv v_pix_row1,0, 0,s3 + .endfunc + + #undef v_res_row01 + #undef v_res_row23 + #undef v_a_row0 + #undef v_a_row1 + #undef v_a_row2 + #undef v_a_row3 + #undef v_a_row1_05mask + #undef v_a_row3_05mask + #undef v_at_row01 + #undef v_at_row23 + #undef v_at_05mask + #undef v_pix_row0 + #undef v_pix_row1 + #undef v_pix_row2 + #undef v_pix_row3 + #undef v_pix_row01 + #undef v_pix_row23 + + +#################################################################### +# +# RESIDUAL: SHARED SCAFFOLDING AND CHROMA RESIDUAL +# +# The chroma residual is the only per-macroblock step that both overlays +# need: an inter macroblock reaches it after motion compensation, an intra +# one after chroma intra-prediction. Assembling it here means that whichever +# overlay is loaded can process it, which is what keeps a macroblock from +# forcing an overlay switch halfway through (see rsph264.c). +# +#################################################################### + + #define src a0 + #define dst a1 + #define dst_pitch a2 + #define dc a3 + #define qp t9 + #define ac fp + #define tcmask s7 + #define npart s5 + #define dst_dma_width v1 + + # Fetch the destination buffer, and load arguments + # INPUT: + # t0: WxH length in DMA register format (eg: 0xF00F for 16x16) + # dst_pitch: must be pre-loaded with the RDRAM pitch + .func ResidualBootstrap +ResidualBootstrap: + lw qp, CMD_ADDR(16, 28) + lw ac, CMD_ADDR(20, 28) + li t2, DMA_IN # DMA direction: RDRAM->DMEM + # fallthrough! + .endfunc + + .func ResidualDma +ResidualDma: + # We need to fetch the destination buffer. It is guaranteed + # to be 4-byte aligned, but we need 8-byte alignment for DMA. + # The width has been already augmented to include possible unalignment. + move s0, dst + li s4, %lo(MB_OUTPUT) + j DMAExec + move t1, dst_pitch + .endfunc + + # Move back the destination buffer into RDRAM + # INPUT: + # t0: WxH length in DMA register format (eg: 0xF00F for 16x16) + # dst_pitch: must be pre-loaded with the RDRAM pitch + .func ResidualEnd +ResidualEnd: + jal ResidualDma + li t2, DMA_OUT # DMA direction: DMEM->RDRAM + + jal_and_j UpdateDeltaBlockPointer, RSPQ_Loop + .endfunc + +#################################################################### +# TransformResidual4x4CheckDC +# +# Calls TransformResidual4x4IDCT, but before processes DC in case +# it was set by the caller. +# +#################################################################### +# ARGS: +# s4: address of predicted pixel buffer in DMEM +# v00/v01: unpacked delta coefficients: 4x4 block of 16-bit values +# (calculated by UnpackNextDeltaBlock) + + .func TransformResidual4x4CheckDC +TransformResidual4x4CheckDC: + # If there's no fixed DC factor, just jump to the actual IDCT transformation. + bltz dc, TransformResidual4x4IDCT + move t4, dst_dma_width # buffer width is expected in t4 + + # Overwrite the first delta coefficient (v00[e8]) with DC, and then + # execute the transform. + j TransformResidual4x4IDCT + mtc2 dc, $v00,0 + .endfunc + +#################################################################### +# Task_ProcessChromaResidual +# +# Process the residual pixels for two 8x8 chroma blocks (Cr/Cb). +#################################################################### + .func Task_ProcessChromaResidual +Task_ProcessChromaResidual: + li dst_dma_width, 8 + jal ResidualBootstrap + li t0, DMA_SIZE(8,8) # first chroma buffer + + # Now load second chroma buffer, consecutive to the first one + lw s0, CMD_ADDR(24, 28) + jal DMAIn + li s4, %lo(MB_OUTPUT+64) + + move tcmask, ac + + jal CalcDequantFactors + move t0, qp + + #vxor $v00, $v00, $v00,0 + li s4, %lo(CHROMA_DC_0) + sqv $v00,0, 0,s4 + +ChromaDC0: + # Check if there are quantized/transformed DC factors for the + # first chroma block. If so, dequantize/transform them. + srl t0, tcmask, 25 + andi t0, 1 + beqz t0, ChromaDC1 + nop + jal UnpackNextDeltaBlock + nop + jal TransformDequantChromaDC + li s4, %lo(CHROMA_DC_0) + +ChromaDC1: + # Check if there are quantized/transformed DC factors for the + # second chroma block. If so, dequantize/transform them. + srl t0, tcmask, 26 + andi t0, 1 + beqz t0, ChromaResidualLoopStart + nop + jal UnpackNextDeltaBlock + nop + jal TransformDequantChromaDC + li s4, %lo(CHROMA_DC_1) + +ChromaResidualLoopStart: + # Main Chroma residual loop: go through each 4x4 partition + # (in a specific order, mandated by CHROMA_DEQUANT_INDEX), + # and dequantize/transform them. Use the previously-transformed + # DC coefficients if existing. + li s6, %lo(CHROMA_DC_0) + li npart, 0 +ChromaResidualLoop: + vxor $v00, $v00, $v00,0 + lhu dc, 0(s6) + vxor $v01, $v01, $v01,0 + addi s6, 2 + + # AC=0 DC=0 => do nothing + # AC=1 DC=0 => unpack, dequant, transform + # AC=0 DC=1 => transform + # AC=1 DC=1 => unpack, dequant, transform + + addi t1, npart, 16 + srlv t0, tcmask, t1 + andi ac, t0, 1 + beqz ac, ChromaResidualTransform + nop + + jal UnpackNextDeltaBlock + nop + jal Dequant4x4 + nop + +ChromaResidualTransform: + or t0, dc, ac + beqz t0, ChromaResidualSkip + lbu t0, %lo(CHROMA_DEQUANT_INDEX)(npart) + + jal TransformResidual4x4CheckDC + addi s4, t0, %lo(MB_OUTPUT) + +ChromaResidualSkip: + bne npart, 7, ChromaResidualLoop + addi npart, 1 + + # DMA back second chroma buffer + lw s0, CMD_ADDR(24, 28) + li t0, DMA_SIZE(8,8) + li s4, %lo(MB_OUTPUT+64) + move t1, dst_pitch + jal_and_j DMAOut, ResidualEnd + + .endfunc + + +#################################################################### +# TransformDequantChromaDC +# +# Reconstruct a 2x2 Chroma DC block from a packed block of coefficients. +# Do a 2x2 inverse Hadamard transformation, followed by a quantization +# step. +# +# This is exposed as OMX_TransformDequantChromaDC (inter overlay only), +# and used internally by Task_ProcessChromaResidual. +#################################################################### +# +# ARGS: +# v00: unpacked coefficients (as generated by UnpackNextDeltaBlock). +# Notice that only the first 4 lanes are used (2x2 block). +# s4: output buffer where the 4 coefficients should be written to. + + .func TransformDequantChromaDC +TransformDequantChromaDC: + vsll $v00, $v00, 1 + + li t0, %lo(IHT_DC_COEFF) + lpv $v04,0, 0*8,t0 + lpv $v05,0, 1*8,t0 + lpv $v06,0, 2*8,t0 + lpv $v07,0, 3*8,t0 + lqv $v08,0, 3*16,t0 + vmulf $v02, $v04, $v00,8 + vmacf $v02, $v05, $v00,9 + vmacf $v02, $v06, $v00,10 + vmacf $v00, $v07, $v00,11 + + # Dequantize using single multiplication factor + vmudn $v00, $v00, $v27,8 + + # Use shift factor, reduced by 1 (so just shift right afterwards) + vmudn $v00, $v00, $v29,8 + vsra $v00, $v00, 1 + + # Mask out lanes 4-7. When this function is called for the + # first 2x2 block, we want to write 0 in the space for the + # second block (because the second block might not exist, + # and zero is the default). + # When called on the second block, only 4 lanes are written + # because the address is unaligned, so the value of those lanes + # doesn't really matter. + vand $v00, $v00, $v08,0 + jr ra + sqv $v00,0, 0,s4 + .endfunc + + #undef src + #undef dst + #undef dst_pitch + #undef dc + #undef qp + #undef ac + #undef tcmask + #undef npart + #undef dst_dma_width + +#if 0 +TEXT_OVERLAY_START: +#endif diff --git a/src/video/rsph264_inter.S b/src/video/rsph264_inter.S new file mode 100644 index 000000000..e6afbffc2 --- /dev/null +++ b/src/video/rsph264_inter.S @@ -0,0 +1,1623 @@ +############################################################ +# RSPH264 - Inter-prediction functions +# +# Authors: Giovanni Bajo +############################################################ +#include + + .data + + RSPQ_BeginOverlayHeader + RSPQ_DefineCommand OMX_DequantTransformResidual, 28 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand OMX_TransformDequantChromaDC, 12 + RSPQ_DefineCommand Task_ProcessLumaInterResidual, 28 + RSPQ_DefineCommand Task_ProcessChromaResidual, 28 + RSPQ_DefineCommand OMX_Interpolate_Luma_Overfill, 44 + RSPQ_DefineCommand OMX_Interpolate_Chroma_Overfill, 44 + RSPQ_DefineCommand Task_SetWeights, 16 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand Task_SetPackedDeltaBuffer, 4 + RSPQ_EndOverlayHeader + +#if 0 + # NOTE: this must be at the start of the .data overlay section + # so that it concatenates to the main table. +DECLARE_OMXDL_TABLE( + 0, // FIXME: remove + 0, // FIXME: remove + OMX_DequantTransformResidual - _start, + OMX_TransformDequantLumaDC - _start, + OMX_TransformDequantChromaDC - _start, + Task_ProcessLumaInterResidual - _start, + Task_ProcessChromaResidual - _start, + OMX_Interpolate_Luma_Overfill - _start, + OMX_Interpolate_Chroma_Overfill - _start +) +#endif + +#include "rsph264_common.inc" + + .data + + .align 4 + .ascii "OVL: Inter-pred " + + .align 4 +InterLumaCoordConstants: + .half 1 # +1/-1 constant for range adjustment and src offset calculation + .half 0xFFF8 # ~7: align dma width + .half 0 + .half 7 # +7: align dma width + + .half 1 # +1/-1 constant for range adjustment and src offset calculation + .half 0xFFFF # ~0: align dma height + .half 0 + .half 0 # +0: align dma height (also used as 0 elsewhere) + +InterLumaMVConst: + .half 3 # Motion vector: isolate fractional part (2 bits) + .half 2 # overread before x + .half 3 # overread after x + .half 0 + + .half 0x4000 # Motion vector SRA constant (vmudm 0x4000 = SRA 2) + .half 2 # overread before y + .half 3 # overread after y + .half 0 + +InterChromaMVConst: + .half 7 # Motion vector: isolate fractional part (3 bits) + .half 1 # overread before x + .half 1 # overread after x + .half 0 # must stay 0 (used as "overread before x" for dst buffer) + + .half 0x2000 # Motion vector SRA constant (vmudm 0x2000 = SRA 3) + .half 1 # overread before y + .half 1 # overread after y + .half 0 + +Interpolate5_AverageYes: + .half 0x80 # shift pixel <<7 before average + .half 0x1 # average pixel multiplier + .half 0x80 # rounding (0.5) +Interpolate5_AverageNo: + .half 0x100 # shift pixel <<8 to store with lpv + .half 0x0 # average pixel multiplier (= discard average) + .half 0x0 # rounding + + + # FIXME ************************** +OMXDL_ARGS: + + .bss + + # Macroblock input buffer. A macroblock is max 16x16, + # but we need to read more lines/pixels because of + # over-reading by interpolation and because of DMA + # alignment constraints, so space for 32x21 pixels + # is required. + .align 8 # 4 is sufficient, but 8 visually helps debugging +MB_INPUT: .dcb.b 32*21 + + # Maximum macroblock output. We fill this in without + # over-reading (and RDRAM buffer is always aligned), + # so we don't need more than 16x16 + .align 8 # 4 is sufficient, but 8 visually helps debugging +MB_OUTPUT: .dcb.b 16*16 + + # Scratch memory for intermediate computations + .align 8 # 4 is sufficient, but 8 visually helps debugging +MB_SCRATCH: .dcb.w 16*21 + + # Address of the WEIGHT_TAB entry of the chroma plane being processed. + # The two chroma components are interpolated by the same code, which + # is re-entered after the first one, so the plane must be tracked here. + .align 2 +WEIGHT_CUR: .dcb.w 1 + + + .text + +#################################################################### +# +# INTERPREDICTION: CHROMA & LUMA INTERPOLATION +# +#################################################################### + + # Registers with a fixed meaning across all the chroma/luma + # interpolation functions. + #define src a0 // Source memory buffer (in RDRAM) + #define src_pitch a1 // Source pitch (in RDRAM) + #define dst a2 // Dest memory buffer (in RDRAM) + #define dst_pitch a3 // Dest pitch (in RDRAM) + #define width v0 // Width of macroblock (real pixels) + #define height v1 // Height of macroblock (real pixels) + #define dx k0 // Fractional horizontal motion vector + #define dy k1 // Fractional vertical motion vector + #define unalign_first_line s5 // Offset of first pixel on first DMA line (from MB_INPUT) + #define src_unalign t4 // Pointer to first real macroblock pixel within MB_INPUT + #define dst_unalign t5 // Pointer to first real macroblock pixel within MB_OUTPUT + #define src_dma_width t6 // Width/pitch of source macroblock in DMEM + #define dst_dma_width t7 // Width/pitch of destination macroblock in DMEM + #define src_dma_height t8 // Height of the source macroblock in DMEM + #define dst_dma_height t9 // Height of the destination macroblock in DMEM + + #define v_mv_const $v18 // constants used for bootstrapping + #define v_frame_size $v19 // frame size ([e8]=width, [e12]=height) + #define v_block_size $v20 // block size ([e8]=width, [e12]=height) + #define v_mv $v21 // fractional motion vector ([e8]=X, [e12]=Y) + #define v_overread $v22 // overread pixels ([e9/10]=before/after_x, [e13/14]=before/after_y) + #define v_const $v23 // constants used for bootstrapping + #define v_pos $v24 // macroblock position within frame ([e8]=X, [e12]=Y) + #define v_dma_size $v25 + + # Local registers + # + # t0-t3: used as fully scratch + # s0-s3: usually used as pointers to DMEM buffers. + # + # Sometimes used as arguments to function: + # + # s0: mostly used as argument to indicate the address of + # the input macroblock to process in a function. + # s4: mostly used as argument to indicate the address of + # the output macroblock to process in a function. + + +#################################################################### +# Interpolate_Bootstrap +# +# Bootstrap function for interpolation (luma or chroma) +#################################################################### +# +# ARGS: +# t0: either %lo(InterLumaMVConst) or %lo(InterChromaMVConst) +# + + .func Interpolate_Bootstrap +Interpolate_Bootstrap: + vxor v_mv, v_mv, v_mv,0 + vxor v_block_size, v_block_size, v_block_size,0 + + # Load constants used in initial calculations + lqv v_mv_const,0, 0*16,t0 + li t1, %lo(InterLumaCoordConstants) + lqv v_const,0, 0*16,t1 + + # Load input coordinates + la t0, CMD_ADDR(0, 44) # FIXME: Valid for overfill commands ONLY + lsv v_mv,0, 12*2,t0 + lsv v_mv,8, 13*2,t0 + lsv v_pos,0, 14*2,t0 + lsv v_pos,8, 15*2,t0 + lsv v_frame_size,0, 8*2,t0 + lsv v_frame_size,8, 9*2,t0 + lsv v_block_size,0, 10*2,t0 + lsv v_block_size,8, 11*2,t0 + + # isolate integer part of motion vector. This is exactly like + # a "vsra" operator, where the shift factor is provided by the + # caller in v_mv_const + vmudm $v00, v_mv, v_mv_const,12 + vaddc v_pos, v_pos, $v00,0 + + # Set overread constants + vor v_mv, v_mv, v_mv,4 # repeat motion vectors (0-3, 4-7) + vand v_mv, v_mv, v_mv_const,8 # isolate fractional parts + vne $v00, v_mv, v_const,15 # compare to zero + vmrg v_overread, v_mv_const, v_const,15 + + cfc2 fp, COP2_CTRL_VCC # fp != 0 if at least 1 fractional MV + mfc2 dx, v_mv,0 # extract fractional X + mfc2 dy, v_mv,8 # extract fractional Y + + # Clamp against [-W-ORAX+1, SW+ORBX-1] / [-H-ORAY+1, SH+ORBY-1], which + # are the largest values that will cause a DMA that contains at least + # 1 pixel of the reference image. Because of overfilling, coordinates + # outside of these ranges produce identical results than the clamped ones. + vsubc $v02, v_const, v_block_size,0 # $v02 = 1-W, 1-H + vsubc $v03, v_frame_size, v_const,8 # $v03 = SW-1, SH-1 + vsubc $v02, $v02, v_overread,6 # $v02 -= ORAX/ORAY + vaddc $v03, $v03, v_overread,5 # $v03 += ORBX/ORBY + vge v_pos, v_pos, $v02,0 + vlt v_pos, v_pos, $v03,0 + + # Now see if there's overfill (that is, if at least one required pixel + # is outside of the reference image). + + # Calculate the corners of the macro-block, starting from the center, + # so: + # $v00 = left/top corner (v_pos - overread) + # $v01 = bottom/right corner (v_pos + overread + block_size) + vsubc $v02, v_pos, v_overread,5 # before + vaddc $v03, v_pos, v_overread,6 # after + + vaddc $v01, $v03, v_block_size,0 + vsubc $v01, $v01, v_const,8 + + vge $v00, $v02, v_const,15 + cfc2 s6, COP2_CTRL_VCC # bit 0/4: overfill left/top + vlt $v01, $v01, v_frame_size,0 + cfc2 s7, COP2_CTRL_VCC # bit 0/4: overfill right/bottom + + # Calculate SRC += (Y-OVER_READ_Y_BEFORE)*SRC_PITCH + (X-OVER_READ_X_BEFORE). + # We use a vector instruction to do the multiplication, and we need to fetch + # the 32-bit result and add it to src. + mtc2 src_pitch, $v04,8 + vmudn $v04, $v04, $v02,0 # $v02 is v_pos - v_overread[before] + vmadn $v04, v_const, $v02,8 # += (v_pos_x-v_overred_before_x)*1 + mfc2 t0, $v04,8 + vsar $v05, $v05, $v05,9 + mfc2 t1, $v05,8 + and t0, 0xffff + sll t1, 16 + add src, t0 + add src, t1 + + # Now src points to the (unaligned) address of the first pixel to + # read (including overread). dst points to the (unaligned) address + # of destination. + + # Compute all DMA sizes and alignments. These are the formulas + # we implement: + # + # src_unalign = src & 7 + # dst_unalign = dst & 7 + # + # dst_width = round8(block_width + dst_unalign) + # dst_height = block_height + # + # src_width = round8(block_width + src_unalign + overread_before_x + overread_after_x) + # src_height = block_height + overread_before_y + overread_after_y + # + # src_unalign += overread_before_x + src_dma_width*overread_before_y + # + # FIXME: src_unalign is a misnomer for "offset of the first pixel within + # MB_INPUT" (same for dst_unalign). Rename. + # + + # $v00: + # <1>: src & 7 (RDRAM address unalignment) + vxor $v00, $v00, $v00,0 + and t0, src, 7 + and dst_unalign, dst, 7 + mtc2 t0, $v00,2 + + # v_dma_size + # <0>: width + # <4>: height + vor v_dma_size, v_block_size, v_block_size,4 + vaddc v_dma_size, v_dma_size, v_const,7 # add 7 (only on width) + vaddc $v01, v_dma_size, $v00,5 # + src_unalign (only x) + vaddc $v01, $v01, v_overread,5 # + overread before (x/y) + vaddc $v01, $v01, v_overread,6 # + overread after (x/y) + vand $v01, $v01, v_const,5 # and ~7 (only on width) + vand v_dma_size, v_dma_size, v_const,5 # and ~7 (only on width) + + mfc2 dst_dma_height, v_dma_size,8 + mfc2 dst_dma_width, v_dma_size,0 + mfc2 src_dma_height, $v01,8 + mfc2 src_dma_width, $v01,0 + mfc2 width, v_block_size,0 + mfc2 height, v_block_size,8 + + # Calculate src_unalign += overread_before_x + src_dma_width*overread_before_y + vaddc $v00, $v00, v_overread,9 + mfc2 unalign_first_line, $v00,2 + vmudn $v02, $v01, v_overread,13 + vaddc $v00, $v00, $v02,0 + mfc2 src_unalign, $v00,2 + + # Prepare the output buffer merge mask. + # When writing to the output buffer, we must not override + # bytes outside the requested size. For instance, we might + # have DMA'd in 8 bytes per row, but the partition is only + # 4 bytes, so we need not to touch the last 4 bytes in each + # row. Prepare a mask in VCC that will be used by VMRG. + # + # Width Mask + # 2 11111100 + # 4 11110000 + # 8 00000000 + # 16 00000000 + li t0, 0xFF + sllv t0, t0, width + ctc2 t0, COP2_CTRL_VCC + +Interpolate_Bootstrap_Phase2: + move ra2, ra + + # DMA into DMEM the input macroblock (source pixels) + sll t0, src_dma_height, 12 + or t0, src_dma_width + addi t0, -0x1001 + li s4, %lo(MB_INPUT) + move s0, src + jal DMAInAsync + move t1, src_pitch + + # DMA into DMEM the output macroblock (destination pixels) + jal Interpolate_ChromaLuma_DMADest + li t2, DMA_IN_ASYNC + + # We now have one transfer in progress (input macroblock) + # and another one queued (output macroblock). Check the overfill + # conditions. If we need access to the source pixels for overfill, + # we will call DMAWaitReady to wait the end of the first transfer only + # (source pixel) and keep the second one running in background. + +OverfillStart: + # Handle overfilling over just-read source buffer. + # + # Overfilling is required when accessing a macroblock on the boundary + # of the screen: in case the motion vector requires interpolating it, + # we will access pixels outside of the screen, and those must be + # simulated by extending the boundary pixel (eg: a pixel at coordinate + # -3,15 is the same of 0,15). + + # Check overfill left + andi t0, s6, 1 + bnez t0, OverfillCheckRight + mfc2 t0, v_pos,0 + + jal DMAWaitReady # We'll need the pixel soon, wait for source transfer to finish. + + # Let s0 point to pixel on coordinate 0. This is always + # the first pixel of the screen, so it's always 8-byte aligned. + sub t0, unalign_first_line, t0 + addi s0, t0, %lo(MB_INPUT) + + # What we need to do is to copy the first pixel to the + # N previous 8-byte words that were DMA'd, that is up to s4. + # There is at least 1 8-byte word to overfill (because X < 0). + # There are at most 3 8-byte words to overfill, in case the x + # coordinate was -15 and there were 3 over_read_x pixels. + addi t1, t0, -8 # t1 > 0 if there are 2 words to overfill + addi t2, t1, -8 # t2 > 0 if there are 3 words to overfill + + # Loop two rows at a time + addi t3, src_dma_height, -2 + vxor $v00, $v00, $v00,0 + SpinWait 2 # a little bit more waiting on the output pixels +OverfillLeftLoop: + lpv $v01,0, 0*8,s0 # First row (value in $v01[e8]) + add s1, s0, src_dma_width + lpv $v02,0, 0*8,s1 # Second row (value in $v02[e8]) + + vor $v01, $v00, $v01,8 + vor $v02, $v00, $v02,8 + + spv $v01,0, -1*8,s0 + blez t1, OverfillLeftLoopEnd + spv $v02,0, -1*8,s1 + + spv $v01,0, -2*8,s0 + blez t2, OverfillLeftLoopEnd + spv $v02,0, -2*8,s1 + + spv $v01,0, -3*8,s0 + spv $v02,0, -3*8,s1 + +OverfillLeftLoopEnd: + add s0, s1, src_dma_width + bgtz t3, OverfillLeftLoop + addi t3, -2 + j OverfillCheckTop + nop + +OverfillCheckRight: + andi t0, s7, 1 + bnez t0, OverfillCheckTop + mfc2 t0, v_pos,0 + mfc2 t1, v_frame_size,0 + + jal DMAWaitReady # We'll need the pixel soon, wait for source transfer to finish. + + # Let s0 point to pixel on coordinate width (so 1 after the last + # pixel). Notice that the last pixel of the screen is + # always at byte 7 within a 8-byte aligned word. + sub t0, unalign_first_line, t0 + add t0, t1 + addi s0, t0, %lo(MB_INPUT) + + # What we need to do is to copy the last pixel to the + # N following 8-byte words that were DMA'd. + # See OverfillLeft for more info. + sub t0, src_dma_width, t0 + addi t1, t0, -8 # t1 > 0 if there are 2 words to overfill + addi t2, t1, -8 # t2 > 0 if there are 3 words to overfill + + # Now make s0 point to the last word (so that last pixel of the + # screen is in [e15]) + addi s0, -8 + + # Loop two rows at a time +OverfillRightStart: + addi t3, src_dma_height, -2 + vxor $v00, $v00, $v00,0 + SpinWait 1 # a little bit more waiting on the output pixels +OverfillRightLoop: + lpv $v01,0, 0*8,s0 # First row (value in $v01[e8]) + add s1, s0, src_dma_width + lpv $v02,0, 0*8,s1 # Second row (value in $v02[e8]) + + vor $v01, $v00, $v01,15 + vor $v02, $v00, $v02,15 + + spv $v01,0, 1*8,s0 + blez t1, OverfillRightLoopEnd + spv $v02,0, 1*8,s1 + + spv $v01,0, 2*8,s0 + blez t2, OverfillRightLoopEnd + spv $v02,0, 2*8,s1 + + spv $v01,0, 3*8,s0 + spv $v02,0, 3*8,s1 + +OverfillRightLoopEnd: + add s0, s1, src_dma_width + bgtz t3, OverfillRightLoop + addi t3, -2 + # fallthrough + +OverfillCheckTop: + # Check overfill top + andi t0, s6, 0x10 + bnez t0, OverfillCheckBottom + mfc2 t1, v_pos,8 + mfc2 t0, v_overread,10 + sub t1, t0 + + # Calculate pointer to first actual line. We want to do + # MB_INPUT + src_dma_width*t0, but it's complicated because we + # don't have multiplications + li s1, %lo(MB_INPUT) + neg t0, t1 +OverfillTopCalcRow: + add t1, 1 + bnez t1, OverfillTopCalcRow + add s1, src_dma_width + + jal DMAWaitReady # We'll need the pixel soon, wait for source transfer to finish. + sub s0, s1, src_dma_width + j OverfillVertical + neg s2, src_dma_width # step + +OverfillCheckBottom: + # Check overfill top + andi t0, s7, 0x10 + bnez t0, OverfillEnd + mfc2 t0, v_pos,8 + mfc2 t1, v_frame_size,8 + mfc2 t2, v_overread,10 + + sub t1, t0 + add t1, t2 + sub t0, src_dma_height, t1 + + li s0, %lo(MB_INPUT) +OverfillBottomCalcRow: + addi t1, -1 + bnez t1, OverfillBottomCalcRow + add s0, src_dma_width + + sub s1, s0, src_dma_width + jal DMAWaitReady # We'll need the pixel soon, wait for source transfer to finish. + move s2, src_dma_width # step + + # Vertical overfilling loop + # (shared between top/bottom) +OverfillVertical: + addi t1, src_dma_width, -8 + addi t2, t1, -8 + addi t3, t2, -8 + + SpinWait 3 # required after DMAWaitReady as a further wait loop, see documentation + lpv $v00,0, 0*8,s1 + lpv $v01,0, 1*8,s1 + lpv $v02,0, 2*8,s1 + lpv $v03,0, 3*8,s1 + + li s4, %lo(MB_INPUT) +OverfillTop: + blez t1, OverfillTopCheckEnd + spv $v00,0, 0*8,s0 + blez t2, OverfillTopCheckEnd + spv $v01,0, 1*8,s0 + blez t3, OverfillTopCheckEnd + spv $v02,0, 2*8,s0 + spv $v03,0, 3*8,s0 +OverfillTopCheckEnd: + addi t0, -1 + bnez t0, OverfillTop + add s0, s2 + +OverfillEnd: + jr ra2 + nop + .endfunc + + + #undef unalign_first_line + +#################################################################### +# Interpolate_ChromaLuma_DMADest +# +# Run DMA transfer between RDRAM "dst" and DMEM buffer (MB_OUTPUT). +# +# Input: +# t2: direction: 0=fetch (RDRAM->DMEM), 1=store (DMEM->RDRAM) +#################################################################### + + .func Interpolate_ChromaLuma_DMADest +Interpolate_ChromaLuma_DMADest: + sll t0, dst_dma_height, 12 + or t0, dst_dma_width + addi t0, -0x1001 + li s4, %lo(MB_OUTPUT) + move s0, dst + j DMAExec + move t1, dst_pitch + .endfunc + + +#################################################################### +# ApplyWeight +# +# Apply explicit weighted prediction (weightp) in place to the +# predicted macroblock in MB_OUTPUT, just before it is transferred +# back to RDRAM. +# +# For each predicted pixel P, the standard requires: +# +# CLIP1( ((W*P + (D ? 1<<(D-1) : 0)) >> D) + O ) +# +# where W is the weight, O the offset and D the log2 denominator. All +# three are constant over a slice for a given reference index, so the +# CPU pre-folds D into the two coefficients we read from WEIGHT_TAB +# (see rsph264_weight_pack): +# +# KW = W << (8-D) KO = O << 7 +# +# Pixels are loaded with luv, so a pixel is P<<7 within its lane, and +# vmulu accumulates (VS*VT + 0x4000)>>15, that is: +# +# ((P<<7) * (W << (8-D)) + (1<<14)) >> 15 = (P*W + (1<<(D-1))) >> D +# +# so the rounding built into vmulu is exactly the rounding required by +# the standard (for D=0 the rounding term falls below the precision of +# the result, as required). The offset is then accumulated as +# KO * 256 * 2 = O<<16, that is +O at the precision of the result. +# +# The offset must be accumulated *before* reading the accumulator, +# because the unsigned read clamps to zero: the standard clamps after +# adding the offset, not before. The read also takes care of clamping +# at the lower bound, while the upper bound (255) is applied by vcl. +# +# vcl only modifies VCCH, so the partition width mask that the +# bootstrap left in VCCL survives and can be used to merge back the +# pixels which are outside of the partition. +# +#################################################################### +# ARGS: +# s2: address of the WEIGHT_TAB entry of the plane being processed + + .func ApplyWeight +ApplyWeight: + lw t0, 0(s2) + li t1, LUMA_FIR_COEFFS + lui t2, 0x0100 # identity transform (W=1, O=0, D=0) + beq t0, t2, ApplyWeightEnd # nothing to do + lqv $v28,0, 0,t1 # $v28[e15] = 255 (for clamping) + + llv $v26,0, 0,s2 # $v26[e8] = KW, $v26[e9] = KO + vxor $v27, $v27, $v27,0 + addi s0, dst_unalign, %lo(MB_OUTPUT) + addi t3, width, -8 # t3 > 0 if a second 8-pixel column is needed + + # vmacu reads the offset lane-wise, so broadcast it to all lanes. + vor $v27, $v27, $v26,9 + + # vcl expects no pending carries. All interpolation loops end with + # a vmrg, which clears them, but do not depend on that. + ctc2 zero, COP2_CTRL_VCO + +ApplyWeightLoopCol: + move t0, height + move s1, s0 + + ############################################################################ + # VU OPCODES SU OPCODES # + ############################################################################ + + .align 3 +ApplyWeightLoop: + luv $v00,0, 0,s1 + add s3, s1, dst_dma_width + luv $v01,0, 0,s3 + vmulu $v10, $v00, $v26,8; addi t0, -2 + vmacu $v10, $v27, vshift8,15 + vmulu $v11, $v01, $v26,8 + vmacu $v11, $v27, vshift8,15 + + # Clamp at 255 (clamping at 0 was performed by the reads above) + vcl $v10, $v10, $v28,15 + vcl $v11, $v11, $v28,15 + + # Back to the <<7 precision expected by suv, then merge with the + # original pixels to preserve those outside of the partition. + vsll $v10, $v10, 7 + vsll $v11, $v11, 7 + vmrg $v10, $v00, $v10,0 + vmrg $v11, $v01, $v11,0 + suv $v10,0, 0,s1 + add s1, s3, dst_dma_width + bgtz t0, ApplyWeightLoop + suv $v11,0, 0,s3 + + # Next column (if any) + addi s0, 8 + bgtz t3, ApplyWeightLoopCol + addi t3, -8 + +ApplyWeightEnd: + jr ra + nop + .endfunc + + +#################################################################### +# Task_SetWeights +# +# Configure the weightp coefficients used by ApplyWeight for the three +# planes. They are packed by the CPU (see rsph264_weight_pack) and are +# valid until the next call, so this command is normally issued once +# per frame. +#################################################################### +# ARGS: +# 0: unused +# 1: LUMA: packed luma coefficients +# 2: CB: packed Cb coefficients +# 3: CR: packed Cr coefficients + + .func Task_SetWeights +Task_SetWeights: + sw a1, %lo(WEIGHT_TAB)+0 + sw a2, %lo(WEIGHT_TAB)+4 + j RSPQ_Loop + sw a3, %lo(WEIGHT_TAB)+8 + .endfunc + + +############################################################ +# Interpolate_CopyMacroblock +# +# "Copy" 8 horizontal bytes from source macroblock (s0) to +# dest macroblock (s4), for the number of lines in "height". +# +# src/dst pitches are read from src_dma_width / src_dma_height +# respectively. +# +# The "copy" is actually a merge respecting the current merge +# mask in VCC (so if the macroblock width is actually less +# then 8 bytes, extra destination bytes are preserved). +# +# For a 16x16 macroblock, we just call this function twice. +# +############################################################# + .func Interpolate_CopyMacroblock +Interpolate_CopyMacroblock: + + move ra2, ra + jal DMAWaitIdle + addi t0, height, -2 + move ra, ra2 + + .align 3 +Interpolate_CopyLoop: + lpv $v00,0, 0*8,s0 ;add s0,src_dma_width + lpv $v01,0, 0*8,s4 ;add s1,s4,dst_dma_width + lpv $v08,0, 0*8,s0 ;add s0,src_dma_width + lpv $v09,0, 0*8,s1 + vmrg $v00, $v01, $v00,0 + vmrg $v08, $v09, $v08,0 + spv $v00,0, 0*8,s4 ;add s4,s1,dst_dma_width + spv $v08,0, 0*8,s1; ;bgtz t0,Interpolate_CopyLoop + addi t0, -2 + + jr ra + nop + .endfunc + + +#################################################################### +# OMX_Interpolate_Chroma_Overfill +# +# Interpolation of chroma subpixels during interprediction, with +# overfillling +#################################################################### +# ARGS: +# 0: SRC: RDRAM address of the full source frame +# 1: SRC_PITCH: source buffer pitch (in bytes) +# 2: DST: RDRAM address of destination buffer +# 3: DST_PITCH: destination buffer pitch (in bytes) +# 4: FRAME_SIZE: +# 0-15: frame width +# 16-31: frame height +# 4: BLOCK_SIZE: +# 0-15: block width +# 16-31: block height +# 5: MV: +# 0-15: motion vector X (fixed 14.2) +# 16-31: motion vector Y (fixed 14.2) +# 6: POS: +# 0-15: source block pos X +# 16-31: source block pos Y +# 7: SRC2: RDRAM address of second full frame (second chroma component). +# 8: DST2: RDRAM address of destination buffer of second chroma component. +# + + .func OMX_Interpolate_Chroma_Overfill +OMX_Interpolate_Chroma_Overfill: + li t1, %lo(WEIGHT_TAB)+4 # first component is Cb + sw t1, %lo(WEIGHT_CUR) + jal Interpolate_Bootstrap + li t0, %lo(InterChromaMVConst) + +ChromaPostBootstrap: + addi s0, src_unalign, %lo(MB_INPUT) + jal Interpolate_Chroma + addi s4, dst_unalign, %lo(MB_OUTPUT) + + jal ApplyWeight + lw s2, %lo(WEIGHT_CUR) + + jal Interpolate_ChromaLuma_DMADest + li t2, DMA_OUT_ASYNC # Direction: DMEM->RDRAM, background + + # Check if we need to do the second chroma component as well. + lw t0, CMD_ADDR(32, 44) + sw zero, CMD_ADDR(32, 44) + beqz t0, RSPQ_Loop + + # Calculate updated src/dst RDRAM pointers, by applying the same offset + # that was previously calculated for the first component. + add src, t0 + + lw t1, CMD_ADDR(36, 44) + lw t0, CMD_ADDR(40, 44) + sub t1, dst, t1 + add dst, t0, t1 + + li t2, %lo(WEIGHT_TAB)+8 # second component is Cr + sw t2, %lo(WEIGHT_CUR) + + # Run only the phase 2 of bootstrap (dma input buffer + overfill), then + # jump back to the beginning of this function. + jal_and_j Interpolate_Bootstrap_Phase2, ChromaPostBootstrap + .endfunc + + + .func Interpolate_Chroma +Interpolate_Chroma: + # DX==DY==0: no interpolation: just copy the input macroblock over + # the output macroblock (respecting the destination mask + # to avoid overwriting too many bytes). + beqz fp, Interpolate_CopyMacroblock + nop + + # Compute the filter coefficients and store them in $v28 + mtc2 dx, $v29,1*2 + mtc2 dx, $v29,3*2 + mtc2 dy, $v27,2*2 + mtc2 dy, $v27,3*2 + li t0,8 + sub t1,t0,dx + sub t2,t0,dy + mtc2 t1, $v29,0*2 + mtc2 t1, $v29,2*2 + mtc2 t2, $v27,0*2 + mtc2 t2, $v27,1*2 + + # Interpolation loop. We don't need to loop horizontally + # because we process 8 pixels at a time, and 8x8 is the + # maximum size of a chroma macroblock (after 4:2:0 subsampling). + # + # Since interpolation applies a 4-tap filter on a 2x2 pixel block, + # we process 2 lines each loop, so that each line is loaded in + # memory only once. Before the loop, we load the first line once, + # and then we will load 2 lines per loop, to achieve the maximum + # parallelism in the loop between VU/SU. + + # The original formula for each pixel is: + # ((VAL * COEFF) + 1<<5) >> 6 + # + # Notice that we will need the pixel value to by left-shifted by 7 to + # match the bits expected by luv/suv for memory transfers, so actually + # what we want is: + # ((VAL * COEFF) + 1<<5) >> 6 << 7 + # ((VAL * COEFF) + 1<<5) << 1 + # ((VAL * COEFF) << 1) + (1<<6) + # + # Remember that we've pre-shifted VAL by 7, COEFF by 10, + # and we've also prepared a register that contains 1<<11. + # + # vmadm will calculate accumulated sums of (A*B)>>16. So this will become: + # ((VAL << 7) * (COEFF << 10) + (1<<11)*(1<<11)) >> 16 + # (((VAL * COEFF) << 17) + (1<<22)) >> 16 + # ((VAL * COEFF) << 1) + (1<<6) + # which is exactly the final result. + + ############################################################################ + # VU OPCODES SU OPCODES # + ############################################################################ + + # Wait for DMA transfer + # and prepare for iteration + move ra2, ra + jal DMAWaitIdle + move t0, height + + # Calculate in $v28 the coefficients + # for the interpolation: + # [0] = (8-dx) * (8-dy) = [e8] + # [1] = dx * (8-dy) = [e9] + # [2] = (8-dx) * dy = [e10] # Load first row as VAL<<7 + # [3] = dx * dy = [e11] # in $v00 / $v01 + vmudh $v28, $v29, $v27,0; luv $v00,0, 0,s0 + + # Zero $v29 + vxor $v29, $v29, $v29,0; add s1,s0,1 + + # Shift coefficients left by 4. See + # below for rationale + vsll8 $v28, $v28, 10; luv $v01,0, 0,s1 + + # vshift8[e12] contains 1<<11 (it's + # the shift vector). Create $v29 + # that contains that value + # in all lanes. + vor $v29, $v29, vshift8,12; add s0,src_dma_width + + .align 3 # loops must be 64-bit aligned for full pairing +ChromaInterpolate: + # Calculate interpolation on + # first and second row # Load second row in $v02/$v03 + vmudm $v11, $v00, $v28,8; luv $v02,0, 0,s0 + vmadm $v11, $v01, $v28,9; add s1, s0, 1 + vmadm $v11, $v29, $v29,0; luv $v03,0, 0,s1 + vmadm $v11, $v02, $v28,10; add s0, src_dma_width + # Load third row in $v00/$v01 + # (will be first row in the next loop) + vmadm $v11, $v03, $v28,11; luv $v00,0, 0,s0 + + # Calculate interpolation on + # second and third row + vmudm $v13, $v02, $v28,8; add s1,s0,1 + vmadm $v13, $v03, $v28,9; luv $v01,0, 0,s1 + # Read the current pixel in the + # two output rows in $v12/$v14 + vmadm $v13, $v29, $v29,0; luv $v12,0, 0,s4 + vmadm $v13, $v00, $v28,10; add s3, s4, dst_dma_width + vmadm $v13, $v01, $v28,11; luv $v14,0, 0,s3 + + # Merge the interpolation output to the + # current output pixels (using the + # precalculated mask in VCC) so that we + # preserve existing pixels outside of + # the macroblock. # Prepare for next loop + vmrg $v11, $v12, $v11,0; add s0, src_dma_width + vmrg $v13, $v14, $v13,0; addi t0, -2 + + # Write output pixels + suv $v11,0, 0,s4 + suv $v13,0, 0,s3 + bnez t0, ChromaInterpolate + add s4, s3, dst_dma_width + + jr ra2 + nop + + .endfunc + + +#################################################################### +# OMX_Interpolate_Luma_Overfill +# +# Interpolation of luma subpixels during interprediction, with +# overfillling +#################################################################### +# ARGS: +# 0: SRC: RDRAM address of the full source frame +# 1: SRC_PITCH: source buffer pitch (in bytes) +# 2: DST: RDRAM address of destination buffer +# 3: DST_PITCH: destination buffer pitch (in bytes) +# 4: FRAME_SIZE: +# 0-15: frame width +# 16-31: frame height +# 4: BLOCK_SIZE: +# 0-15: block width +# 16-31: block height +# 5: MV: +# 0-15: motion vector X (fixed 14.2) +# 16-31: motion vector Y (fixed 14.2) +# 6: POS: +# 0-15: source block pos X +# 0-16: source block pos Y + + .func OMX_Interpolate_Luma_Overfill +OMX_Interpolate_Luma_Overfill: + jal Interpolate_Bootstrap + li t0, %lo(InterLumaMVConst) + + .equ srchalfver, s6 + .equ srchalfhor, s7 +LumaPostBootstrap: + li t0,3 +LumaSetupHalfVer: + bne dx, t0, LumaSetupHalfHor + addi srchalfver, src_unalign, %lo(MB_INPUT) + addi srchalfver, 1 + +LumaSetupHalfHor: + bne dy, t0, LumaSwitch + addi srchalfhor, src_unalign, %lo(MB_INPUT) + add srchalfhor, src_dma_width + +LumaSwitch: + addi s0, src_unalign, %lo(MB_INPUT) + + beqz fp, LumaCopy # dx==0 && dy == 0 -> copy + nop + beqz dy, LumaDy0 # dy==0 => halfhor + average + nop + beqz dx, LumaDx0 # dx==0 => halver + average + li t0, 2 + beq dx, t0, LumaDiag + nop + beq dy, t0, LumaDiag + nop + +LumaQuarter: + # Interpolate half-hor into output, + # then interpolate half-ver and average with output + addi s2, srchalfhor, -2 + addi s3, dst_unalign, %lo(MB_OUTPUT) + li s5, 1 + li t0, %lo(Interpolate5_AverageNo) + jal_and_j Interpolate5, LumaInterpolateHalfVerAvgOutput + +LumaDy0: + # DX==1 || DX==3 => Average(dst, halfver) => dst + andi t0, dx, 1 + beqz t0, LumaInterpolateHalfHor + li t0, %lo(Interpolate5_AverageNo) + + move s4, srchalfver + move s0, src_dma_width + j LumaInterpolateHalfHor + li t0, %lo(Interpolate5_AverageYes) + +LumaDx0: + # DY==1 || DY==3 => Average(dst, halfhor) => dst + andi t0, dy, 1 + beqz t0, LumaInterpolateHalfVer + li t0, %lo(Interpolate5_AverageNo) + + move s4, srchalfhor + move s0, src_dma_width + j LumaInterpolateHalfVer + li t0, %lo(Interpolate5_AverageYes) + +LumaDiag: + jal LumaInterpolateDiag + addi s4, dst_unalign, %lo(MB_OUTPUT) + + # DX == 1 || DX == 3 + andi t0,dx,1 + bnez t0,LumaInterpolateHalfVerAvgOutput + + # DY == 1 || DY == 3 + andi t0,dy,1 + beqz t0,LumaEnd + # fallthrough! + +LumaInterpolateHalfHorAvgOutput: + # Interpolate half-hor buffer and then average with output buffer + addi s4, dst_unalign, %lo(MB_OUTPUT) + move s0, dst_dma_width + li t0, %lo(Interpolate5_AverageYes) + +LumaInterpolateHalfHor: + # Interpolate half-hor buffer. Average yes/no must be already + # configured in t0. + addi s2, srchalfhor, -2 + addi s3, dst_unalign, %lo(MB_OUTPUT) + li s5, 1 + jal_and_j Interpolate5, LumaEnd + +LumaInterpolateHalfVerAvgOutput: + # Interpolate half-ver buffer and then average with output buffer + addi s4, dst_unalign, %lo(MB_OUTPUT) + move s0, dst_dma_width + li t0, %lo(Interpolate5_AverageYes) + +LumaInterpolateHalfVer: + # Interpolate half-ver buffer. Average yes/no must be already + # configured in t0. + addi s3, dst_unalign, %lo(MB_OUTPUT) + sub s2, srchalfver, src_dma_width + sub s2, src_dma_width + move s5, src_dma_width + jal_and_j Interpolate5, LumaEnd + + +LumaCopy: + addi s0, src_unalign, %lo(MB_INPUT) + addi s4, dst_unalign, %lo(MB_OUTPUT) + + # Copy macroblock (up to 8x16) + addi s2, s0, 8 + jal Interpolate_CopyMacroblock + addi s3, s4, 8 + + # If this is a 16x16 block, do a second copy (second 8x16 column) + andi t0, width, 16 + beqz t0, LumaEnd + move s0, s2 + move s4, s3 + jal_and_j Interpolate_CopyMacroblock, LumaEnd + +LumaEnd: + jal ApplyWeight + li s2, %lo(WEIGHT_TAB) + + jal Interpolate_ChromaLuma_DMADest + li t2, DMA_OUT_ASYNC # Direction: DMEM->RDRAM + + # Check if we also need to interpolate the chroma buffers + lw t0, CMD_ADDR(32, 44) + lw src, CMD_ADDR(0, 44) + beqz t0, RSPQ_Loop + + # t0 is chroma_src-luma_src + # + # We need to update pointers and scale all coordinates/sizes + # by two because of chroma subsampling. + # Adjust src/dst and pitches for chroma + srl src_pitch, 1 + srl dst_pitch, 1 + add src, t0 + lw dst, CMD_ADDR(36, 44) + + # Scale back pointer offsets in preparation for the second chroma buffer + # They will be used as offsets between chroma buffers, so they + # must be four times smaller. + srl t0, 2 + sw t0, CMD_ADDR(32, 44) + + # Scale frame size, block size, and position by 2. + # We don't need to scale the motion vector. + lw t0, CMD_ADDR(16, 44) # frame size + lw t1, CMD_ADDR(20, 44) # block size + lw t2, CMD_ADDR(28, 44) # position + srl t0, 1 + srl t1, 1 + srl t2, 1 + sw t0, CMD_ADDR(16, 44) + sw t1, CMD_ADDR(20, 44) + j OMX_Interpolate_Chroma_Overfill + sw t2, CMD_ADDR(28, 44) + + .endfunc + + +#################################################################### +# 5-tap interpolation of a macroblock, with optional average with +# another block. +# +# INPUT: +# s2: input buffer (sizes: width / height / src_dma_width) +# s3: output buffer (sizes: width / height / dst_dma_width) +# s5: horizontal step between samples in input buffer to average +# (it will be 1 for horizontal interpolation, or src_dma_width for +# vertical interpolation). +# t0: either %lo(Interpolate5_AverageYes) or %lo(Interpolate5_AverageNo) +# to enable/disable average with another buffer. +# s4: average buffer (sizes: width / height / s0) +# +# This function can be used to perform both horizontal and vertical +# interpolation, depending on the value of s5. Moreover, it can +# simultaneously average the result with a third buffer. This is useful +# because, depending on the input parameters, Luma interpolation is often +# made of both an interpolation and averaging step. +#################################################################### + + .func Interpolate5 +Interpolate5: + # Load average coefficients + lqv $v29,0, 0,t0 + + # Read coefficients, pre-shifted by 4 + li t0, LUMA_FIR_COEFFS + lqv $v28,0, 0,t0 + + move ra2, ra + jal DMAWaitIdle # wait for input pixels to arrive + addi t1, width, -8 + move ra, ra2 + + # Since we process 8 pixels at a time, we either loop one column or two + # (depending on macroblock width). +Interpolate5LoopCol: + move t0, height + move t2, s2 + sub t3, s3, dst_dma_width # Subtract one because we will add one on each loop before using it + move sp, s4 + + ############################################################################ + # VU OPCODES SU OPCODES # + ############################################################################ + + .align 3 +Interpolate5Loop: + # 5-tap FIR filter, with coefficients in $v28, using + # MACs with signed coefficients and unsigned clamping. + # + # This sequence does many things that we expect: + # * Signed multiplications (coefficients can be negative) + # * Clamping in [0,0x7FFF]. We need to clamp [0,0xFF], so + # half of it is already taken care of. + # * Correct last-bit rounding to the nearest (assuming the + # values are correctly pre-shifted) + # + # The result is shifted by 15, so we get the correct precision: + # (VAL<<7)*(COEFF<<3)>>15 => (VAL*COFF)>>5 + # + + # Load pixels into $v00/$v05 + luv $v00,0, 0,t2 + add s1, t2, s5 + luv $v01,0, 0,s1 + vmulu $v10, $v00, $v28,8; add s1, s5 + luv $v02,0, 0,s1 + vmacu $v11, $v01, $v28,9; add s1, s5 + luv $v03,0, 0,s1 + vmacu $v12, $v02, $v28,10; add s1, s5 + luv $v04,0, 0,s1 + vmacu $v13, $v03, $v28,10; add s1, s5 + luv $v05,0, 0,s1 + # Load average buffer + vmacu $v14, $v04, $v28,9; luv $v19,0, 0,sp + # Prepare for next loop + vmacu $v15, $v05, $v28,8; add t3,dst_dma_width + # + # + + # Clamp against 255. + vcl $v17, $v15, $v28,15; add t2,src_dma_width + # (latency) + # (latency) + + # Average calculation. The general formula for average is simply + # ((P + A) + 1) >> 1 + # + # where P is our interpolated pixel, and A is the pixel coming from + # the average buffer. Notice that we want the final value to be + # left-shifted by 8 to be able to store it with spv. So the actual + # formula becomes: + # (P << 7) + (A << 7) + (1 << 7) + # + # A has been already loaded in $v19 with luv, so it's already <<7. + # We thus need to do: + # ($v17 << 7) + $v19 + 0x80 + # + # Remember that we simulate <<7 with a multiplication; so to do a MAC + # sequence, we also multiply the $v19 by 1, so that it becomes: + # ($v17 * 0x80) + ($v19 * 0x1) + 0x80 + # + # Since the caller might have requested to disable average calculation + # (sometimes it's not required), we can keep the same code sequence with + # different coefficients: + # ($v17 * 0x100) + ($v19 * 0x0) + 0x0 + # + # $v29 has been already loaded with either Interpolate5_AverageYes or + # Interpolate5_AverageNo, which contains either set of coefficients + # depending on which calculation we need to perform. + + # shift to <<8 (no average) or <<7 + # (average). Add average if any. # Load output pixel + vmudn $v18, $v17, $v29,8; lpv $v20,0, 0*8,t3 + vmadn $v18, $v19, $v29,9; add sp, s0 + # + # Add rounding after average (if any). + vaddc $v18, $v18, $v29,10; addi t0, -1 + + # OK we're done! Merge it to the previous + # output (using the precalculated mask in + # VCC) and write it back. + vmrg $v18, $v20, $v18,0; bgtz t0, Interpolate5Loop + # (latency) + # # Store final value + spv $v18,0, 0,t3 + + # Next column (if any) + addi s2, 8 + addi s3, 8 + addi s4, 8 + bgtz t1, Interpolate5LoopCol + addi t1, -8 + + jr ra + nop + .endfunc + +#################################################################### +# LumaInterpolateDiag +# +# Interpolate half-pels diagonally +# INPUT: +# s0: input buffer +# s4: output buffer +#################################################################### + .func LumaInterpolateDiag +LumaInterpolateDiag: + li t0, LUMA_FIR_COEFFS + lqv $v29,0, 0,t0 + + ######################################## + # 1. horizontal interpolation + ######################################## + + # MB_SCRATCH will store the 16 bpp intermediate results, so we + # we need twice as many bytes. Store that doubled dest width in t2. + li s3, %lo(MB_SCRATCH) + sll t3, dst_dma_width, 1 + + # Shift coefficients by 5; they were already pre-shifted by + # 3, so now we get COEFF<<8. + vsll $v28, $v29, 5 + + # We need to start two lines earlier than where the actual + # image start. These lines were over-read so they're available. + sub s0, src_dma_width + sub s0, src_dma_width + move ra2, ra + jal DMAWaitIdle # wait for input pixel to finish arriving + addi t0, width, -8 + addi t2, src_dma_width, -5 + +LumaDiagLoopCol: + # Start one line before, because the loop will be + # pre-incrementing the output buffer (for instruction + # pairing reasons). + sub s3, t3 + + # Start two pixels before on each row (they were over-read) + addi s2, s0, -2 + + # Iteration count + addi t1, height, 5 + + .align 3 +LumaDiagLoop: + # Compute the 5-tap filter but store the full intermediate results + # without clamping nor rounding. + # + # ((VAL<<7) * (COEFF<<8)) >> 15 = VAL*COEFF (unshifted) + # + # We use vmulf/vmacf because they clamp at [0x8000,0x7FFF] (we never + # hit that); also, the rounding is ignored because it affects a bit + # lower than the actual precision, which is always zero. + + luv $v00,0, 0,s2 + addi s2,1 + vmulf $v10, $v00, $v28,8; luv $v01,0, 0,s2 + addi s2,1 + vmacf $v11, $v01, $v28,9; luv $v02,0, 0,s2 + addi s2,1 + vmacf $v12, $v02, $v28,10; luv $v03,0, 0,s2 + addi s2,1 + vmacf $v13, $v03, $v28,10; luv $v04,0, 0,s2 + addi s2,1 + vmacf $v14, $v04, $v28,9; luv $v05,0, 0,s2 + add s2, t2 # jump to next row (input) + vmacf $v15, $v05, $v28,8; add s3, t3 # jump to next row (output) + addi t1, -1 + bnez t1, LumaDiagLoop + # Store the full 16-bit result + sqv $v15,0, 0,s3 + + + # See if we need to iterate on the second column (16x16 macroblock) + addi s0, 8 + li s3, %lo(MB_SCRATCH)+16 + bgtz t0, LumaDiagLoopCol + addi t0, -8 + + ######################################## + # 2. vertical interpolation + ######################################## + + # Shift coeffs by 2 to obtain COEFF<<5 + vsll $v28, $v29, 2 + + li s2, %lo(MB_SCRATCH) + + addi t0, width, -8 +LumaDiagLoop2Col: + # Start one line before because the loop will pre-increment + # s3 for pairing purposes. + sub s3, s4, dst_dma_width + move t1, height + + move s1, s2 + lqv $v00,0, 0,s1 + add s1, t3 + lqv $v01,0, 0,s1 + add s1, t3 + lqv $v02,0, 0,s1 + add s1, t3 + lqv $v03,0, 0,s1 + add s1, t3 + lqv $v04,0, 0,s1 + add s1, t3 + lqv $v05,0, 0,s1 + add s2, t3 + .align 3 +LumaDiagLoop2: + # 5-tap filter: + # (VAL16 * (COEFF<<5)) >> 15 = (VAL16*COEFF)>>10 + # + # We also benefit from unsigned clamping and rounding here + # (see LumaInterpolateHalfHor for more information). + vmulu $v10, $v00, $v28,8; move s1, s2 + vmacu $v11, $v01, $v28,9; lqv $v00,0, 0,s1 + vmacu $v12, $v02, $v28,10; add s1, t3 + vmacu $v13, $v03, $v28,10; lqv $v01,0, 0,s1 + vmacu $v14, $v04, $v28,9; add s1, t3 + vmacu $v15, $v05, $v28,8; lqv $v02,0, 0,s1 + add s1, t3 + lqv $v03,0, 0,s1 + add s1, t3 + + # Clamp at 255 (clamping at + # 0 already performed). # Load output pixel + vcl $v17, $v15, $v29,15; add s3, dst_dma_width + lpv $v19,0, 0*8,s3 + + # Continue loading next row + lqv $v04,0, 0,s1 + vsll8 $v18, $v17, 8; add s1, t3 + lqv $v05,0, 0,s1 + add s2, t3 + + # Merge with output pixel + vmrg $v18, $v19, $v18,0; addi t1, -1 + + bnez t1, LumaDiagLoop2 + spv $v18,0, 0,s3; + + + addi s4, 8 + li s2, %lo(MB_SCRATCH)+16 + bgtz t0, LumaDiagLoop2Col + addi t0, -8 + + jr ra2 + nop + + .endfunc + +#################################################################### + + #undef src + #undef src_pitch + #undef dst + #undef dst_pitch + #undef width + #undef height + #undef dx + #undef dy + #undef over_read_x + #undef over_read_y + #undef src_unalign + #undef dst_unalign + #undef src_dma_width + #undef dst_dma_width + #undef src_dma_height + #undef dst_dma_height + +#################################################################### +#################################################################### + +#################################################################### +# +# RESIDUAL: CHROMA & LUMA DEQUANTIZATION AND IDCT +# +#################################################################### + + #define src a0 + #define dst a1 + #define dst_pitch a2 + #define dc a3 + #define qp t9 // NOTE: this must match definition in rsph264_common.inc + #define ac fp + #define tcmask s7 + + #define npart s5 + #define dst_dma_width v1 + + # NOTE: ResidualBootstrap, ResidualDma, ResidualEnd, + # TransformResidual4x4CheckDC, Task_ProcessChromaResidual and + # TransformDequantChromaDC live in rsph264_common.inc, because the intra + # overlay needs them too. They use the register names defined just above, + # which are kept in sync there. + +#################################################################### +# OMX_DequantTransformResidual +# +# Reconstruct 4x4 residual block, dequantize it, do iDCT and +# add it to the predicted data (motion compensation or intra prediction). +#################################################################### +# ARGS: +# 0: SRC: RDRAM address of coefficient buffer +# 1: DST: RDRAM address of predicted (destination) buffer +# 2: DST_PITCH: destination buffer pitch (in bytes) +# 3: DC: DC coeffient (or -1 if it doesn't exist) +# 4: QP: quantization parameter: +# Bit 0-7: matrix to use +# Bit 8-15: shift factor +# 5: AC: flag to indicate if non-zero AC coefficients exists + + .func OMX_DequantTransformResidual +OMX_DequantTransformResidual: + # Process a 4x4 block, but fetch a 8x4 block in case there's + # unalignment of the destination buffer (it is guaranteed to + # be 4-byte aligned, but not 8-byte). + li dst_dma_width, 8 + jal ResidualBootstrap + li t0, DMA_SIZE(8,4) + + #vxor $v00, $v00, $v00,0 + beqz ac, TransformOnly + vxor $v01, $v01, $v01,0 + + jal UnpackNextDeltaBlock + nop + move tcmask, ac + + jal CalcDequantFactors + move t0, qp + jal Dequant4x4 + nop + +TransformOnly: + jal TransformResidual4x4CheckDC + nop + + # DMA back into RDRAM + j ResidualEnd + li t0, DMA_SIZE(8,4) + .endfunc + +#################################################################### +# OMX_TransformDequantLumaDC: removed (dead code, see header table). +# TransformDequantLumaDC is no longer assembled into this overlay. +#################################################################### + +#define dcdmasize t8 +#define dcfunc t9 + +#################################################################### +# OMX_TransformDequantChromaDC +# +# Reconstruct the 2x2 Chroma DC block from packed delta buffer. Save +# it within the internal CHROMA_DC structure, and DMA back into RDRAM. +# +# a0: +# a1: dst +# a2: qp +#################################################################### + .func OMX_TransformDequantChromaDC +OMX_TransformDequantChromaDC: + li s4, %lo(CHROMA_DC_0) + li dcdmasize, DMA_SIZE(8,1) + li dcfunc, %lo(TransformDequantChromaDC) + # fallthrough! + .endfunc + + +#################################################################### +# TransformDequantAnyDC +# +# Common code for DC dequantization/transform for both Luma and Chroma +#################################################################### + + .func TransformDequantAnyDC +TransformDequantAnyDC: + jal CalcDequantFactors + move t0, a2 # qp + + jal UnpackNextDeltaBlock + nop + + jalr dcfunc + move t1, a2 # qp + + # DMA back the buffer + move t0, dcdmasize + move s0, a1 # dst + jal_and_j DMAOut, RSPQ_Loop + .endfunc + + + +#################################################################### +# Task_ProcessLumaInterResidual +# +# Reconstruct residual luma 16x16 block, dequantize it, do iDCT and +# add it to the predicted data (motion compensation or intra prediction). +#################################################################### + + .func Task_ProcessLumaInterResidual +Task_ProcessLumaInterResidual: + # Process a 16x16 block, that is guaranteed to be aligned to + # 8 byte. So we don't need to handle unalignment: always fetch + # exactly 16x16. + li dst_dma_width, 16 + jal ResidualBootstrap + li t0, DMA_SIZE(16,16) + + jal CalcDequantFactors + move t0, qp + + # Go through 16 4x4 partitions of the 16x16 block + li npart, 0 +LumaResidualLoop: + # See if there's residual data to process in this 4x4 partition + srlv t0, ac, npart + andi t0, 1 + beqz t0, LumaDequantSkip + nop + + jal UnpackNextDeltaBlock + nop + jal Dequant4x4 + nop + + # Load the offset within the 16x16 block of the current partition. + lbu t0, %lo(LUMA_DEQUANT_INDEX)(npart) + jal TransformResidual4x4CheckDC + addi s4, t0, %lo(MB_OUTPUT) + +LumaDequantSkip: + bne npart, 15, LumaResidualLoop + addi npart, 1 + + # DMA back into RDRAM + j ResidualEnd + li t0, DMA_SIZE(16,16) + + .endfunc + +#################################################################### + + #undef src + #undef dst + #undef dst_pitch + #undef dc + #undef qp + #undef ac + #undef tcmask + #undef npart + #undef dst_dma_width + #undef ac16 diff --git a/src/video/rsph264_internal.h b/src/video/rsph264_internal.h new file mode 100644 index 000000000..726169c69 --- /dev/null +++ b/src/video/rsph264_internal.h @@ -0,0 +1,244 @@ +/** + * @file rsph264_internal.h + * @author Giovanni Bajo + */ +/* + * RSPH264 - low-level interface library to RSP ucode + * + * This library is used internally by the H264 player + * to initialize and communicate with the RSP ucode. It + * is not meant for high-level usage. + * + */ + +#ifndef RSPH264_H +#define RSPH264_H + +#include +#include + +/*********************************************************** + * Main API. + ***********************************************************/ + +// Initialize RSPH264 library (call it once) +void rsph264_init(void); + +// Start working on a frame. This function uploads the ucode +// to RSP and starts RSP processing. +void rsph264_begin_frame(void); + +// Sync RSP: wait for currently queued functions to terminate. +// This can be used to force sync points in which all previously enqueued +// tasks have been performed, but obviously wastes CPU cycles. +// It should be used only for debugging purposes. +void rsph264_sync(void); + + +/*********************************************************** + * Profiling. + * + * Set RSPH264_PROFILE to 1 to count the commands and the overlay switches + * generated by this module, and to log them per frame together with the RSP + * DMA traffic measured through the emux profiler (see h264.c). + * + * This is off by default because the per-frame logging drains the RSP pipeline + * at every frame boundary, to attribute the traffic to the frame that caused + * it, and so perturbs the very timings it measures. + ***********************************************************/ + +#ifndef RSPH264_PROFILE +#define RSPH264_PROFILE 0 +#endif + +#if RSPH264_PROFILE + +// Statistics on the RSP command stream produced by this module. +// +// These count what the CPU wrote into the rspq queue, not what the RSP +// actually did: overlay switches caused by other subsystems preempting us +// through the highpri queue are not visible here. +// +// Overlay switches are the dominant source of RSP DMA traffic in the H264 +// decoder (each one reloads a few KiB of IMEM and DMEM), so this counter is +// the main knob to watch when optimizing memory bandwidth. +// Index into the per-overlay arrays below. +#define RSPH264_STAT_INTER 0 +#define RSPH264_STAT_INTRA 1 + +typedef struct { + uint32_t commands; // Commands written into the queue + uint32_t ovl_switches; // Commands that forced an overlay switch + // Same two counters split per overlay: cmds[i] counts the commands sent to + // overlay i, switches_to[i] the switches that made it the current one. + // Together they say how the command stream is partitioned, which is what + // tells whether reordering the macroblocks could merge more runs. + uint32_t cmds[2]; + uint32_t switches_to[2]; +} rsph264_stats_t; + +void rsph264_stats_reset(void); +void rsph264_stats_get(rsph264_stats_t *stats); + +// Average DMA bytes that one overlay switch costs, measured at runtime by +// issuing no-op commands that do nothing but force switches back and forth. +// Needs an emux-capable emulator; returns 0 elsewhere. +uint32_t rsph264_measure_overlay_switch_cost(void); + +#endif + + +/*********************************************************** + * Task queueing API. + * + * These functions enqueue a new task for execution by RSP. + * The task will be performed in background as soon as the RSP + * finishes previously enqueued tasks. + * + * Tasks are strictly executed in enqueued order, as they might + * depend one on each other. + ***********************************************************/ + +// Cache flag: Do not force cache writeback of source buffer +#define RSPH264_CACHE_SKIP_SOURCE (1<<0) + +// Cache flag: Do not force cache invalidation of destination buffer +#define RSPH264_CACHE_SKIP_DEST (1<<1) + +// Cache flag: Do not force cache writeback/invalidate at all +#define RSPH264_CACHE_SKIP_ALL (RSPH264_CACHE_SKIP_SOURCE | RSPH264_CACHE_SKIP_DEST) + + +void rsph264_queue_interpolate_luma( + int cache_flags, + const uint8_t *src, uint32_t src_pitch, + uint8_t *dst, uint32_t dst_pitch, + uint32_t width, uint32_t height, + uint32_t dx, uint32_t dy); + +void rsph264_queue_interpolate_chroma( + int cache_flags, + const uint8_t *src, uint32_t src_pitch, + uint8_t *dst, uint32_t dst_pitch, + uint32_t width, uint32_t height, + uint32_t dx, uint32_t dy); + +void rsph264_queue_interpolate_luma_overfill( + int cache_flags, + const uint8_t *frame, uint32_t frame_pitch, + uint8_t *dst, uint32_t dst_pitch, + uint32_t frame_size, + uint32_t block_size, + uint32_t mv, uint32_t pos); + +void rsph264_queue_interpolate_chroma_overfill( + int cache_flags, + const uint8_t *frame, uint32_t frame_pitch, + uint8_t *dst, uint32_t dst_pitch, + uint32_t frame_size, + uint32_t block_size, + uint32_t mv, uint32_t pos); + +void rsph264_queue_interpolate_all_overfill( + int cache_flags, + const uint8_t *frame, uint32_t frame_pitch, + uint8_t *dst_luma, uint8_t *dst_chroma_1, uint8_t *dst_chroma_2, uint32_t dst_pitch, + uint32_t frame_size, + uint32_t block_size, + uint32_t mv, uint32_t pos); + +void rsph264_queue_intrapred_chroma_8x8( + int cache_flags, + const uint8_t *src_l, const uint8_t *src_u, const uint8_t *src_ul, + uint8_t *dst, uint32_t left_pitch, uint32_t dst_pitch, + uint32_t mode, uint32_t availability); + +// Packed weightp coefficients of a plane whose prediction must be left +// untouched (weight 1, offset 0, denominator 0). The RSP detects this value +// and skips the weighting pass altogether. +#define RSPH264_WEIGHT_IDENTITY 0x01000000 + +// Pack the explicit weighted prediction coefficients of one plane into the +// format expected by rsph264_queue_set_weights_if_changed(). The denominator +// is folded into the weight, and the offset is pre-shifted to the precision +// used by the RSP (see ApplyWeight in rsph264_inter.S). +static inline uint32_t rsph264_weight_pack(int weight, int offset, int denom) +{ + // The bitstream can only code weights in [-128,127], but the default weight + // used when a reference has no explicit weights is 1<= -128 && weight <= 128); + assert(offset >= -128 && offset <= 127); + assert(denom >= 0 && denom <= 7); + + if (weight == (1 << denom) && offset == 0) + return RSPH264_WEIGHT_IDENTITY; + + uint32_t kw = (uint32_t)(weight * (1 << (8 - denom))) & 0xFFFF; + uint32_t ko = (uint32_t)(offset * 128) & 0xFFFF; + return (kw << 16) | ko; +} + +// Configure the weightp coefficients used by subsequent interpolation tasks, +// for the three planes. The coefficients must be packed with +// rsph264_weight_pack(). The command is sent to the RSP only when the +// coefficients actually change, so this can be called for every partition. +void rsph264_queue_set_weights_if_changed( + uint32_t luma, uint32_t cb, uint32_t cr); + +void rsph264_queue_set_packed_delta_buffer( + int cache_flags, + const uint8_t *src); + +void rsph264_queue_set_packed_delta_buffer_if_changed( + int cache_flags, + const uint8_t *src); + +void rsph264_queue_reset_packed_delta_buffer(void); + +const uint8_t* rsph264_cur_delta_buffer(const uint8_t *buf); + +void rsph264_queue_dequant_transform_residual( + int cache_flags, + uint8_t *dst, uint32_t dst_pitch, + const int16_t *dc, uint32_t qp, uint32_t ac); + +void rsph264_queue_transform_dequant_chromadc( + int cache_flags, + int16_t *dst, uint32_t qp); + +void rsph264_queue_process_luma_inter_residual( + int cache_flags, + uint8_t *dst, uint32_t dst_pitch, + const int16_t *dc, uint32_t qp, uint32_t totalCoeffMask); + +void rsph264_queue_process_chroma_residual( + int cache_flags, + uint8_t *dst1, uint8_t *dst2, uint32_t dst_pitch, + uint32_t qp, uint32_t totalCoeffMask); + +void rsph264_queue_process_luma_intra4_residual( + int cache_flags, + const uint8_t *src, uint8_t *dst, + uint32_t src_pitch, uint32_t dst_pitch, + const uint8_t *modeAvail, + uint32_t qp, uint32_t totalCoeffMask); + +void rsph264_queue_process_luma_intra16_residual( + int cache_flags, + const uint8_t *src, uint8_t *dst, + uint32_t src_pitch, uint32_t dst_pitch, + const uint32_t mode, const uint32_t availability, + uint32_t qp, uint32_t totalCoeffMask); + +void rsph264_queue_write_macroblock( + int cache_flags, + const uint8_t *src, + uint8_t *dst_luma, uint8_t *dst_cb, uint8_t *dst_cr, + uint32_t mb_width); + +// debugging functions (only for tests) +void rsph264_queue_debug_random_status(void); +void rsph264_queue_debug_load_overlay(const char* ovlname); + +#endif diff --git a/src/video/rsph264_intra.S b/src/video/rsph264_intra.S new file mode 100644 index 000000000..fa42b7242 --- /dev/null +++ b/src/video/rsph264_intra.S @@ -0,0 +1,1604 @@ +############################################################ +# RSPH264 - Intra-prediction functions +# +# Authors: Giovanni Bajo +############################################################ +#include + + .data + + RSPQ_BeginOverlayHeader + RSPQ_DefineCommand OMX_IntraPredictChroma8x8, 32 + RSPQ_DefineCommand Task_ProcessLumaIntra4x4Residual, 40 + RSPQ_DefineCommand Task_ProcessLumaIntra16x16Residual, 32 + RSPQ_DefineCommand Task_ProcessChromaResidual, 28 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand RSPQ_Loop, 4 + RSPQ_DefineCommand Task_SetPackedDeltaBuffer, 4 + RSPQ_EndOverlayHeader + +#include "rsph264_common.inc" + + .data + + .align 4 + .ascii "OVL: Intra-pred " + +INTRALUMA4_TABLE: + .half IL4X4_VER - _start + .half IL4X4_HOR - _start + .half IL4X4_DC - _start + .half IL4X4_DIAG_DL - _start + .half IL4X4_DIAG_DR_VR_HD - _start + .half IL4X4_DIAG_DR_VR_HD - _start + .half IL4X4_DIAG_DR_VR_HD - _start + .half IL4X4_VL - _start + .half IL4X4_HU - _start + +INTRALUMA16_TABLE: + .half IL16x16_VER - _start + .half IL16x16_HOR - _start + .half IL16x16_DC - _start + .half IL16x16_PLANE - _start + +INTRACHROMA8_TABLE: + .half IC8x8_DC - _start + .half IC8x8_HOR - _start + .half IC8x8_VER - _start + .half IC8x8_PLANE - _start + + .align 4 +INTRALUMA4_COEFF: + .half 1<<9 # Used by IL4X4_DC / IC8X8_DC (note: it MUST stay in the first lane!) + .half 1<<6 # Used by IL4X4_DIAG_*, for 3-tap filter + .half 2<<6 # Used by IL4X4_DIAG_*, for 3-tap filter + .half 1<<7 # Used by IL4X4_FINISH_VR/HD, for 2-tap filter + .half 0 + .half 128 + .half 2<<7 # Rounding, used by IL4X4_DC + .half 0 # Used to zero accumulator by IL4X4_DC + +INTRALUMA16_COEFF1: # Used by IL16x16_PLANE + .half 1 + .half 2 + .half 3 + .half 4 + .half 5 + .half 6 + .half 7 + .half 8 + + #define ILSH 10 +INTRALUMA16_COEFF2: # Used by IL16x16_PLANE + .half (-7<>16, so the end result is accumulating the sum + # into $v00, fully unshifted. + # Notice that we care only about the first lane ([e8]), and we ignore + # the other lanes. + vmadm $v00, v_coeff, v_l0,0 + vmadm $v00, v_coeff, v_l1,0 + vmadm $v00, v_coeff, v_l2,0 + vmadm $v00, v_coeff, v_l3,0 + vmadm $v00, v_coeff, v_coeff,14 # +2 + addi t1, -1 + +IDCTestUpper: + andi t0, availability, OMX_VC_UPPER + beqz t0, IDCCheck + nop + + # Calculate U0+U1+U2+U3+2, result into $v00[e8] + vmadm $v00, v_coeff, v_u,8 + vmadm $v00, v_coeff, v_u,9 + vmadm $v00, v_coeff, v_u,10 + vmadm $v00, v_coeff, v_u,11 + vmadm $v00, v_coeff, v_coeff,14 # +2 + addi t1, -1 + +IDCCheck: + # To calculate the average, we should divide by either 4 (>>2) or 8 (>>3), + # depending on the number of pixels we added. After that, we want to shift + # back the pixel into the higher part (<<7) to be used by LUV/SUV. So in + # total, we should either <<5 or <<4. + beqz t1, IDCFinal + vsll $v00, $v00,4 # <<4 + addi t1, -1 + beqz t1, IDCFinal + vsll $v00, $v00,1 # <<4 <<1 = <<5 + + # Zero elements were added: use 128 as final prediction + li t2, 128<<7 + mtc2 t2, $v00,0 + +IDCFinal: + jr ra + nop + .endfunc + +#################################################################### +# IL4X4 +# +# Intra-prediction of Luma 4x4 block: inner function. +#################################################################### +# ARGS: +# $v00: upper-left pixel (only first lane will be used) +# $v01: top 8 pixels +# t2: mode +# s3: left buffer +# t3: pitch of the left buffer +# s4: destination buffer +# t4: pitch of the destination buffer +# fp (availability macro: neighbor availability bitmask +# +# OUTPUT: +# $v00-$v03: predicted pixels (first 4 lanes in each vector) +# Predicted pixels are also stored at s4 +# + .func IL4X4 +IL4X4: + # Set v_zero to 0 + vxor v_zero, v_zero, v_zero,0 + + # Load L0-L4 + luv v_l0,0, 0,s3 + add s3, t3 + luv v_l1,0, 0,s3 + add s3, t3 + luv v_l2,0, 0,s3 + add s3, t3 + luv v_l3,0, 0,s3 + + # Create a mask to preserve the other 4 pixels in each row. + li t0, 0xF0 + ctc2 t0, COP2_CTRL_VCC + + # Prepare v_u. If upper right MB doesn't exist, overfill last valid pixel + andi t0, availability, OMX_VC_UPPER_RIGHT + bnez t0, NoOverfill + vor v_u, v_zero, $v01,0 + + vor $v01, v_zero, v_u,11 + vmrg v_u, $v01, v_u,0 + +NoOverfill: + # Store (potentially overfilled) v_u into temporary buffer V_0. + li t0, %lo(V_0) + suv v_u,0, 0,t0 + + # Load v_ul_u, also recovering the first pixel that was read before. + addi t0, -1 + luv v_ul_u,0, 0,t0 + vmov v_ul_u,0, $v00,0 + + # S4 is the buffer where pixels must be predicted (MB_OUPUT). + # Point S0 to the scratch buffer, that can be used by prediction functions + li s0, %lo(MB_SCRATCH) + + # Jump to the function implementing the requested type of prediction + sll t0, t2, 1 + lhu t0, %lo(INTRALUMA4_TABLE)(t0) + li t1, %lo(INTRALUMA4_COEFF) + jr t0 + lqv v_coeff,0, 0,t1 + .endfunc + + .func IL4X4_VER +IL4X4_VER: + # Vertical intra-prediction: + # Fill output pixels with U0-U4 repeated in each row + vor $v00, v_zero, v_u,0 + vor $v01, v_zero, v_u,0 + vor $v02, v_zero, v_u,0 + j IL4X4_FINISH + vor $v03, v_zero, v_u,0 + .endfunc + + .func IL4X4_HOR +IL4X4_HOR: + # Horizontal intra-prediction: + # Fill output pixels with L0-L4 repeated in each column + vor $v00, v_zero, v_l0,8 + vor $v01, v_zero, v_l1,8 + vor $v02, v_zero, v_l2,8 + j IL4X4_FINISH + vor $v03, v_zero, v_l3,8 + .endfunc + + .func IL4X4_DC +IL4X4_DC: + move sp, ra + jal IntraDC4x4 + nop + move ra, sp + vor $v00, v_zero, $v00,8 + vor $v01, v_zero, $v00,8 + vor $v02, v_zero, $v00,8 + j IL4X4_FINISH + vor $v03, v_zero, $v00,8 + .endfunc + + + .func IL4X4_DIAG_DL +IL4X4_DIAG_DL: + suv v_u,0, 0,s0 + luv $v01,15, 0,s0 + luv $v02,14, 0,s0 + + # We now have: + # V_U: U0 U1 U2 U3 U4 U5 U6 U7 + # $v01: U1 U2 U3 U4 U5 U6 U7 U0 + # $v02: U2 U3 U4 U5 U6 U7 U0 U1 + # + # We don't care to adjust the last lane, but we need to replicate U7 in the 6th + # lane of $v02 becuase the filter behaves like with overfilling, and we need + # the 6th lane to hold the correct result + vmov $v02,14, $v02,13 + + # Calculate 3-tap filter and shift pixel result <<7 (into higher part, aligned + # for LUV/SUV). + vmulf $v00, v_u, v_coeff,9 + vmacf $v00, $v01, v_coeff,10 + vmacf $v00, $v02, v_coeff,9 + vsll $v00, $v00, 7 + + # Rotate lines as expected for output (in diagonal) + suv $v00,0, 0,s0 + luv $v01,15, 0,s0 + luv $v02,14, 0,s0 + j IL4X4_FINISH + luv $v03,13, 0,s0 + .endfunc + + + .func IL4X4_DIAG_DR_VR_HD +IL4X4_DIAG_DR_VR_HD: + vor $v04, v_zero, v_ul_u,0 + vmov $v04,4, v_l3,8 + vmov $v04,5, v_l2,8 + vmov $v04,6, v_l1,8 + vmov $v04,7, v_l0,8 + suv $v04,0, 0*8,s0 + suv $v04,0, 1*8,s0 + luv $v00,2, 0*8,s0 + luv $v01,3, 0*8,s0 + luv $v02,4, 0*8,s0 + + # We now have: + # $v00: L1 L0 UL U0 U1 U2 L3 L2 + # $v01: L2 L1 L0 UL U0 U1 U2 L3 + # $v02: L3 L2 L1 L0 UL U0 U1 U2 + # + # Correct $v00 6th-lane, putting U3 instead of L3. We don't care about lane 7. + vmov $v00,14, v_ul_u,12 + + # Calculate 3-tap filter and shift pixel result <<7 (into higher part, aligned + # for LUV/SUV). + vmulf $v04, $v00, v_coeff,9 + vmacf $v04, $v01, v_coeff,10 + beq t2, 6, IL4X4_FINISH_HD + vmacf $v04, $v02, v_coeff,9 + vsll $v04, $v04, 7 + + suv $v04,0, 0*8,s0 + beq t2, 5, IL4X4_FINISH_VR + suv $v04,0, 1*8,s0 + + # Rotate lines as expected for output (in diagonal) + luv $v00,5, 0,s0 + luv $v01,6, 0,s0 + luv $v02,7, 0,s0 + j IL4X4_FINISH + luv $v03,8, 0,s0 + +IL4X4_FINISH_VR: + luv $v01,5, 0,s0 + luv $v03,6, 0,s0 + vmov $v03,8, $v04,9 + + vmulf $v00, v_ul_u, v_coeff,11 + vmacf $v00, v_u, v_coeff,11 + vsll $v00, $v00, 7 + + suv $v00,0, 0,s0 + luv $v02,1, 0,s0 + j IL4X4_FINISH + vmov $v02,8, $v04,10 + +IL4X4_FINISH_HD: + # $v04 contains the (unshifted) result of the 3-tap filter, so the pixel + # value is in the LSB. + vmov $v05,8, v_l3,8 + vmov $v05,9, v_l2,8 + vmov $v05,10, v_l1,8 + vmov $v05,11, v_l0,8 + vmov $v05,12, v_ul_u,8 + suv $v05,0, 0,s0 + luv $v06,15, 0,s0 + + # $v05 = L3 L2 L1 L0 UL + # $v06 = L2 L1 L0 UL + # $v07 = 2-tap filters of the above + vmulf $v07, $v05, v_coeff,11 + vmacf $v07, $v06, v_coeff,11 + vsll8 $v07, $v07, 8 + + # Interleave $v04 and $v07 and write them to scratch buffer + vor $v07, $v04, $v07,0 + sqv $v07,0, 0,s0 + + luv $v03,0, 0,s0 + luv $v02,14, 0,s0 + luv $v01,12, 0,s0 + luv $v00,10, 0,s0 + + # Adjust last two pixels of v00 which are different + vsll $v04, $v04, 7 + vmov $v00,10, $v04,12 + j IL4X4_FINISH + vmov $v00,11, $v04,13 + .endfunc + + .func IL4X4_VL +IL4X4_VL: + suv v_u,0, 0,s0 + luv $v04,15, 0,s0 + luv $v05,14, 0,s0 + vmulf $v00, v_u, v_coeff,11 + vmacf $v00, $v04, v_coeff,11 + + vmulf $v01, v_u, v_coeff,9 + vmacf $v01, $v04, v_coeff,10 + vmacf $v01, $v05, v_coeff,9 + + vsll $v00, $v00, 7 + vsll $v01, $v01, 7 + + suv $v00,0, 0,s0 + luv $v02,15, 0,s0 + + suv $v01,0, 0,s0 + j IL4X4_FINISH + luv $v03,15, 0,s0 + .endfunc + + .func IL4X4_HU +IL4X4_HU: + # Create: + # $v04 = L0 L1 L2 L3 L3 L3 L3 L3 + vmov $v04,8, v_l3,8 + vor $v04, v_zero, $v04,8 + vmov $v04,8, v_l0,8 + vmov $v04,9, v_l1,8 + vmov $v04,10, v_l2,8 + + # $v04 = L0 L1 L2 L3 L3 L3 L3 L3 + # $v05 = L1 L2 L3 L3 L3 L3 L3 L0 + # $v06 = L2 L3 L3 L3 L3 L3 L0 L1 + # (obviously, we only care about the first 4 lanes) + suv $v04,0, 0,s0 + luv $v05,15, 0,s0 + luv $v06,14, 0,s0 + + # 2-tap filter + vmulf $v07, $v04, v_coeff,11 + vmacf $v07, $v05, v_coeff,11 + + # 3-tap filter + vmulf $v04, $v04, v_coeff,9 + vmacf $v04, $v05, v_coeff,10 + vmacf $v04, $v06, v_coeff,9 + + # Interleave 2-tap and 3-tap + vsll8 $v07, $v07, 8 + vor $v07, $v07, $v04,0 + + # Compose destination registers as expected + sqv $v07,0, 0,s0 + luv $v00,0, 0,s0 + luv $v01,14, 0,s0 + luv $v02,12, 0,s0 + j IL4X4_FINISH + luv $v03,10, 0,s0 + .endfunc + + .func IL4X4_FINISH +IL4X4_FINISH: + # We have now calculated the 4x4 block into the first + # 4 lanes of $v00-$v03. We need to put it into the 8x8 + # block at s4, preserving the other pixels. Use VMRG, + # considering that the mask has been already calculated. + + # Calculate beginning of each line using pitch in t4 + add s3, s4, t4 + add s2, s3, t4 + add s1, s2, t4 + + luv $v16,0, 0,s4 + luv $v17,0, 0,s3 + luv $v18,0, 0,s2 + luv $v19,0, 0,s1 + + vmrg $v16, $v16, $v00,0 + vmrg $v17, $v17, $v01,0 + vmrg $v18, $v18, $v02,0 + vmrg $v19, $v19, $v03,0 + + suv $v16,0, 0,s4 + suv $v17,0, 0,s3 + suv $v18,0, 0,s2 + jr ra + suv $v19,0, 0,s1 + .endfunc + + +#################################################################### +# OMX_IntraPredictChroma8x8 +# +# Intra-prediction of a 8x8 chroma macroblock +#################################################################### +# ARGS: +# 0: srcbuf_l: pointer to first pixel in left column (-1,0) +# 1: srcbuf_u: pointer to first pixel in top column (0,-1) +# 2: srcbuf_ul: pointer to pixel in top-left corner (-1,-1) +# 3: dstbuf: pointer to destination buffer +# 4: src_dma_width: pitch of the source buffers +# 5: dst_dma_width: pitch of the destination buffers +# 6: mode: prediction mode +# 7: availability: bitmask of availability of neighbor macroblocks + + .func OMX_IntraPredictChroma8x8 +OMX_IntraPredictChroma8x8: + lw src_dma_width, CMD_ADDR(16, 32) + lw dst_dma_width, CMD_ADDR(20, 32) + lw availability, CMD_ADDR(28, 32) + + # $v14: U4 U5 U6 U7 -- -- -- -- + #define v_u4 $v14 + #define dmem_l s3 + + vxor v_zero, v_zero, v_zero,0 + + # Load UL byte into $v00, will be used later to compose v_ul_u. We load + # 8 pixels but we only care about first one. + move s0, srcbuf_ul + jal IntraSrcIn + li t0, DMA_SIZE(8,1) + luv $v00,0, 0,s4 + + # Read U. This is a sequence of 8 ALIGNED bytes + move s0, srcbuf_u + jal IntraSrcIn + li t0, DMA_SIZE(8,1) + luv v_u,0, 0,s4 + addi s4, 4 + luv v_u4,0, 0,s4 + addi s4, -5 + luv v_ul_u,0, 0,s4 + vmov v_ul_u,8, $v00,8 + + # Read L. This will be a 8x8 block where the first column + # is the L vector + move s0, srcbuf_l + jal IntraSrcIn + li t0, DMA_SIZE(8,8) + move dmem_l, s4 + + # Dispatch + lw t0, CMD_ADDR(24, 32) + sll t2, t0, 1 + lhu t2, %lo(INTRACHROMA8_TABLE)(t2) + jr t2 + li s4, %lo(MB_OUTPUT) + + +IntraChroma8End: + # Copy 8x8 macroblock back into RDRAM + li t2, DMA_OUT # direction: DMEM -> RDRAM + jal_and_j IntraMBInOut, RSPQ_Loop + .endfunc + + .func IC8x8_DC +IC8x8_DC: + # Load coefficients needed by IntraDC4x4 + li t0, %lo(INTRALUMA4_COEFF) + lqv v_coeff,0, 0,t0 + + li t0, 0x0F + ctc2 t0, COP2_CTRL_VCC + + andi t8, availability, 3 # Keep only OMX_VC_UPPER | OMX_VC_LEFT + vor $v04, v_zero, v_u,0 # Save v_u into $v04 + + # Load first 4 left pixels, as we're going to process + # the first two 4x4 upper blocks first + luv v_l0,0, 0*8,dmem_l + luv v_l1,0, 1*8,dmem_l + luv v_l2,0, 2*8,dmem_l + luv v_l3,0, 3*8,dmem_l + + # Call IntraDC on upper-left 4x4 block. Save result into $v01 + jal IntraDC4x4 + nop + vor $v01, v_zero, $v00,8 + + # Call IntraDC on upper-right 4x4 block. We need to use + # v_u4 instead of v_u, and we must change the availability mask + # to only use the upper pixels if available + lbu availability, %lo(INTRADC_UPPER_MASK)(t8) + jal IntraDC4x4 + vor v_u, v_zero, v_u4,0 + + # Merge upper-left and upper-right result into $v01 + vmrg $v01, $v01, $v00,8 + + # Now we're moving to lower 4x4 blocks. Load the last 4 left + # pixels. + luv v_l0,0, 4*8,dmem_l + luv v_l1,0, 5*8,dmem_l + luv v_l2,0, 6*8,dmem_l + luv v_l3,0, 7*8,dmem_l + + # Call IntraDC on lower-left 4x4 block. Restore original + # v_u, and change the availability mask to only use the left + # pixels if available. Save result into $v02. + lbu availability, %lo(INTRADC_LEFT_MASK)(t8) + jal IntraDC4x4 + vor v_u, v_zero, $v04,0 + vor $v02, v_zero, $v00,8 + + # Call IntraDC on lower-right 4x4 block. Restore original + # availability, and use v_u4. + move availability, t8 + jal IntraDC4x4 + vor v_u, v_zero, v_u4,0 + + # Merge lower-left and lower-right result into $v02 + vmrg $v02, $v02, $v00,8 + + # Store the result + suv $v01,0, 0*8,s4 + suv $v01,0, 1*8,s4 + suv $v01,0, 2*8,s4 + suv $v01,0, 3*8,s4 + suv $v02,0, 4*8,s4 + suv $v02,0, 5*8,s4 + suv $v02,0, 6*8,s4 + j IntraChroma8End + suv $v02,0, 7*8,s4 + .endfunc + + .func IC8x8_HOR +IC8x8_HOR: + addi t0, s4, 64-16 +IC8x8_HOR_StoreLoop: + lpv $v00,0, 0*8,dmem_l + vor $v00, v_zero, $v00,8 + lpv $v01,0, 1*8,dmem_l + vor $v01, v_zero, $v01,8 + addi dmem_l, 16 + spv $v00,0, 0*8,s4 + spv $v01,0, 1*8,s4 + bne t0, s4, IC8x8_HOR_StoreLoop + addi s4, 16 + j IntraChroma8End + nop + .endfunc + + .func IC8x8_VER +IC8x8_VER: + suv v_u,0, 0*8,s4 + suv v_u,0, 1*8,s4 + suv v_u,0, 2*8,s4 + suv v_u,0, 3*8,s4 + suv v_u,0, 4*8,s4 + suv v_u,0, 5*8,s4 + suv v_u,0, 6*8,s4 + j IntraChroma8End + suv v_u,0, 7*8,s4 + .endfunc + + .func IC8x8_PLANE +IC8x8_PLANE: + #define v_coeff_hv $v16 + #define v_coeff_int $v17 + #define v_coeff_misc $v18 + + li t0, %lo(INTRACHROMA_COEFF1) + lqv v_coeff_hv,0, 0*16,t0 + lqv v_coeff_int,0, 1*16,t0 + lqv v_coeff_misc,0, 2*16,t0 + + # HV0 = $v02 = U7 U6 U5 U4 L4 L5 L6 L7 + # HV1 = $v01 = UL U0 U1 U2 L2 L1 L0 UL + # V_COEFF_HV = 4 3 2 1 1 2 3 4 + + vor $v01, v_zero, v_ul_u,0 + luv $v03,0, 0*8,dmem_l # L0 + luv $v04,0, 1*8,dmem_l # L1 + luv $v05,0, 2*8,dmem_l # L2 + vmov $v01,15, v_ul_u,8 + vmov $v01,14, $v03,8 + vmov $v01,13, $v04,8 + vmov $v01,12, $v05,8 + + vmov $v02,8, v_u,15 + vmov $v02,9, v_u,14 + vmov $v02,10, v_u,13 + vmov $v02,11, v_u,12 + + luv $v03,0, 4*8,dmem_l # L4 + luv $v04,0, 5*8,dmem_l # L5 + luv $v05,0, 6*8,dmem_l # L6 + luv $v06,0, 7*8,dmem_l # L7 + vmov $v02,12, $v03,8 + vmov $v02,13, $v04,8 + vmov $v02,14, $v05,8 + vmov $v02,15, $v06,8 + + # Compute (HV0-HV1)*coeff + vsubc $v00, $v02, $v01,0 + vsra $v00, $v00, 7 + vmudh $v00, $v00, $v16,0 + + # Compute sum of first 4-lanes and second 2-lanes + vaddc $v00, $v00, $v00,3 + vaddc $v00, $v00, $v00,6 + + # Compute (17*H/V + 32)>>6. We use vmulf to have the +32 for free. + vmulf $v06, $v00, v_coeff_misc,9 + + # Compute 16*(U7+L7). Notice that we have U7 in v_u[e15] + # and L7 in $v02[15]. Put the result in $v01[e15] + vsrl $v02, $v02, 3 + vsrl $v01, v_u, 3 + vaddc $v01, $v01, $v02,0 + + # Now we have + # a = $v01[e15] + # b = $v06[e8] + # c = $v06[e12] + # + # all unshifted + + # Compute -3*c + vmudh $v07, $v06, v_coeff_misc,10 + + # Compute $v07 = A + -3*C. This is the base addend which + # is constant across all pixels in the first line. + # We'll update it adding C every line. + vaddc $v07, $v07, $v01,15 + vor $v07, v_zero, $v07,12 # copy into all lanes + + # Main loop. Generate two lines at a time to lower the overhead + li t1, 4 +LoopPlane8: + # Compute (a + b*(x-3) + c*(y-3) + 16)>>5 + # where x is the lane number (0..7) + vmulf $v00, v_coeff_int, $v06,8 + vmacu $v00, $v07, v_coeff_misc,11 + + # Update the constant addend: $v07 += c + vaddc $v07, $v07, $v06,12 + + # Compute the same on next line + vmulf $v01, v_coeff_int, $v06,8 + vmacu $v01, $v07, v_coeff_misc,11 + + # Update the constant addend: $v07 += c + vaddc $v07, $v07, $v06,12 + + vch $v00, $v00, v_coeff_misc,8 + vch $v01, $v01, v_coeff_misc,8 + + vsll $v00, $v00, 7 + vsll $v01, $v01, 7 + + suv $v00,0, 0*8,s4 + suv $v01,0, 1*8,s4 + addi s4, 16 + + bnez t1, LoopPlane8 + addi t1, -1 + + #undef v_coeff_misc + #undef v_coeff_int + #undef v_coeff_hv + + j IntraChroma8End + nop + .endfunc + + #undef v_u4 + #undef dmem_l + +#################################################################### + + # Registers shared by Luma4x4 and Chroma8x8 + #undef v_ul_u + #undef v_u + #undef v_l0 + #undef v_l1 + #undef v_l2 + #undef v_l3 + +#################################################################### +#################################################################### + + +#################################################################### +# IL16x16 +# +# Inner function for Intra-prediction of a 16x16 luma macroblock partition. +#################################################################### +# ARGS: +# a0: srcbuf_l: pointer to first pixel in left column (-1,0) +# a1: srcbuf_u: pointer to first pixel in top column (0,-1) +# a2: srcbuf_ul: pointer to pixel in top-left corner (-1,-1) +# k0: src_dma_width: pointer to destination buffer +# t3: prediction mode +# + .func IL16x16 +IL16x16: + #define v_u0 $v08 + #define v_u1 $v09 + #define v_ul $v11 + #define v_l0 $v12 + #define v_l1 $v13 + #define dmem_l s3 + + vxor v_zero, v_zero, v_zero,0 + + # We save return address in sp to be able to call subroutines from this + # function. + move sp, ra + + .macro IL16x16Return + jr sp; nop + .endm + + andi t0, availability, OMX_VC_UPPER + beqz t0, IL16x16_LoadLeft + + # Load UL byte into the corresponding register. We load + # 8 pixels but we only care about first one. + move s0, srcbuf_ul + jal IntraSrcIn + li t0, DMA_SIZE(8,1) + luv v_ul,0, 0*8,s4 + + # Read U. This is a sequence of 16 ALIGNED bytes + move s0, srcbuf_u + jal IntraSrcIn + li t0, DMA_SIZE(16,1) + luv v_u0,0, 0*8,s4 + luv v_u1,0, 1*8,s4 + +IL16x16_LoadLeft: + andi t0, availability, OMX_VC_LEFT + beqz t0, IL16x16_Execute + + # Read L. This will be a 8x16 block where the first column + # is the L vector + move s0, srcbuf_l + jal IntraSrcIn + li t0, DMA_SIZE(8,16) + move dmem_l, s4 + + # Prepare the vectors V_L0/V_L1 containing the first column that + # was copied by DMA, that is 1 byte every 8. We use LFV to load + # one every 4 bytes, then store into scratch memory, and then + # again LHV to load one every 2 bytes. + li t0, %lo(V_0) + lfv v_l0,0, 0*16,dmem_l + lfv v_l0,8, 1*16,dmem_l + lfv v_l1,0, 2*16,dmem_l + lfv v_l1,8, 3*16,dmem_l + suv v_l0,0, 0*8,t0 + suv v_l1,0, 1*8,t0 + + lfv v_l0,0, 4*16,dmem_l + lfv v_l0,8, 5*16,dmem_l + lfv v_l1,0, 6*16,dmem_l + lfv v_l1,8, 7*16,dmem_l + suv v_l0,0, 2*8,t0 + suv v_l1,0, 3*8,t0 + + lhv v_l0,0, 0*16,t0 + lhv v_l1,0, 1*16,t0 + +IL16x16_Execute: + sll t3, t3, 1 + lhu t3, %lo(INTRALUMA16_TABLE)(t3) + jr t3 + li s4, %lo(MB_OUTPUT) + .endfunc + + + .func IL16x16_VER +IL16x16_VER: + addi t0, s4, 256-32 +IL16x16_VER_StoreLoop: + suv v_u0,0, 0*8,s4 + suv v_u1,0, 1*8,s4 + suv v_u0,0, 2*8,s4 + suv v_u1,0, 3*8,s4 + bne t0, s4, IL16x16_VER_StoreLoop + addi s4, 32 + IL16x16Return + .endfunc + + .func IL16x16_HOR +IL16x16_HOR: + addi t0, s4, 256-32 +IL16x16_HOR_StoreLoop: + lpv $v00,0, 0*8,dmem_l + vor $v00, v_zero, $v00,8 + lpv $v01,0, 1*8,dmem_l + vor $v01, v_zero, $v01,8 + addi dmem_l, 8 + spv $v00,0, 0*8,s4 + spv $v00,0, 1*8,s4 + spv $v01,0, 2*8,s4 + spv $v01,0, 3*8,s4 + bne t0, s4, IL16x16_HOR_StoreLoop + addi s4, 16 + IL16x16Return + .endfunc + + .func IL16x16_DC +IL16x16_DC: + li t3, 2 + andi t0, availability, OMX_VC_UPPER + beqz t0, IDC16TestLeft + li t2, 0 + + vaddc $v00, v_u0, v_u1,0 + jal VAddLanes + vsrl $v00, $v00, 7 + mfc2 t2, $v00,0 + addi t2, 8 + addi t3, -1 + +IDC16TestLeft: + andi t0, availability, OMX_VC_LEFT + beqz t0, IDC16Calc + + vaddc $v00, v_l0, v_l1,0 + jal VAddLanes + vsrl $v00, $v00, 7 + mfc2 t1, $v00,0 + add t2, t1 + addi t2, 8 + addi t3, -1 + +IDC16Calc: + beqz t3, IDC16Store + sll t2, 2 + addi t3, -1 + beqz t3, IDC16Store + sll t2, 1 + li t2, 128<<7 +IDC16Store: + mtc2 t2, $v00, 0 + vor v_u0, v_zero, $v00,8 + vor v_u1, v_zero, $v00,8 + j IL16x16_VER + nop + .endfunc + + .func IL16x16_PLANE +IL16x16_PLANE: + #define v_coeff_hv $v16 + #define v_coeff_int1 $v17 + #define v_coeff_int2 $v18 + #define v_coeff_misc $v19 + #define plane_c t6 + + li t0, %lo(INTRALUMA16_COEFF1) + lqv v_coeff_hv,0, 0*16,t0 + lqv v_coeff_int1,0, 1*16,t0 + lqv v_coeff_int2,0, 2*16,t0 + lqv v_coeff_misc,0, 3*16,t0 + + # Compute the horizontal filter H into $v06[e9] + vor $v01, v_zero, v_u0,0 + jal PlaneAccum + vor $v02, v_zero, v_u1,0 + vmov $v06,9, $v00,8 + + # Compute the vertical filter V into $v06[e10] + vor $v01, v_zero, v_l0,0 + jal PlaneAccum + vor $v02, v_zero, v_l1,0 + vmov $v06,10, $v00,8 + + # Compute (5*H/V+32)>>6. We use vmulf to get the +32 + # for free. + # FIXME: The maximum value we can get for H/V is 0x1BE4, + # so check whether this overflows. + vmulf $v06, $v06, v_coeff_misc,9 + + # Compute 16*(U15+L15), into $v01[e15] + vsrl $v01, v_u1, 3 + vsrl $v02, v_l1, 3 + vaddc $v01, $v01, $v02,0 + + # Now we have: + # $v01[e15] = a + # $v06[e9] = b + # $v06[e10] = c + # + # all unshifted + + # Compute -7*c + vmudh $v07, $v06, v_coeff_misc,10 + + # Compute $v07 = A + -7*C. This is the base addend which + # is constant across all pixels in the first line. + # We'll update it adding C every line. + vaddc $v07, $v07, $v01,15 + vor $v07, v_zero, $v07,10 # copy into all lanes + + li t1, 15 +LoopPlane: + # Compute (a + b*(x-7) + c*(y-7) + 16)>>5 + # where x is the lane number (0..15), split between + # $v00 and $v01 + vmulf $v00, v_coeff_int1, $v06,9 + vmacu $v00, $v07, v_coeff_misc,11 + + vmulf $v01, v_coeff_int2, $v06,9 + vmacu $v01, $v07, v_coeff_misc,11 + + vch $v00, $v00, v_coeff_misc,8 + vch $v01, $v01, v_coeff_misc,8 + + vsll $v00, $v00, 7 + vsll $v01, $v01, 7 + + # Update the constant addend: $v07 += c + vaddc $v07, $v07, $v06,10 + + suv $v00,0, 0*8,s4 + suv $v01,0, 1*8,s4 + addi s4, 16 + + bnez t1, LoopPlane + addi t1, -1 + + IL16x16Return + .endfunc + + .func PlaneAccum +PlaneAccum: + # Given: + # $v01: U0 U1 U2 U3 U4 U5 U6 U7 + # $v02: U8 U9 U10 U11 U12 U13 U14 U15 + # + # We need to calculate: + # 8 * (U15 - UL) + + # 7 * (U14 - U0) + + # 6 * (U13 - U1) + + # 5 * (U12 - U2) + + # 4 * (U11 - U3) + + # 3 * (U10 - U4) + + # 2 * (U9 - U5) + + # 1 * (U8 - U6) + # + # (Notice the presence of UL, and U7 being ignored). + # + # So the first problem is that we need to reverse most of $v01 + # to be able to subtract it from $v02. Unfortunately, + # there is no fast way of doing it, but moving each lane + # one at a time. + vmov $v00,8, $v01,14 + vmov $v00,9, $v01,13 + vmov $v00,10, $v01,12 + vmov $v00,11, $v01,11 + vmov $v00,12, $v01,10 + vmov $v00,13, $v01,9 + vmov $v00,14, $v01,8 + vmov $v00,15, v_ul,8 + + # Do the subtraction, and multiply by the coefficient + # vector (8, 7, .., 1). + vsubc $v00, $v02, $v00,0 + vsra $v00, $v00, 7 + vmudh $v00, $v00, v_coeff_hv,0 + + # FALLTRHOUGH! + .endfunc + + .func VAddLanes +VAddLanes: + # Sum all lanes together into $v00[e8] + vaddc $v00, $v00, $v00,3 + vaddc $v00, $v00, $v00,6 + jr ra + vaddc $v00, $v00, $v00,12 + .endfunc + + #undef v_coeff_hv + #undef v_coeff_int1 + #undef v_coeff_int2 + #undef v_coeff_misc + #undef plane_c + + #undef v_u0 + #undef v_u1 + #undef v_zero + #undef v_ul + #undef v_l0 + #undef v_l1 + #undef dmem_l + +#################################################################### + + #undef v_zero + #undef v_coeff + + #undef srcbuf_l + #undef srcbuf_u + #undef srcbuf_ul + #undef dstbuf + + #undef src_dma_width + #undef dst_dma_width + +#################################################################### +#################################################################### + +################################################################### +# Task_ProcessLumaIntra4x4Residual +# +#################################################################### +# ARGS: +# 0: src frame (used only for neighbor pixels) +# 1: dst buffer (where the destination macroblock will be written) +# 2: src_dma_width +# 3: dst_dma_width +# 4: modeAvail bits (0-3) +# 5: modeAvail bits (4-7) +# 6: modeAvail bits (8-11) +# 7: modeAvail bits (12-15) +# 8: qp +# 9: totalCoeffMask (bit i=1 if totalCoeff[i] != 0) + + #define src a0 + #define dst a1 + #define src_dma_width a2 + #define dst_dma_width a3 + + #define npart t8 + #define tcmask s5 + + .func Task_ProcessLumaIntra4x4Residual +Task_ProcessLumaIntra4x4Residual: + lw tcmask, CMD_ADDR(36, 40) + +LoadBlocks: + # We will treat MB_INPUT as a 33x17 buffer, where the first line + # and column come from the source frame (neighbour macroblocks), and + # the final macroblock will be predicted at (1,1)-(17,17). + + # DMA from source frame pixel (-1,-1) into MB_INPUT. We only need + # the first line and column, but it's faster to simply bring the whole + # 33x17 in (aka 40x17 after unalignment adjustment). + sub s0, src, src_dma_width + addi s0, -1 + li s4, %lo(MB_INPUT) + li t0, DMA_SIZE(INTRA4RES_MBINPUT_WIDTH, 17) + jal DMAIn + move t1, src_dma_width + + # Calculate dequantization factors in v29 / v30 / v31. + jal CalcDequantFactors + lw t0, CMD_ADDR(32, 40) + + li npart, 0 +LumaBlockLoop: + # Calculate starting position of the 4x4 block within MB_INPUT. + # This will be our output buffer (in s4) + sll t0, npart, 1 + lhu t0, %lo(INTRA4RES_OFFSET)(t0) + addi s4, t0, %lo(MB_INPUT) + + # Load upper-left pixel in $v00, and upper line in $v01, + # and leave s3 pointing at the first left pixel. + addi s3, s4, -INTRA4RES_MBINPUT_WIDTH-1 + luv $v00,0, 0*8,s3 + addi s3, 1 + luv $v01,0, 0*8,s3 + addi s3, INTRA4RES_MBINPUT_WIDTH-1 + + # Load mode_avail + la t0, CMD_ADDR(16, 40) + add t0, npart + lbu t0, 0(t0) + + # Extract mode and availability (remapping bits) + srl t2, t0, 4 + andi t0, t0, 0xF + lbu fp, %lo(INTRA4RES_AVAILABILITY)(t0) + + li t3, INTRA4RES_MBINPUT_WIDTH # left pitch + jal IL4X4 + li t4, INTRA4RES_MBINPUT_WIDTH # dest pitch + + # Check whether we need to apply residual to this block. + srlv t0, tcmask, npart + andi t0, 1 + beqz t0, LumaBlockLoopEnd + nop + + jal UnpackNextDeltaBlock + nop + jal Dequant4x4 + nop + # s4 still points to the output buffer, where residual + # will be applied + jal TransformResidual4x4IDCT + nop + +LumaBlockLoopEnd: + bne npart, 15, LumaBlockLoop + addi npart, 1 + + # Our macroblock is now written in MB_INPUT at (8,8)-(24,24) + # We need to move it to MB_OUTPUT as a 16x16 block without extra pixels, + # to be able to DMA it to DMEM. + li s0, %lo(MB_INPUT)+INTRA4RES_INITIAL_OFFSET + li s4, %lo(MB_OUTPUT) + + # Copy one line at a time + li t0, 15 +Intra4Res_CopyLoop: + luv $v00,0, 0*8,s0 + luv $v01,0, 1*8,s0 + suv $v00,0, 0*8,s4 + suv $v01,0, 1*8,s4 + + addi s0, INTRA4RES_MBINPUT_WIDTH + addi s4, 16 + bnez t0, Intra4Res_CopyLoop + addi t0, -1 + + # DMA from MB_OUTPUT into DMEM and exit + move s0, dst + li s4, %lo(MB_OUTPUT) + move t1, dst_dma_width + li t0, DMA_SIZE(16,16) + jal_and_j DMAOut, RSPQ_Loop + + .endfunc + + #undef src + #undef dst + #undef src_dma_width + #undef dst_dma_width + + #undef npart + #undef tcmask + + +################################################################### +# Task_ProcessLumaIntra16x16Residual +# +#################################################################### +# ARGS: +# 0: src frame (used only for neighbor pixels) +# 1: dst buffer (where the destination macroblock will be written) +# 2: src_dma_width +# 3: dst_dma_width +# 4: predMode +# 5: availability +# 6: qp +# 7: totalCoeffMask (bit i=1 if totalCoeff[i] != 0) + + #define src a0 + #define dst a1 + #define src_dma_width k0 // note: this must match the one used in IL16x16 + #define dst_dma_width k1 // note: this must match the one used in IL16x16 + #define availability fp // note: this must match the one used in IL16x16 + + #define dc t7 + #define npart t8 + #define qp t9 + #define tcmask s5 + + +#################################################################### +# TransformDequantLumaDC +# +# Reconstruct a 4x4 Luma DC block from a packed block of coefficients. +# Do a 4x4 inverse Hadamard transformation, followed by a quantization +# step. +#################################################################### +# ARGS: +# $v00/$v01: 4x4 coefficient matrix +# t1: qp scale factor +# FIXME: used to be gp +# s4: address of destination buffer in DMEM +# + + .func TransformDequantLumaDC +TransformDequantLumaDC: + # Apply a IHT 4X4 transformation. This is quite similar to + # TransformResidual4x4IDCT, but there are a few differences: + # first, the coefficient matrix is different. Second, the matrix + # does not contain 0.5 / -0.5 values which require special care + # in TransformResidual4x4IDCT (see the various vand opcodes). + + # So we don't try to reuse the code for now, but just redo the + # double matrix multiplication. + vsll $v00, $v00, 1 + vsll $v01, $v01, 1 + + # Load coefficients + li t0, %lo(IHT_DC_COEFF) + lpv $v08,0, 0*8,t0 + lpv $v09,0, 2*8,t0 + lpv $v10,0, 3*8,t0 + lpv $v11,0, 1*8,t0 + + lpv $v12,0, 4*8,t0 + lpv $v13,0, 5*8,t0 + + vmulf $v02, $v08, $v00,4 + vmacf $v02, $v09, $v00,5 + vmacf $v02, $v10, $v00,6 + vmacf $v02, $v11, $v00,7 + + vmulf $v04, $v08, $v01,4 + vmacf $v04, $v09, $v01,5 + vmacf $v04, $v10, $v01,6 + vmacf $v04, $v11, $v01,7 + + vsll $v02, $v02, 1 + vsll $v04, $v04, 1 + + li t0, %lo(V_0) + sqv $v02,8, 0*16,t0 + sqv $v04,8, 1*16,t0 + lqv $v03,0, 0*16,t0 + lqv $v05,0, 1*16,t0 + + vmulf $v00, $v02, $v12,4 + vmacf $v00, $v03, $v12,5 + vmacf $v00, $v04, $v12,6 + vmacf $v00, $v05, $v12,7 + + vmulf $v01, $v02, $v13,4 + vmacf $v01, $v03, $v13,5 + vmacf $v01, $v04, $v13,6 + vmacf $v01, $v05, $v13,7 + + # Unified dequantization path (full precision, no branch): + # result = ((T * LS * (1 << (qp/6))) + 2) >> 2 + # This is equivalent to OMX reference for all QP ranges, while keeping + # the multiply in ACC to avoid 16-bit intermediate overflow. + vmudn $v06, $v27, $v29,8 # Lane e8 = LS * (1 << (qp/6)) + vxor $v07, $v07, $v07 + vaddc $v07, $v07, vshift,15 # v07 = 1 + + vmudm $v02, $v00, $v06,8 # ACC = T * scale + vmadm $v02, $v07, vshift,14 # ACC += 2 (1*2), v02 = ACCM + vsar $v03, COP2_ACC_LO # v03 = ACCL + vdsrl $v00, $v03, $v02, 2 + + vmudm $v02, $v01, $v06,8 + vmadm $v02, $v07, vshift,14 + vsar $v03, COP2_ACC_LO + vdsrl $v01, $v03, $v02, 2 + +LumaDCEnd: + sqv $v00,0, 0*16,s4 + jr ra + sqv $v01,0, 1*16,s4 + + .endfunc + + + .func Task_ProcessLumaIntra16x16Residual +Task_ProcessLumaIntra16x16Residual: + move src_dma_width, a2 + move dst_dma_width, a3 + lw qp, CMD_ADDR(24, 32) + lw availability, CMD_ADDR(20, 32) + + jal CalcDequantFactors + move t0, qp + + # Load TotalCoeff table (if asked to do so). Otherwise, + # it's already loaded correctly and we can just use it. + lw tcmask, CMD_ADDR(28, 32) + + # Reset LUMA_DC to zero + li s4, %lo(LUMA_DC) + vxor $v00, $v00, $v00,0 + sqv $v00,0, 0*16,s4 + sqv $v00,0, 1*16,s4 + + # Check whether totalCoeff[24] != 0, which means that + # there are DC data to unpack and transform. + srl t0, tcmask, 24 + andi t0, 1 + beqz t0, Luma16Predict + nop + + jal UnpackNextDeltaBlock + nop + jal TransformDequantLumaDC + move t1, qp + +Luma16Predict: + sub a1, src, src_dma_width # U + addi a2, a1, -1 # UL + addi a0, src, -1 # L + jal IL16x16 + lw t3, CMD_ADDR(16, 32) # prediction mode + + # Go through the 16 4x4 blocks + li npart, 0 +Luma16ResidualLoop: + # Check whether AC is present for this block (totalCoeffMask bit) and + # whether DC is non-zero (from LUMA_DC). + lbu t1, %lo(DC_DEQUANT_INDEX)(npart) + srlv t0, tcmask, npart + andi t0, 1 + lh dc, %lo(LUMA_DC)(t1) + + # If AC/DC are both 0, just skip this block, there's no residual. + or t2, dc, t0 + beqz t2, Luma16ResidualNext + nop + + # If AC is not 0, there is a residual coefficient matrix to unpack + # and dequant (into $v00/$v01). Otherwise, initialize the matrix + # to zero. + vxor $v00, $v00, $v00,0 + beqz t0, Luma16ResidualIDCT + vxor $v01, $v01, $v01,0 + + jal UnpackNextDeltaBlock + nop + jal Dequant4x4 + nop + +Luma16ResidualIDCT: + # Set DC as first coefficient in the matrix + mtc2 dc, $v00,0 + + # Run IDCT over the coefficient matrix and add the result to the + # predicted pixels in s4 + lbu t2, %lo(LUMA_DEQUANT_INDEX)(npart) + li t4, 16 + jal TransformResidual4x4IDCT + addi s4, t2, %lo(MB_OUTPUT) + +Luma16ResidualNext: + bne npart, 15, Luma16ResidualLoop + addi npart, 1 + + # Copy 16x16 macroblock back into RDRAM + lw a3, CMD_ADDR(4, 32) + li t2, DMA_OUT # Direction DMEM -> RDRAM + jal_and_j IntraMB16InOut, RSPQ_Loop + .endfunc diff --git a/src/video/subtitles.c b/src/video/subtitles.c new file mode 100644 index 000000000..eaf892571 --- /dev/null +++ b/src/video/subtitles.c @@ -0,0 +1,469 @@ +/** + * @file subtitles.c + * @author Giovanni Bajo + * @brief Subtitles API via the SUB64 format + * @ingroup video + * + * @section sub64_format SUB64 subtitle stream format (v1) + * + * This module consumes subtitle streams produced by `videoconv64` as `.sub64` sidecars. + * + * The `.sub64` file is typically stored **asset-compressed** (eg. `DCA3` header). In that case, + * all offsets described below are relative to the **decompressed SUB64 payload**, not the compressed + * bytes on disk. + * + * @subsection sub64_layout Layout and endianness + * + * - Multi-byte integers and floats are **big-endian**. + * - `DELT` uses **VarUInt LEB128** (7-bit groups, little-endian base-128). + * - Layout is fixed and sequential: `[Header][IDX0][DELT][OPC0][TXT0]`. + * + * @subsection sub64_header Header (fixed 36 bytes) + * + * - `char magic[5]` = `"SUB64"` + * - `u8 version` = `1` + * - `u16 flags` (currently 0) + * - `f32 fps` (IEEE754, big-endian) + * - `u32 num_frames` (number of frames in the subtitle stream) + * - `u32 num_syncs` (number of syncpoints) + * - `u8[44+44+4] runtime_state` (reserved for runtime state; empty in the file) + * + * @subsection sub64_idx0 IDX0 (seek index) + * + * `IDX0` is an array of `idx0_count` fixed-size entries (16 bytes each): + * + * - `u32 sync_frame` frame index of the syncpoint + * - `u32 delt_off` absolute byte offset to a position inside `DELT` + * - `u32 opc_off` absolute byte offset to a position inside `OPC0` + * - `u32 txt_off` absolute byte offset to a position inside `TXT0` + * + * `videoconv64` chooses syncpoints in **gaps** (moments with no active subtitles) in a best-effort way + * to split the stream into roughly equal chunks. `sync_frame = 0` is always present and duplicates + * (eg. due to long gaps) are omitted. + * + * @subsection sub64_streams Streams + * + * Event i is read in lock-step: + * + * - `delta_frames = read_varuint(DELT)` + * - `opcode = OPC0[i]` + * + * `DELT` includes one final terminal delta (without a matching opcode), + * used to define the exclusive end frame of the last subtitle state. + * + * Some opcodes also consume one NUL-terminated UTF-8 string from `TXT0` (in order). + * + * @subsection sub64_opcodes Opcodes and regions + * + * At most **one cue per region** can be visible at a time (3 regions: bottom/top/center). + * If multiple input cues overlap in the same region, the converter keeps the **latest** one: + * a newer `SHOW_*` replaces the previous cue in that region. When an older cue ends, it does + * not produce a `HIDE_*` if it was already replaced. + * + * SHOW opcodes select one of 3 fixed screen regions (minimal REGION support): + * + * - `0x01` SHOW_BOTTOM: show next string in the bottom region + * - `0x02` SHOW_TOP: show next string in the top region + * - `0x03` SHOW_CENTER: show next string in the center region + * + * HIDE opcodes hide the current cue in a specific region: + * + * - `0x10` HIDE_BOTTOM + * - `0x11` HIDE_TOP + * - `0x12` HIDE_CENTER + * + * Reserved: + * + * - `0x04` CLEAR: hide all subtitles (not currently emitted by the tool) + * + * @subsection sub64_text Text and markup + * + * `TXT0` contains UTF-8 strings terminated by `\\0`. + * + * `videoconv64` converts a minimal WebVTT subset to `rdpq_text` escapes: + * + * - ` .. ` -> `$02 ... $01` + * - ` .. ` -> `$03 ... $01` + * - `` is mapped to `$04` in the converter (best-effort) + * - `
` -> newline + * + */ + +#include "subtitles.h" +#include "../asset.h" +#include "../utils.h" +#include "display.h" +#include "rspq.h" +#include "rdpq.h" +#include "rdpq_rect.h" +#include "rdpq_mode.h" +#include "rdpq_text.h" +#include "rdpq_paragraph.h" +#include "rdpq_font.h" +#include "debug.h" +#include +#include + +typedef enum { + SUB64_REGION_BOTTOM = 0, + SUB64_REGION_TOP = 1, + SUB64_REGION_CENTER = 2, +} sub64_region_t; + +typedef enum { + // SHOW opcodes select one of 3 fixed regions on N64: bottom/top/center. + SUB64_OP_SHOW = 0x00, + SUB64_OP_SHOW_BOTTOM = SUB64_OP_SHOW + SUB64_REGION_BOTTOM, + SUB64_OP_SHOW_TOP = SUB64_OP_SHOW + SUB64_REGION_TOP, + SUB64_OP_SHOW_CENTER = SUB64_OP_SHOW + SUB64_REGION_CENTER, + // HIDE opcodes hide the current cue in a specific region. + SUB64_OP_HIDE = 0x10, + SUB64_OP_HIDE_BOTTOM = SUB64_OP_HIDE + SUB64_REGION_BOTTOM, + SUB64_OP_HIDE_TOP = SUB64_OP_HIDE + SUB64_REGION_TOP, + SUB64_OP_HIDE_CENTER = SUB64_OP_HIDE + SUB64_REGION_CENTER, + SUB64_OP_CLEAR = 0x04, +} sub64_opcode_t; + +typedef struct seek_entry_s { + uint32_t sync_frame; + uint8_t *deltas; + uint8_t *opcodes; + char *txts; +} seek_entry_t; + +typedef struct sub_state_s { + subtitle_cue_t cues[3]; ///< Bottom, top, center + int begin_frame_idx; ///< Frame index at which this state begins + int end_frame_idx; ///< Frame index at which next state begins (exclusive bound) + const uint8_t *cur_delta; ///< Current position in DELT stream + const uint8_t *cur_opcode; ///< Current position in OPC0 stream + char *cur_txt; ///< Current position in TXT0 stream +} sub_state_t; + +typedef struct subtitles_s { + char magic[5]; + uint8_t version; + uint16_t flags; + float fps; + uint32_t num_frames; + uint32_t num_syncs; + struct { ///< Runtime state (empty in the file) + sub_state_t state; ///< Current subtitle state + sub_state_t next_state; ///< Subtitle state at next change + int cur_frame_idx; ///< Current frame index + }; + seek_entry_t seek_entries[]; +} subtitles_t; + +_Static_assert(offsetof(subtitles_t, seek_entries) - offsetof(subtitles_t, state) == 44+44+4, + "invalid runtime state layout"); + +#define PTR_DECODE(sub, ptr) ((void*)(((uint8_t*)(sub)) + (uint32_t)(ptr))) + +subtitles_t* subtitles_load(const char *fn) { + subtitles_t *sub = asset_load(fn, NULL); + assertf(memcmp(sub->magic, "SUB64", 5) == 0, "Invalid subtitle file: %s", fn); + assertf(sub->version == 1, "Unsupported subtitle version %d in file: %s", sub->version, fn); + + for (int i=0; inum_syncs; i++) { + seek_entry_t *entry = &sub->seek_entries[i]; + entry->deltas = PTR_DECODE(sub, entry->deltas); + entry->opcodes = PTR_DECODE(sub, entry->opcodes); + entry->txts = PTR_DECODE(sub, entry->txts); + } + + subtitles_seek(sub, 0); + return sub; +} + +void subtitles_free(subtitles_t *sub) { + free(sub); +} + +static void parse_next(sub_state_t *state) +{ + do { + state->begin_frame_idx += __read_varint_u64(&state->cur_delta); + sub64_opcode_t opcode = *state->cur_opcode++; + + switch (opcode) { + case SUB64_OP_SHOW_BOTTOM: + case SUB64_OP_SHOW_TOP: + case SUB64_OP_SHOW_CENTER: { + sub64_region_t region = (sub64_region_t)(opcode - SUB64_OP_SHOW); + state->cues[region].text = state->cur_txt; + // Advance cur_txt to next NUL + while (*state->cur_txt++ != '\0') {} + state->cues[region].region_hint = (int8_t)region; + state->cues[region].visible = true; + break; + } + case SUB64_OP_HIDE_BOTTOM: + case SUB64_OP_HIDE_TOP: + case SUB64_OP_HIDE_CENTER: { + sub64_region_t region = (sub64_region_t)(opcode - SUB64_OP_HIDE); + state->cues[region].visible = false; + break; + } + case SUB64_OP_CLEAR: { + for (int i=0; i<3; i++) { + state->cues[i].visible = false; + } + break; + } + default: + assertf(0, "Unknown subtitle opcode: 0x%02X", opcode); + } + } while (__peek_varint_u64(state->cur_delta) == 0); + + state->end_frame_idx = state->begin_frame_idx + __peek_varint_u64(state->cur_delta); +} + +void subtitles_next_frame(subtitles_t *sub) +{ + // End-of-stream guard + if (sub->cur_frame_idx >= (int)sub->num_frames - 1) { + return; + } + + sub->cur_frame_idx++; + if (sub->cur_frame_idx >= sub->state.end_frame_idx) { + parse_next(&sub->state); + assertf(sub->cur_frame_idx >= sub->state.begin_frame_idx && sub->cur_frame_idx < sub->state.end_frame_idx, + "Subtitle state frame indices invalid after parsing: cur=%d, begin=%d, end=%d", + sub->cur_frame_idx, sub->state.begin_frame_idx, sub->state.end_frame_idx); + + debugf("Subtitles advanced to frame %d: begin_frame_idx=%d, end_frame_idx=%d\n", + sub->cur_frame_idx, sub->state.begin_frame_idx, sub->state.end_frame_idx); + for (int i=0; i<3; i++) { + if (sub->state.cues[i].visible) + debugf(" Region %d: visible=%d, text=\"%s\"\n", + i, sub->state.cues[i].visible, sub->state.cues[i].text ? sub->state.cues[i].text : ""); + } + } +} + +int subtitles_get_current_cues(subtitles_t *sub, subtitle_cue_t *cues, int max_cues) +{ + int count = 0; + for (int i=0; i<3 && count < max_cues; i++) { + if (sub->state.cues[i].visible) { + cues[count++] = sub->state.cues[i]; + } + } + return count; +} + +void subtitles_seek(subtitles_t *sub, int frame_idx) +{ + frame_idx = CLAMP(frame_idx, 0, (int)sub->num_frames - 1); + + for (int i=0; inum_syncs; i++) + debugf(" Seek entry %d: sync_frame=%ld\n", i, sub->seek_entries[i].sync_frame); + + // Find the closest seek entry via binary search + int lo = 0, hi = sub->num_syncs; + while (lo < hi) { + int mid = lo + (hi - lo) / 2; + if ((int)sub->seek_entries[mid].sync_frame <= frame_idx) + lo = mid + 1; + else + hi = mid; + } + int seek_idx = lo - 1; + if (seek_idx < 0) seek_idx = 0; + + // Initialize state from the seek entry. All seek entries are + // guaranteed to have zero active cues, so that the state is well-defined. + seek_entry_t *entry = &sub->seek_entries[seek_idx]; + sub->state = (sub_state_t){ + .cur_delta = entry->deltas, + .cur_opcode = entry->opcodes, + .cur_txt = entry->txts, + .begin_frame_idx = (int)entry->sync_frame, + .end_frame_idx = (int)entry->sync_frame + __peek_varint_u64(entry->deltas), + }; + + debugf("Subtitles seek to frame %d using seek entry %d (sync_frame=%ld)\n", + frame_idx, seek_idx, entry->sync_frame); + debugf(" Initial state: begin_frame_idx=%d, end_frame_idx=%d\n", + sub->state.begin_frame_idx, sub->state.end_frame_idx); + + // Parse states until we reach the desired frame. We stop once we + // reach a state whose range contains frame_idx. + while (sub->state.end_frame_idx <= frame_idx) { + parse_next(&sub->state); + } + + // Set current frame index + sub->cur_frame_idx = frame_idx; + + debugf(" Final state: begin_frame_idx=%d, end_frame_idx=%d, cur_frame_idx=%d\n", + sub->state.begin_frame_idx, sub->state.end_frame_idx, sub->cur_frame_idx); +} + +typedef struct subrenderer_s subrenderer_t; + +typedef struct subrenderer_s { + int canvas_width; + int canvas_height; + void (*render)(subrenderer_t *renderer, subtitle_cue_t *cues, int num_cues); + void (*free)(subrenderer_t *renderer); +} subrenderer_t; + + +typedef struct { + subrenderer_t base; + const char *par_cue[3]; + rspq_block_t *pars[3]; + subrenderer_rdpq_parms_t parms; + rdpq_font_t *fonts[3]; // 0=normal, 1=bold, 2=italic +} subrenderer_rdpq_t; + + +static void subrenderer_rdpq_render(subrenderer_t* base, subtitle_cue_t *cues, int num_cues) +{ + subrenderer_rdpq_t *sr = (subrenderer_rdpq_t*)base; + int canvas_width = sr->base.canvas_width ? sr->base.canvas_width : display_get_width(); + int canvas_height = sr->base.canvas_height ? sr->base.canvas_height : display_get_height(); + + const int WIDTH_MARGIN = 10; + + int x0 = WIDTH_MARGIN; + int y0s[3] = { + canvas_height - 60, // bottom + 10, // top + canvas_height / 2 - 40 // center + }; + rdpq_valign_t valigns[3] = { + VALIGN_BOTTOM, + VALIGN_TOP, + VALIGN_CENTER + }; + + rdpq_textparms_t textparms = { + .width = canvas_width - 2 * WIDTH_MARGIN, + .height = canvas_height / 4, + .align = ALIGN_CENTER, + .wrap = WRAP_WORD, + .char_spacing = 1, + .disable_aa_fix = true, + }; + + for (int i=0; ivisible) continue; + textparms.valign = valigns[cue->region_hint]; + int y0 = y0s[cue->region_hint]; + + if (sr->par_cue[cue->region_hint] != cue->text) { + sr->par_cue[cue->region_hint] = cue->text; + if (sr->pars[cue->region_hint]) { + rdpq_call_deferred((void(*)(void*))rspq_block_free, sr->pars[cue->region_hint]); + sr->pars[cue->region_hint] = NULL; + } + + rspq_block_begin(); + + int nbytes = strlen(cue->text); + rdpq_paragraph_t *par = rdpq_paragraph_build( + &textparms, 1, cue->text, &nbytes); + + if (sr->parms.bkg_color.a > 0) { + // Draw background box + if (sr->parms.bkg_color.a == 255) { + rdpq_set_fill_color(sr->parms.bkg_color); + } else { + rdpq_set_mode_standard(); + rdpq_set_prim_color(sr->parms.bkg_color); + rdpq_mode_combiner(RDPQ_COMBINER_FLAT); + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + } + + debugf("Bounding box for subtitle region %d: " + "x0=%f, y0=%f, x1=%f, y1=%f\n", + cue->region_hint, + par->bbox.x0, + par->bbox.y0, + par->bbox.x1, + par->bbox.y1 + ); + debugf("Drawing subtitle background box at region %d: " + "x0=%d, y0=%d, x1=%d, y1=%d\n", + cue->region_hint, + (int)(x0 - 5 + par->bbox.x0), + (int)(y0 - 5 + par->bbox.y0), + (int)(x0 + 5 + par->bbox.x1), + (int)(y0 + 5 + par->bbox.y1) + ); + + rdpq_fill_rectangle( + x0 - 5 + par->bbox.x0, + y0 - 5 + par->bbox.y0, + x0 + 5 + par->bbox.x1, + y0 + 5 + par->bbox.y1 + ); + } + + rdpq_paragraph_render(par, x0, y0); + rdpq_paragraph_free(par); + sr->pars[cue->region_hint] = rspq_block_end(); + } + + rspq_block_run(sr->pars[cue->region_hint]); + } +} + +static void subrenderer_rdpq_free(subrenderer_t* base) { + subrenderer_rdpq_t *renderer = (subrenderer_rdpq_t*)base; + for (int i = 0; i < 3; i++) { + if (renderer->fonts[i]) { + rdpq_text_unregister_font(i+1); + rdpq_font_free(renderer->fonts[i]); + } + } +} + +subrenderer_t* subrenderer_create_rdpq(subrenderer_rdpq_parms_t *parms) +{ + subrenderer_rdpq_t *renderer = malloc(sizeof(subrenderer_rdpq_t)); + assertf(renderer, "Out of memory"); + memset(renderer, 0, sizeof(subrenderer_rdpq_t)); + if (parms) renderer->parms = *parms; + + renderer->base.render = subrenderer_rdpq_render; + renderer->base.free = subrenderer_rdpq_free; + + for (int i = 0; i < 3; i++) { + if (renderer->parms.fonts[i]) { + renderer->fonts[i] = rdpq_font_load(renderer->parms.fonts[i]); + } else if (i == 0) { + // Load default font for normal text if not specified + renderer->fonts[i] = rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_VAR); + } + + // Register the font (or fallback to normal) + if (!renderer->fonts[i]) { + rdpq_text_register_font(i+1, renderer->fonts[i]); + } else { + rdpq_text_register_font(i+1, renderer->fonts[0]); + } + } + + return &renderer->base; +} + +void subrenderer_set_frame_size(subrenderer_t *base, int width, int height) { + base->canvas_width = width; + base->canvas_height = height; +} + +void subrenderer_render(subrenderer_t *renderer, subtitle_cue_t *cues, int num_cues) { + renderer->render(renderer, cues, num_cues); +} + +void subrenderer_free(subrenderer_t *renderer) { + renderer->free(renderer); + free(renderer); +} + diff --git a/src/video/video.c b/src/video/video.c new file mode 100644 index 000000000..50650ae3d --- /dev/null +++ b/src/video/video.c @@ -0,0 +1,202 @@ +/** + * @file video.c + * @author Giovanni Bajo + * @brief Video player subsystem + * + * + */ + +#include "video.h" +#include "video_internal.h" +#include "debug.h" +#include "asset.h" +#include + +static video_codec_t *registered_codecs = NULL; + +typedef struct video_seektable_s { + char magic[3]; ///< "VSK" magic + uint8_t version; ///< Version of the seek table format + int num_entries; ///< Number of entries in the seek table + uint32_t *file_offsets; ///< Array of file offsets for keyframes + int *frame_idx; ///< Array of frame indices for keyframes + uint32_t mean_offset; ///< Mean of the offsets deltas + uint32_t mean_frame; ///< Mean of the frame deltas +} video_seektable_t; + +/** @brief Decode a pointer relative to object start */ +#define PTR_DECODE(font, ptr) ((void*)(((uint8_t*)(font)) + (uint32_t)(ptr))) + +static video_seektable_t *video_seektable_load(const char *fn) +{ + video_seektable_t *tbl = asset_load(fn, NULL); + assertf(memcmp(tbl->magic, "VSK", 3) == 0, "Invalid seek table file: %s", fn); + assertf(tbl->version == 2, "Unsupported seek table version %d in file: %s", tbl->version, fn); + + tbl->file_offsets = PTR_DECODE(tbl, tbl->file_offsets); + tbl->frame_idx = PTR_DECODE(tbl, tbl->frame_idx); + + // Decode residuals to absolute values. The file stores residuals from the mean + // of the deltas to improve compressibility, but we need absolute values at runtime. + for (int i=1; i< tbl->num_entries; i++) { + tbl->file_offsets[i] += tbl->file_offsets[i-1] + tbl->mean_offset; + tbl->frame_idx[i] += tbl->frame_idx[i-1] + tbl->mean_frame; + } + + return tbl; +} + +/** + * @brief Lookup the seektable for a specified frame. + * + * This function performs a binary search on the seek table to find the + * file offset corresponding to the specified frame index. If the exact frame + * index is not found, it returns the file offset of the closest preceding + * keyframe. + * + * @param tbl Pointer to the seek table + * @param frame_idx Frame index to seek to. On return, it contains the actual + * frame index found in the seek table (which might be + * different from the requested one). + * @return File offset of the keyframe + */ +static uint32_t video_seektable_lookup(video_seektable_t *tbl, int *frame_idx) +{ + int left = 0; + int right = tbl->num_entries - 1; + int result_idx = -1; + + while (left <= right) { + int mid = left + (right - left) / 2; + if (tbl->frame_idx[mid] == *frame_idx) { + result_idx = mid; + break; + } else if (tbl->frame_idx[mid] < *frame_idx) { + result_idx = mid; // Keep track of the closest preceding keyframe + left = mid + 1; + } else { + right = mid - 1; + } + } + + if (result_idx >= 0) { + *frame_idx = tbl->frame_idx[result_idx]; + return tbl->file_offsets[result_idx]; + } else { + // No valid keyframe found; return start of the file + *frame_idx = 0; + return 0; + } +} + +static void video_seektable_free(video_seektable_t *tbl) +{ + free(tbl); +} + +void video_register_codec(video_codec_t *codec) +{ + if (codec->next_codec != NULL) { + debugf("video_register_codec: codec for %s already registered\n", codec->extension); + return; + } + codec->next_codec = registered_codecs; + registered_codecs = codec; +} + +video_t* video_open(const char *fn, const video_parms_t *parms) +{ + video_codec_t *codec = registered_codecs; + + const char *ext = strrchr(fn, '.'); + assertf(ext, "File %s has no extension", fn); + + while (codec) { + if (strcmp(codec->extension, ext) == 0) { + video_t *v = codec->open(fn, parms); + if (v) { + v->codec = codec; + + // If the codec support fast seeking, check if there is a seek + // table for this video + if (v->codec->seekfast) { + int fnlen = strlen(fn), extlen = strlen(ext); + char *seekfn = alloca(fnlen - extlen + strlen(".seek") + 1); + strcpy(seekfn, fn); + strcpy(seekfn + (fnlen - extlen), ".seek"); + + struct stat st; + if (stat(seekfn, &st) == 0) { + v->seektable = video_seektable_load(seekfn); + } + } + } + return v; + } + codec = codec->next_codec; + } + + assertf(false, "No registered codec found for video file: %s", fn); + return NULL; +} + +void video_close(video_t *v) +{ + if (v->seektable) { + video_seektable_free(v->seektable); + v->seektable = NULL; + } + v->codec->close(v); +} + +video_info_t video_get_info(video_t *v) +{ + return v->info; +} + +int video_poll(video_t *v) +{ + if (!v->codec->poll) return 0; + return v->codec->poll(v); +} + +bool video_next_frame(video_t *v) +{ + return v->codec->next_frame(v); +} + +yuv_frame_t video_get_frame(video_t *v) +{ + return v->codec->get_frame(v); +} + +void video_rewind(video_t *v) +{ + v->codec->rewind(v); +} + +int video_seek(video_t *v, int frame_idx) +{ + if (v->seektable) { + int keyframe_idx = frame_idx; + uint32_t keyframe_off = video_seektable_lookup(v->seektable, &keyframe_idx); + v->codec->seekfast(v, keyframe_idx, keyframe_off); + return keyframe_idx; + } + + if (v->codec->seek) { + return v->codec->seek(v, frame_idx); + } + + return -1; +} + +bool video_is_seekable(video_t *v, int frame_idx) +{ + if (!v->seektable) return false; + + int found = frame_idx; + video_seektable_lookup(v->seektable, &found); + return found == frame_idx; +} + diff --git a/src/video/video_internal.h b/src/video/video_internal.h new file mode 100644 index 000000000..0a74176bc --- /dev/null +++ b/src/video/video_internal.h @@ -0,0 +1,41 @@ +/** + * @file video_internal.h + * @author Giovanni Bajo + * @brief Video player subsystem + * + * + */ + +#ifndef LIBDRAGON_VIDEO_INTERNAL_H +#define LIBDRAGON_VIDEO_INTERNAL_H + +#include "video.h" + +///@cond +typedef struct video_seektable_s video_seektable_t; +///@endcond + +/** @brief Basic video structure */ +typedef struct video_s { + video_info_t info; ///< Video information + video_codec_t *codec; ///< Codec used to decode this video + video_seektable_t *seektable; ///< Optional seek table for fast seeking +} video_t; + +/** @brief Video codec structure */ +typedef struct video_codec_s { + const char *extension; ///< File extension handled by this codec + video_t* (*open)(const char *fn, const video_parms_t *parms); ///< Open a video file + void (*close)(video_t *v); ///< Close a video file + int (*poll)(video_t *v); ///< Decode a bit + bool (*next_frame)(video_t *v); ///< Advance to the next frame + yuv_frame_t (*get_frame)(video_t *v); ///< Get the current frame + void (*rewind)(video_t *v); ///< Rewind the video to the beginning + void (*seekfast)(video_t *v, int frame_idx, uint32_t offset); ///< Seek to a specific keyframe + int (*seek)(video_t *v, int frame_idx); ///< Seek to a specific frame index + + struct video_codec_s* next_codec; ///< Next registered codec +} video_codec_t; + + +#endif diff --git a/src/video/video_sync.c b/src/video/video_sync.c new file mode 100644 index 000000000..d2cb0b663 --- /dev/null +++ b/src/video/video_sync.c @@ -0,0 +1,187 @@ +/** + * @file video_sync.c + * @author Giovanni Bajo + * @brief Video sync helper (frameskipping/seek decisions driven by a master clock) + * @ingroup video + */ + +#include "video_sync.h" + +#include "debug.h" +#include "utils.h" + +#include +#include +#include +#include + +typedef struct video_sync_s { + video_t *v; + float framerate; + video_sync_params_t p; + + // Smoothed lag (in frames) + float lag_filt; + // Skip budget (in frames) + float skip_budget; + + double last_master_time; + double last_seek_time; +} video_sync_t; + +// Internal constants +static const float VIDEO_SYNC_DEADZONE_SEC = 0.05f; +static const float VIDEO_SYNC_LAG_SMOOTH_TAU_SEC = 0.20f; +static const float VIDEO_SYNC_SEEK_COOLDOWN_SEC = 0.25f; + +static video_sync_params_t video_sync_default_params(void) +{ + return (video_sync_params_t){ + .recovery_time_sec = 1.0f, + .keyframe_window_sec = 0.20f, + .max_lag_sec = 0.75f, + }; +} + +video_sync_t* video_sync_create(video_t *v, const video_sync_params_t *p) +{ + video_sync_t *s = calloc(1, sizeof(video_sync_t)); + assertf(s, "Out of memory"); + + s->v = v; + s->framerate = video_get_info(v).framerate; + s->p = video_sync_default_params(); + if (p) { + if (p->recovery_time_sec != 0.0f) s->p.recovery_time_sec = p->recovery_time_sec; + if (p->keyframe_window_sec != 0.0f) s->p.keyframe_window_sec = p->keyframe_window_sec; + if (p->max_lag_sec != 0.0f) s->p.max_lag_sec = p->max_lag_sec; + } + + // Sanitize parameters + if (s->p.recovery_time_sec <= 0.0f) s->p.recovery_time_sec = video_sync_default_params().recovery_time_sec; + // Negative values explicitly disable the behavior. + if (s->p.keyframe_window_sec < 0.0f) s->p.keyframe_window_sec = 0.0f; + if (s->p.max_lag_sec < 0.0f) s->p.max_lag_sec = 0.0f; + + video_sync_reset(s, 0); + return s; +} + +void video_sync_destroy(video_sync_t *s) +{ + free(s); +} + +void video_sync_reset(video_sync_t *s, int frame_idx) +{ + s->lag_filt = 0.0f; + s->skip_budget = 0.0f; + s->last_master_time = 0.0; + s->last_seek_time = -1e30; +} + +static inline int frames_from_sec(float framerate, float sec) +{ + if (sec <= 0) return 0; + return (int)floorf(sec * framerate + 0.5f); +} + +static inline float ema_alpha_from_dt_tau(float dt, float tau) +{ + // 1-pole low-pass alpha derived from time constant tau (stable across dt). + // alpha = 1 - exp(-dt/tau) + if (tau <= 0.0f) return 1.0f; + if (dt <= 0.0f) return 0.0f; + float a = 1.0f - expf(-dt / tau); + return CLAMP(a, 0.0f, 1.0f); +} + +video_sync_action_t video_sync_step(video_sync_t *s, double master_time_sec, int cur_frame_idx) +{ + video_sync_action_t act = { .kind = VIDEO_SYNC_RENDER_NEXT, .seek_frame = -1 }; + assert(master_time_sec >= 0); + assertf(master_time_sec >= s->last_master_time, "Master time went backwards"); + + // Compute time delta in frames, clamped to avoid explosions + float dt_frames = (master_time_sec - s->last_master_time) * s->framerate; + dt_frames = CLAMP(dt_frames, 0.0f, 5.0f); + s->last_master_time = master_time_sec; + + // Convert master time to target frame index. We want floor here to get the + // last frame that should have been rendered by now. Nonetheless, we add + // 1e-9 to avoid treating 123.99999999999997 as 123. + int target_frame = (int)floor(master_time_sec * (double)s->framerate + 1e-9); + + // Compute how much we are behind the target frame. + int lag_frames = target_frame - cur_frame_idx; + if (lag_frames <= 0) { + // If we're not behind, do not accumulate budget. + s->skip_budget = 0.0f; + s->lag_filt = 0.0f; + return act; + } + + // Smooth lag in frames + float lag = (float)lag_frames; + float smooth_alpha = ema_alpha_from_dt_tau(dt_frames / s->framerate, VIDEO_SYNC_LAG_SMOOTH_TAU_SEC); + s->lag_filt = s->lag_filt + smooth_alpha * (lag - s->lag_filt); + + // Ignore lag smaller than the deadzone. + int deadzone_frames = frames_from_sec(s->framerate, VIDEO_SYNC_DEADZONE_SEC); + float lag_over = s->lag_filt - (float)deadzone_frames; + if (lag_over < 0.0f) lag_over = 0.0f; + + float recovery_window_frames = s->framerate * s->p.recovery_time_sec; + if (recovery_window_frames < 1.0f) recovery_window_frames = 1.0f; + + // Accumulate budget softly: recover lag over recovery_time_sec (approximately). + s->skip_budget += dt_frames * (lag_over / recovery_window_frames); + s->skip_budget = CLAMP(s->skip_budget, 0.0f, 1024.0f); + if (s->skip_budget < 1.0f) { + // Not enough budget to skip anything yet. + return act; + } + + // Seek cooldown (best-effort on master time) + bool seek_allowed = (master_time_sec - s->last_seek_time) >= (double)VIDEO_SYNC_SEEK_COOLDOWN_SEC; + if (seek_allowed) { + + // If we are too far behind (more than max_lag_sec), seek to the target frame. + // This is a best-effort to catch up, but it is not guaranteed to succeed. + int max_lag_frames = frames_from_sec(s->framerate, s->p.max_lag_sec); + if (max_lag_frames > 0 && lag_frames >= max_lag_frames) { + act.kind = VIDEO_SYNC_SEEK_AND_RENDER; + act.seek_frame = target_frame; + // Budget is considered spent by the seek; clamp to zero. + s->skip_budget = 0.0f; + s->last_seek_time = master_time_sec; + return act; + } + + // Keyframe-aware seek in a small window (only if it helps and budget can pay it) + int window_frames = frames_from_sec(s->framerate, s->p.keyframe_window_sec); + window_frames = MIN(window_frames, lag_frames); + window_frames = MIN(window_frames, 240); + if (window_frames > 0) { + // Try to find a keyframe within the window (and within the budget) + int kmax = MIN(window_frames, (int)floorf(s->skip_budget)); + for (int k = 1; k <= kmax; k++) { + if (video_is_seekable(s->v, cur_frame_idx + k)) { + // Found a keyframe within the window and within the budget: seek to it. + act.kind = VIDEO_SYNC_SEEK_AND_RENDER; + act.seek_frame = cur_frame_idx + k; + s->skip_budget -= (float)k; + if (s->skip_budget < 0.0f) s->skip_budget = 0.0f; + s->last_seek_time = master_time_sec; + return act; + } + } + } + } + + // Soft skip (drop one frame) + act.kind = VIDEO_SYNC_SKIP_NEXT; + s->skip_budget -= 1.0f; + if (s->skip_budget < 0.0f) s->skip_budget = 0.0f; + return act; +} diff --git a/src/video/yuv.c b/src/video/yuv.c new file mode 100644 index 000000000..4e4daac8a --- /dev/null +++ b/src/video/yuv.c @@ -0,0 +1,478 @@ +/** + * @file yuv.c + * @author Giovanni Bajo + * @brief Hardware accelerated YUV conversion + * @ingroup video + */ + +#include "yuv.h" +#include "yuv_internal.h" +#include "rsp.h" +#include "rdpq.h" +#include "rdpq_tex.h" +#include "../rdpq/rdpq_tex_internal.h" +#include "rdpq_mode.h" +#include "rdpq_rect.h" +#include "rdpq_debug.h" +#include "rspq.h" +#include "n64sys.h" +#include "debug.h" +#include "utils.h" +#include + +/** @brief Internal buffer used to interleave U and V components */ +static surface_t internal_buffer; + +// Calculated with: yuv_new_colorspace(0.299, 0.114, 16, 219, 224); +const yuv_colorspace_t YUV_BT601_TV = { + .c0=1.16895, .c1=1.60229, .c2=-0.393299, .c3=-0.816156, .c4=2.02514, .y0=16, + .k0=175, .k1=-43, .k2=-89, .k3=222, .k4=111, .k5=43 +}; + +// Calculated with: yuv_new_colorspace(0.299, 0.114, 0, 256, 256); +const yuv_colorspace_t YUV_BT601_FULL = { + .c0=1, .c1=1.402, .c2=-0.344136, .c3=-0.714136, .c4=1.772, .y0=0, + .k0=179, .k1=-44, .k2=-91, .k3=227, .k4=0, .k5=0 +}; + +// Calculated with: yuv_new_colorspace(0.2126, 0.0722, 16, 219, 224); +const yuv_colorspace_t YUV_BT709_TV = { + .c0=1.16895, .c1=1.79977, .c2=-0.214085, .c3=-0.534999, .c4=2.12069, .y0=16, + .k0=197, .k1=-23, .k2=-59, .k3=232, .k4=111, .k5=43 +}; + +// Calculated with: yuv_new_colorspace(0.2126, 0.0722, 0, 256, 256); +const yuv_colorspace_t YUV_BT709_FULL = { + .c0=1, .c1=1.5748, .c2=-0.187324, .c3=-0.468124, .c4=1.8556, .y0=0, + .k0=202, .k1=-24, .k2=-60, .k3=238, .k4=0, .k5=0 +}; + + +static void resize_internal_buffer(int w, int h) +{ + if (internal_buffer.width != w || internal_buffer.height != h) { + surface_free(&internal_buffer); + internal_buffer = surface_alloc(FMT_IA16, w, h); + } +} + +static void yuv_assert_handler(rsp_snapshot_t *state, uint16_t code) { + switch (code) { + case ASSERT_INVALID_INPUT_Y: + printf("Input buffer for Y plane was not configured\n"); + break; + case ASSERT_INVALID_INPUT_CB: + printf("Input buffer for CB plane was not configured\n"); + break; + case ASSERT_INVALID_INPUT_CR: + printf("Input buffer for CR plane was not configured\n"); + break; + case ASSERT_INVALID_OUTPUT: + printf("Output buffer was not configured\n"); + break; + } +} + +static int ovl_yuv; +DEFINE_RSP_UCODE(rsp_yuv, + .assert_handler = yuv_assert_handler); + +#define CMD_YUV_SET_INPUT 0x0 +#define CMD_YUV_SET_OUTPUT 0x1 +#define CMD_YUV_INTERLEAVE4_32X16 0x2 +#define CMD_YUV_INTERLEAVE2_32X16 0x3 + +static int8_t yuv_initialized = 0; + +void yuv_init(void) +{ + if (yuv_initialized == 0) { + rspq_init(); + ovl_yuv = rspq_overlay_register(&rsp_yuv); + } + yuv_initialized++; +} + +void yuv_close(void) +{ + assert(yuv_initialized > 0); + yuv_initialized--; + if (yuv_initialized == 0) { + surface_free(&internal_buffer); + rspq_overlay_unregister(ovl_yuv); + } +} + +yuv_colorspace_t yuv_new_colorspace(float kr, float kb, int y0i, int yrangei, int crangei) +{ + yuv_colorspace_t cs; + // Matrix from: https://en.wikipedia.org/wiki/YCbCr#YCbCr + float kg = 1.0f - kr - kb; + float m[3][3] = { + { kr, kg, kb, }, + { -0.5f*kr/(1.0f-kb), -0.5f*kg/(1.0f-kb), 0.5f, }, + { 0.5f, -0.5f*kg/(1.0f-kr), -0.5f*kb/(1.0f-kr) }, + }; + + // Invert matrix + float idet = 1.0f / + (m[0][0] * (m[1][1] * m[2][2] - m[2][1] * m[1][2]) - + m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])); + float im[3][3] = { + {(m[1][1] * m[2][2] - m[2][1] * m[1][2]) * idet, + (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * idet, + (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * idet}, + {(m[1][2] * m[2][0] - m[1][0] * m[2][2]) * idet, + (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * idet, + (m[1][0] * m[0][2] - m[0][0] * m[1][2]) * idet}, + {(m[1][0] * m[2][1] - m[2][0] * m[1][1]) * idet, + (m[2][0] * m[0][1] - m[0][0] * m[2][1]) * idet, + (m[0][0] * m[1][1] - m[1][0] * m[0][1]) * idet} + }; + + // Bring range arguments into 0..1 range + float y0 = y0i * (1.0f / 255.0f); + float yrange = 256.0f / yrangei; + float crange = 256.0f / crangei; + + // Using im, we can convert YUV to RGB using a standard + // matrix multiplication: + // + // RGB = YUV * im + // + // Fortunately, most elements of the matrix are 0, so we + // can save a few multiplications and end up with this + // formula: + // + // Which simplify our formula: + // + // R = C0 * Y + C1*V + // G = C0 * Y + C2*U + C3*V + // B = C0 * Y + C4*U + // + // This does not take the range into account. To do so, + // we can adjust Y by y0, and then pre-multiply yrange + // into C0, and crange into C1..C4. The final + // formula will be: + // + // R = C0 * (Y-y0) + C1*V + // G = C0 * (Y-y0) + C2*U + C3*V + // B = C0 * (Y-y0) + C4*U + // + // which is the one used by #yuv_to_rgb. + // + cs.c0 = im[0][0] * yrange; + cs.c1 = im[0][2] * crange; + cs.c2 = im[1][1] * crange; + cs.c3 = im[1][2] * crange; + cs.c4 = im[2][1] * crange; + cs.y0 = y0i; + + // Now calculate the RDP coefficients. + // The RDP cannot do exactly this formula. What the RDP does is + // slightly different, and it does it in two steps. The first step is + // the texture filter, which calculates: + // + // TF_R = Y + K0*V + // TF_G = Y + K1*U + K2*V + // TF_B = Y + K3*U + // + // The second step is the color combiner, which will use the following + // formula: + // + // R = (TF_R - K4) * K5 + TF_R = (TF_R - (K4*K5)/(1+K5)) * (1+K5) + // G = (TF_G - K4) * K5 + TF_G = (TF_G - (K4*K5)/(1+K5)) * (1+K5) + // B = (TF_B - K4) * K5 + TF_B = (TF_B - (K4*K5)/(1+K5)) * (1+K5) + // + // By concatenating the two steps, we find: + // + // R = (Y + K0*V - (K4*K5)/(1+K5))) * (1+K5) + // G = (Y + K1*U + K2*V - (K4*K5)/(1+K5))) * (1+K5) + // B = (Y + K3*U - (K4*K5)/(1+K5))) * (1+K5) + // + // So let's now compare this with the standard formula above. We need to find + // a way to express K0..K5 in terms of C0..C4 (plus y0). Let's take + // the standard formula and factor C0: + // + // R = (Y - y0 + C1*V/C0) * C0 + // G = (Y - y0 + C2*U/C0 + C3*V/C0) * C0 + // B = (Y - y0 + C4*U/C0) * C0 + // + // We can now derive all coefficients: + // + // 1+K5 = C0 => K5 = C0 - 1 + // (K4*K5)/(1+K5) = y0 => K4 = (y0 * (1+K5)) / K5) = y0/K5 + y0 + // + // K0 = C1 / C0 + // K1 = C2 / C0 + // K2 = C3 / C0 + // K3 = C4 / C0 + // + float ic0 = 1.0f / cs.c0; + float k5 = cs.c0 - 1; + float k4 = k5 != 0 ? y0 / k5 + y0 : 0; + float k0 = cs.c1 * ic0; + float k1 = cs.c2 * ic0; + float k2 = cs.c3 * ic0; + float k3 = cs.c4 * ic0; + cs.k0 = roundf(k0*128.f); + cs.k1 = roundf(k1*128.f); + cs.k2 = roundf(k2*128.f); + cs.k3 = roundf(k3*128.f); + cs.k4 = roundf(k4*255.f); + cs.k5 = roundf(k5*255.f); + return cs; +} + +color_t yuv_to_rgb(uint8_t y, uint8_t u, uint8_t v, const yuv_colorspace_t *cs) +{ + float yp = (y - cs->y0) * cs->c0; + float r = yp + cs->c1 * (v-128) + .5f; + float g = yp + cs->c2 * (u-128) + cs->c3 * (v-128) + .5f; + float b = yp + cs->c4 * (u-128) + .5f; + + return (color_t){ + .r = r > 255 ? 255.f : r < 0 ? 0 : r, + .g = g > 255 ? 255.f : g < 0 ? 0 : g, + .b = b > 255 ? 255.f : b < 0 ? 0 : b, + .a = 0xFF, + }; +} + +void rsp_yuv_set_input_buffer(uint8_t *y, uint8_t *cb, uint8_t *cr, int y_pitch) +{ + rspq_write(ovl_yuv, CMD_YUV_SET_INPUT, + PhysicalAddr(y), PhysicalAddr(cb), PhysicalAddr(cr), y_pitch); +} + +void rsp_yuv_set_output_buffer(uint8_t *out, int out_pitch) +{ + rspq_write(ovl_yuv, CMD_YUV_SET_OUTPUT, + PhysicalAddr(out), out_pitch); +} + +void rsp_yuv_interleave4_block_32x16(int x0, int y0) +{ + rspq_write(ovl_yuv, CMD_YUV_INTERLEAVE4_32X16, + (x0<<12) | y0); +} + +void rsp_yuv_interleave2_block_32x16(int x0, int y0) +{ + rspq_write(ovl_yuv, CMD_YUV_INTERLEAVE2_32X16, + (x0<<12) | y0); +} + +static void yuv_tex_blit_setup(surface_t *yp, surface_t *up, surface_t *vp) +{ + assertf(yp->width == up->width*2 && yp->height == up->height*2, + "wrong plane sizes: only YUV 4:2:0 is supported (Y:%dx%d U:%dx%d)", + yp->width, yp->height, up->width, up->height); + assertf(yp->width == vp->width*2 && yp->height == vp->height*2, + "wrong plane sizes: only YUV 4:2:0 is supported (Y:%dx%d V:%dx%d)", + yp->width, yp->height, vp->width, vp->height); + + // Make sure we have the internal buffer ready. We will interleave U and V + // planes so we need a buffer that handles two of those planes at the same time. + resize_internal_buffer(up->width, up->height); + + // Interleave U and V planes into the internal buffer, using RSP + rsp_yuv_set_input_buffer(yp->buffer, up->buffer, vp->buffer, yp->width); + rsp_yuv_set_output_buffer(internal_buffer.buffer, internal_buffer.stride); + assert((yp->height % 16) == 0 && (yp->width % 32) == 0); + for (int y=0; y < yp->height; y += 16) { + for (int x=0; x < yp->width; x += 32) { + // FIXME: for now this only works with subsampling 4:2:0 + rsp_yuv_interleave2_block_32x16(x, y); + } + rspq_flush(); + } + + // Setup the two buffers as RDP lookup addresses, that will be referenced + // later. This way, we can compile yuv_tex_blit_run in a block. + rdpq_set_lookup_address(1, yp->buffer); + rdpq_set_lookup_address(2, internal_buffer.buffer); +} + +static void yuv_tex_blit_run(int width, int height, float x0, float y0, + const rdpq_blitparms_t *parms, const yuv_colorspace_t *cs, bool enable_dithering) +{ + rdpq_set_mode_yuv(true); + if (cs) rdpq_set_yuv_parms(cs->k0, cs->k1, cs->k2, cs->k3, cs->k4, cs->k5); + rdpq_mode_filter(FILTER_BILINEAR); + + if (enable_dithering) + rdpq_mode_dithering(DITHER_SQUARE_SQUARE); + + // To avoid the need of pre-interleaving Y and UV together, we load them + // separately into TMEM using separate LOAD_BLOCK commands. + + // Tiles used to draw the two lines onto the screen. Notice that the second + // line will not be preswapped, so we cannot use a single tile for both + rdpq_set_tile(TILE0, FMT_YUV16, 0, 0, NULL); + rdpq_set_tile(TILE1, FMT_YUV16, width, 0, NULL); + + // Tiles used to load the UV lines from the internal buffer into TMEM. We + // load the first line at offset 0 in TMEM, and the second line immediately + // after (after "width" texels). + rdpq_set_tile(TILE4, FMT_IA16, 0, 0, NULL); + rdpq_set_tile(TILE5, FMT_IA16, width, 0, NULL); + + // Tile used to load the Y line from the Y buffer into TMEM. The Y texels + // are stored in the upper half of TMEM, so we need to load them at offset + // 2048. + rdpq_set_tile(TILE6, FMT_I8, 2048, 0, NULL); + + surface_t yp = surface_make_placeholder_linear(1, FMT_I8, width, height); + surface_t uvp = surface_make_placeholder_linear(2, FMT_IA16, width/2, height/2); + + void ltd_yuv2(rdpq_tile_t tile, const surface_t *_, int s0, int t0, int s1, int t1, + void (*draw_cb)(rdpq_tile_t tile, int s0, int t0, int s1, int t1), bool filtering) + { + for (int y=t0; yy, &frame->u, &frame->v); + yuv_tex_blit_run(frame->y.width, frame->y.height, x0, y0, parms, cs, false); +} + +yuv_blitter_t yuv_blitter_new(int video_width, int video_height, float x0, float y0, const rdpq_blitparms_t *parms, + const yuv_colorspace_t *cs) +{ + assertf(yuv_initialized, "yuv not initialized, call yuv_init() first"); + // Compile the yuv_tex_blit_run into a block with the given parameters. + rspq_block_begin(); + yuv_tex_blit_run(video_width, video_height, x0, y0, parms, cs, false); + rspq_block_t *block = rspq_block_end(); + return (yuv_blitter_t){ + .block = block, + }; +} + +yuv_blitter_t yuv_blitter_new_fmv(int video_width, int video_height, + int screen_width, int screen_height, const yuv_fmv_parms_t *parms) +{ + assertf(yuv_initialized, "yuv not initialized, call yuv_init() first"); + static const yuv_fmv_parms_t default_parms = {0}; + if (!parms) parms = &default_parms; + + // A fully-zero background color is the default value of the structure, so + // interpret it as opaque black. + color_t bkg_color = parms->bkg_color; + if (color_to_packed32(bkg_color) == 0) + bkg_color = RGBA32(0, 0, 0, 255); + + int final_width, final_height; + if (parms->zoom == YUV_ZOOM_NONE) { + final_width = video_width; + final_height = video_height; + } else if (parms->zoom == YUV_ZOOM_FULL) { + final_width = screen_width; + final_height = screen_height; + } else { + /* YUV_ZOOM_KEEP_ASPECT: fit video (aspect V) inside display (aspect D) with letterbox/pillarbox */ + float V = parms->video_aspect_ratio > 0 ? parms->video_aspect_ratio + : (float)video_width / (float)video_height; + float D = parms->display_aspect_ratio > 0 ? parms->display_aspect_ratio : (4.0f / 3.0f); + if (V >= D) { + final_width = screen_width; + final_height = (int)(screen_height * D / V + 0.5f); + } else { + final_width = (int)(screen_width * V / D + 0.5f); + final_height = screen_height; + } + } + + float scalew = final_width / (float)video_width; + float scaleh = final_height / (float)video_height; + bool filtering = final_width != video_width || final_height != video_height; + + int x0 = 0, y0 = 0; + if (screen_width) { + switch (parms->halign) { + case YUV_ALIGN_CENTER: x0 = ((screen_width - final_width) / 2); break; + case YUV_ALIGN_MIN: x0 = 0; break; + case YUV_ALIGN_MAX: x0 = (screen_width - final_width); break; + default: assertf(0, "invalid yuv config: halign=%d", parms->halign); + } + } + if (screen_height) { + switch (parms->valign) { + case YUV_ALIGN_CENTER: y0 = ((screen_height - final_height) / 2); break; + case YUV_ALIGN_MIN: y0 = 0; break; + case YUV_ALIGN_MAX: y0 = (screen_height - final_height); break; + default: assertf(0, "invalid yuv config: valign=%d", parms->valign); + } + } + + rspq_block_begin(); + + // Clear the screen. To save fillrate, we just clear the part outside + // of the image that we will draw (if any). + int fw = (int)final_width, fh = (int)final_height; + if (screen_height > fh || screen_width > fw) { + rdpq_set_mode_fill(bkg_color); + if (y0 > 0) + rdpq_fill_rectangle(0, 0, screen_width, y0); + if (y0 + fh < screen_height) + rdpq_fill_rectangle(0, y0 + fh, screen_width, screen_height); + if (x0 > 0) + rdpq_fill_rectangle(0, y0, x0, y0 + fh); + if (x0 + fw < screen_width) + rdpq_fill_rectangle(x0 + fw, y0, screen_width, y0 + fh); + } + + // Do the blit (optionally scaling) + yuv_tex_blit_run(video_width, video_height, x0, y0, &(rdpq_blitparms_t){ + .scale_x = scalew, .scale_y = scaleh, .filtering = filtering, + }, parms->cs, parms->enable_dithering); + + rspq_block_t *block = rspq_block_end(); + return (yuv_blitter_t){ + .block = block, + }; +} + +void yuv_blitter_run(yuv_blitter_t *blitter, yuv_frame_t *frame) +{ + yuv_tex_blit_setup(&frame->y, &frame->u, &frame->v); + rspq_block_run(blitter->block); +} + +void yuv_blitter_free(yuv_blitter_t *blitter) +{ + rspq_block_free(blitter->block); + blitter->block = NULL; +} diff --git a/src/video/yuv_internal.h b/src/video/yuv_internal.h new file mode 100644 index 000000000..92400de8b --- /dev/null +++ b/src/video/yuv_internal.h @@ -0,0 +1,16 @@ +/** + * @file yuv_internal.h + * @author Giovanni Bajo + */ +#ifndef __LIBDRAGON_YUV_INTERNAL_H +#define __LIBDRAGON_YUV_INTERNAL_H + +#define ASSERT_INVALID_INPUT_Y 0x0001 +#define ASSERT_INVALID_INPUT_CB 0x0002 +#define ASSERT_INVALID_INPUT_CR 0x0003 +#define ASSERT_INVALID_OUTPUT 0x0004 + +#define BLOCK_W 32 +#define BLOCK_H 16 + +#endif diff --git a/tests/.gitignore b/tests/.gitignore index 6f7b62607..7a5c9b8e0 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,3 +1,8 @@ filesystem/*.dso filesystem/*.dso.sym -filesystem/*.sprite \ No newline at end of file +filesystem/*.sprite +filesystem/*.mdb +filesystem/texdb/ +filesystem/test_dir/ +filesystem_sf64/ +assets/sf64test.sf2 \ No newline at end of file diff --git a/tests/Makefile b/tests/Makefile index b7e01d876..9bb99210a 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,19 +1,23 @@ BUILD_DIR=build DSO_COMPRESS_LEVEL=0 +LIBDRAGON_PREVIEW = 2 # The test ROM covers the whole library, preview APIs included include $(N64_INST)/include/n64.mk -all: testrom.z64 testrom_emu.z64 +N64_MKMATERIAL = $(N64_INST)/bin/mkmaterial + +all: testrom.z64 testrom_emu.z64 testrom_h264.z64 testrom_wav64.z64 testrom_samplebuffer.z64 MAIN_ELF_EXTERNS := $(BUILD_DIR)/testrom.externs DSO_MODULES = dl_test_syms.dso dl_test_relocs.dso dl_test_imports.dso dl_test_ctors.dso DSO_LIST = $(addprefix filesystem/, $(DSO_MODULES)) -$(BUILD_DIR)/testrom.dfs: $(wildcard filesystem/*) $(DSO_LIST) +$(BUILD_DIR)/testrom.dfs: $(wildcard filesystem/*) $(DSO_LIST) filesystem/test_dir ASSETS = filesystem/grass1.ci8.sprite \ filesystem/grass1.rgba32.sprite \ filesystem/grass1sq.rgba32.sprite \ - filesystem/grass2.rgba32.sprite + filesystem/grass2.rgba32.sprite \ + filesystem/basic.mdb OBJS = $(BUILD_DIR)/test_constructors_cpp.o \ $(BUILD_DIR)/rsp_test.o \ @@ -26,7 +30,26 @@ filesystem/grass2.rgba32.sprite: MKSPRITE_FLAGS=--mipmap BOX filesystem/%.sprite: assets/%.png @mkdir -p $(dir $@) @echo " [SPRITE] $@" - @$(N64_MKSPRITE) $(MKSPRITE_FLAGS) -o filesystem "$<" + $(N64_MKSPRITE) $(MKSPRITE_FLAGS) -o filesystem "$<" + +filesystem/%.mdb: assets/%.mat + @mkdir -p $(dir $@) + @echo " [MDB] $@" + $(N64_MKMATERIAL) -o filesystem "$<" + +filesystem/test_dir: + @mkdir -p $(dir $@) + @echo " [DIR] $@" + mkdir -p filesystem/aaaa-empty1 + mkdir -p filesystem/aaaa-empty2 + mkdir -p filesystem/test_dir/subdir + mkdir -p filesystem/test_dir/subdir/subempty1 + mkdir -p filesystem/test_dir/subdir/subempty2 + mkdir -p filesystem/test_dir/subdir/subsubdir + touch filesystem/test_dir/subdir/subsubdir/file3.txt + touch filesystem/test_dir/subdir/file2.txt + touch filesystem/test_dir/file1.txt + $(BUILD_DIR)/testrom.elf: $(BUILD_DIR)/testrom.o $(OBJS) $(MAIN_ELF_EXTERNS) $(ASSETS) testrom.z64: N64_ROM_TITLE="Libdragon Test ROM" @@ -36,6 +59,15 @@ $(BUILD_DIR)/testrom_emu.elf: $(BUILD_DIR)/testrom_emu.o $(OBJS) $(MAIN_ELF_EXTE testrom_emu.z64: N64_ROM_TITLE="Libdragon Test ROM" testrom_emu.z64: $(BUILD_DIR)/testrom.dfs $(BUILD_DIR)/testrom_emu.msym +$(BUILD_DIR)/testrom_h264.elf: $(BUILD_DIR)/test_h264.o +testrom_emu.z64: N64_ROM_TITLE="Libdragon H264 Test" + +$(BUILD_DIR)/testrom_wav64.elf: $(BUILD_DIR)/test_wav64.o +testrom_wav64.z64: N64_ROM_TITLE="Libdragon WAV64 Test" + +$(BUILD_DIR)/testrom_samplebuffer.elf: $(BUILD_DIR)/test_samplebuffer.o +testrom_samplebuffer.z64: N64_ROM_TITLE="Samplebuffer Test" + $(BUILD_DIR)/testrom_emu.o: $(SOURCE_DIR)/testrom.c @mkdir -p $(dir $@) @echo " [CC] $<" @@ -43,8 +75,8 @@ $(BUILD_DIR)/testrom_emu.o: $(SOURCE_DIR)/testrom.c ${BUILD_DIR}/rsp_test.o: IS_OVERLAY=1 -$(BUILD_DIR)/testrom_emu.msym: $(BUILD_DIR)/testrom_emu.elf -$(BUILD_DIR)/testrom.msym: $(BUILD_DIR)/testrom.elf +$(BUILD_DIR)/testrom_emu.msym: $(BUILD_DIR)/testrom_emu.elf $(MAIN_ELF_EXTERNS) +$(BUILD_DIR)/testrom.msym: $(BUILD_DIR)/testrom.elf $(MAIN_ELF_EXTERNS) $(MAIN_ELF_EXTERNS): $(DSO_LIST) filesystem/dl_test_syms.dso: $(BUILD_DIR)/dl_test_syms.o @@ -53,8 +85,12 @@ filesystem/dl_test_imports.dso: $(BUILD_DIR)/dl_test_imports.o filesystem/dl_test_ctors.dso: $(BUILD_DIR)/dl_test_ctors.o clean: - rm -rf $(BUILD_DIR) testrom.z64 testrom_emu.z64 + rm -rf $(BUILD_DIR) testrom.z64 testrom_emu.z64 testrom_h264.z64 testrom_wav64.z64 testrom_samplebuffer.z64 + $(MAKE) -C h264decoder_host clean + +h264decoder_host: + $(MAKE) -C h264decoder_host -include $(wildcard $(BUILD_DIR)/*.d) -.PHONY: all clean +.PHONY: all clean h264decoder_host diff --git a/tests/assets/basic.mat b/tests/assets/basic.mat new file mode 100644 index 000000000..7e7441e92 --- /dev/null +++ b/tests/assets/basic.mat @@ -0,0 +1,41 @@ +[glass] +tex0.name = grass1sq.rgba32.png +tex0.fmt = RGBA32 +tex0.s.repeats = inf +tex0.t.repeats = inf +tex0.s.mirror = true +tex0.t.mirror = false + +combiner.rgb = tex0 - prim + +rm.antialias = none +rm.alpha_compare = 1 + +#combiner.rgb.raw = (prim, 0, tex0, 0) +#blender.mode = multiply_const +#blender.fog.a = 0.3 + +[empty] + +[blender_none] +blender.mode = none + +[blender_multiply] +blender.mode = multiply + +[blender_multiply_const] +blender.mode = multiply_const +blender.const = 0.6 + +[blender_additive] +blender.mode = additive + +[ext_test] +combiner.rgb = prim + +ext.test.b = true +ext.test.i1 = 0x12345678 +ext.test.i2 = -1234567 +ext.test.f = 12345.67 +ext.test.s = this is a string + diff --git a/tests/cpakfs/Makefile b/tests/cpakfs/Makefile new file mode 100644 index 000000000..e9f8adf35 --- /dev/null +++ b/tests/cpakfs/Makefile @@ -0,0 +1,21 @@ +EXE = $(if $(findstring Windows,$(OS)),.exe,) +CFLAGS += -Wall -Werror -std=gnu11 +CFLAGS += -I ../../include +# Host unit test of preview cpakfs path APIs +CFLAGS += -DLIBDRAGON_PREVIEW=2 + +all: test_cpakfs_path$(EXE) + +test_cpakfs_path$(EXE): test_cpakfs_path.o + @echo " [CC] $<" + $(CC) $(LDFLAGS) -o $@ $< + + +test: test_cpakfs_path$(EXE) + @echo " [RUN] $<" + ./$< + +ifneq ($(V),1) +.SILENT: +endif +.PHONY: all test diff --git a/tests/cpakfs/test_cpakfs_path.c b/tests/cpakfs/test_cpakfs_path.c new file mode 100644 index 000000000..2087b972c --- /dev/null +++ b/tests/cpakfs/test_cpakfs_path.c @@ -0,0 +1,470 @@ +/** + * @file test_cpakfs_path.c + * @brief Test for cpakfs path parsing and formatting functions + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define assertf(condition, fmt, ...) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "Assertion failed: " fmt "\n", ##__VA_ARGS__); \ + exit(EXIT_FAILURE); \ + } \ + } while (0) + +#define __randn(n) (rand() % (n)) + +// Include the cpakfs header first for types +#include "../../include/cpakfs.h" + +// Include the actual cpakfs_path.c file +#include "../../src/joybus/cpakfs_path.c" + +// Test framework +static int tests_run = 0; +static int tests_failed = 0; + +#define TEST(name) \ + static void test_##name(void); \ + static void run_test_##name(void) { \ + printf("Running test: " #name "... "); \ + tests_run++; \ + test_##name(); \ + printf("PASSED\n"); \ + } \ + static void test_##name(void) + +#define ASSERT_EQ(expected, actual) \ + do { \ + if ((expected) != (actual)) { \ + printf("FAILED\n Expected: %d, Got: %d at line %d\n", (int)(expected), (int)(actual), __LINE__); \ + tests_failed++; \ + return; \ + } \ + } while(0) + +#define ASSERT_STR_EQ(expected, actual) \ + do { \ + if (strcmp((expected), (actual)) != 0) { \ + printf("FAILED\n Expected: \"%s\", Got: \"%s\" at line %d\n", (expected), (actual), __LINE__); \ + tests_failed++; \ + return; \ + } \ + } while(0) + +#define ASSERT_MEM_EQ(expected, actual, len) \ + do { \ + if (memcmp((expected), (actual), (len)) != 0) { \ + printf("FAILED\n Memory mismatch at line %d\n", __LINE__); \ + tests_failed++; \ + return; \ + } \ + } while(0) + +// Data-driven test structure for valid paths +typedef struct { + const char *description; + const char *utf8_fullname; + cpakfs_path_t expected_path; +} path_test_case_t; + +static const path_test_case_t valid_path_tests[] = { + { + .description = "Basic ASCII path with extension", + .utf8_fullname = "NSME.01-SAVE.A", + .expected_path = { "NSME", "01", "\x2C\x1A\x2F\x1E", "\x1A" }, + }, + { + .description = "Lowercase conversion", + .utf8_fullname = "nsme.01-save.a", + .expected_path = { "NSME", "01", "\x2C\x1A\x2F\x1E", "\x1A" }, + }, + { + .description = "Path without extension", + .utf8_fullname = "GAME.01-FILENAME", + .expected_path = { "GAME", "01", "\x1F\x22\x25\x1E\x27\x1A\x26\x1E", "" }, + }, + { + .description = "Numeric gamecode and pubcode", + .utf8_fullname = "1234.56-SAVE.A", + .expected_path = { "1234", "56", "\x2C\x1A\x2F\x1E", "\x1A" }, + }, + { + .description = "Mixed alphanumeric codes", + .utf8_fullname = "TEST.99-MYFILE.DAT", + .expected_path = { "TEST", "99", "\x26\x32\x1F\x22\x25\x1E", "\x1D\x1A\x2D" }, + }, + { + .description = "Embedded NUL characters in filename", + .utf8_fullname = "GAME.01-FOO\001BAR.TXT", + .expected_path = { "GAME", "01", "\x1F\x28\x28\x00\x1B\x1A\x2B", "\x2D\x31\x2D" }, + }, + { + .description = "Hex gamecode with normal pubcode", + .utf8_fullname = "04050607.01-SAVE.A", + .expected_path = { "\x04\x05\x06\x07", "01", "\x2C\x1A\x2F\x1E", "\x1A" }, + }, + { + .description = "Normal gamecode with hex pubcode", + .utf8_fullname = "GAME.FF00-SAVE.A", + .expected_path = { "GAME", "\xFF\x00", "\x2C\x1A\x2F\x1E", "\x1A" }, + }, + { + .description = "Both gamecode and pubcode in hex", + .utf8_fullname = "01234567.ABCD-SAVE.A", + .expected_path = { "\x01\x23\x45\x67", "\xAB\xCD", "\x2C\x1A\x2F\x1E", "\x1A" }, + }, + { + .description = "Hex with lowercase letters", + .utf8_fullname = "deadbeef.cafe-SAVE.A", + .expected_path = { "\xDE\xAD\xBE\xEF", "\xCA\xFE", "\x2C\x1A\x2F\x1E", "\x1A" }, + }, + { + .description = "Multiple dots in filename", + .utf8_fullname = "GAME.01-FOO.BAR.BAZ", + .expected_path = { "GAME", "01", "\x1F\x28\x28\x3C\x1B\x1A\x2B", "\x1B\x1A\x33" }, + }, + { + .description = "Multiple dots in filename without extension (trailing dot disambiguation)", + .utf8_fullname = "GAME.01-FOO.BAR.", + .expected_path = { "GAME", "01", "\x1F\x28\x28\x3C\x1B\x1A\x2B", "" }, + }, + { + .description = "Multiple dots with extension", + .utf8_fullname = "TEST.99-A.B.C.D", + .expected_path = { "TEST", "99", "\x1A\x3C\x1B\x3C\x1C", "\x1D" }, + }, + // Japanese/CJK character tests + { + .description = "Japanese filename with katakana", + .utf8_fullname = "GAME.01-。ァィゥ.TXT", // 。ァィゥ = 0x42,0x45,0x46,0x47 + .expected_path = { "GAME", "01", "\x42\x45\x46\x47", "\x2D\x31\x2D" }, + }, + { + .description = "Japanese extension with katakana", + .utf8_fullname = "GAME.01-SAVE.ェォ", // ェォ = 0x48,0x49 + .expected_path = { "GAME", "01", "\x2C\x1A\x2F\x1E", "\x48\x49" }, + }, + { + .description = "Mixed ASCII and Japanese", + .utf8_fullname = "GAME.01-MYッャ.DAT", // ッャ = 0x4A,0x4B + .expected_path = { "GAME", "01", "\x26\x32\x4A\x4B", "\x1D\x1A\x2D" }, + }, + { + .description = "Japanese symbols", + .utf8_fullname = "GAME.01-。゛゜ァ.TXT", // 。゛゜ァ = 0x42,0x43,0x44,0x45 + .expected_path = { "GAME", "01", "\x42\x43\x44\x45", "\x2D\x31\x2D" }, + } +}; + +// Data-driven test structure for invalid paths +typedef struct { + const char *description; + const char *utf8_fullname; + const char *error_pos; // visual pointer to the error position in the string + cpakfs_parse_err_t expected_error; +} path_error_test_case_t; + +static const path_error_test_case_t error_path_tests[] = { + { + .description = "Too short gamecode (3 chars)", + .utf8_fullname = "GAM.01-SAVE.A", + .error_pos = " ^ ", + .expected_error = CPAKFS_PARSE_ERR_GAMECODE_TOO_SHORT, + }, + { + .description = "Too short pubcode (1 char)", + .utf8_fullname = "GAME.0-SAVE.A", + .error_pos = " ^ ", + .expected_error = CPAKFS_PARSE_ERR_PUBCODE_TOO_SHORT, + }, + { + .description = "Invalid character in gamecode", + .utf8_fullname = "GA@E.01-SAVE.A", + .error_pos = " ^ ", + .expected_error = CPAKFS_PARSE_ERR_GAMECODE_CHAR, + }, + { + .description = "Empty filename", + .utf8_fullname = "GAME.01-.A", + .error_pos = " ^ ", + .expected_error = CPAKFS_PARSE_ERR_FILENAME_TOO_SHORT, + }, + { + .description = "Invalid hex gamecode (8 chars with non-hex)", + .utf8_fullname = "GAMECODE.01-SAVE.A", + .error_pos = " ^ ", + .expected_error = CPAKFS_PARSE_ERR_GAMECODE_TOO_LONG, + }, + { + .description = "Invalid hex pubcode (non-hex chars)", + .utf8_fullname = "GAME.PUBZ-SAVE.A", + .error_pos = " ^ ", + .expected_error = CPAKFS_PARSE_ERR_PUBCODE_TOO_LONG, + }, + { + .description = "Wrong gamecode length (6 chars)", + .utf8_fullname = "ABCDEF.01-SAVE.A", + .error_pos = " ^ ", + .expected_error = CPAKFS_PARSE_ERR_GAMECODE_TOO_LONG, + }, + { + .description = "Wrong pubcode length (3 chars)", + .utf8_fullname = "GAME.ABC-SAVE.A", + .error_pos = " ^ ", + .expected_error = CPAKFS_PARSE_ERR_PUBCODE_TOO_LONG, + }, + // Additional test cases for new error codes + { + .description = "Gamecode too short (2 chars)", + .utf8_fullname = "GA.01-SAVE.A", + .error_pos = " ^ ", + .expected_error = CPAKFS_PARSE_ERR_GAMECODE_TOO_SHORT, + }, + { + .description = "Gamecode too short (1 char)", + .utf8_fullname = "G.01-SAVE.A", + .error_pos = " ^ ", + .expected_error = CPAKFS_PARSE_ERR_GAMECODE_TOO_SHORT, + }, + { + .description = "Gamecode too long (9 chars, not valid hex)", + .utf8_fullname = "TOOLONGAB.01-SAVE.A", + .error_pos = " ^ ", + .expected_error = CPAKFS_PARSE_ERR_GAMECODE_TOO_LONG, + }, + { + .description = "Pubcode too short (empty)", + .utf8_fullname = "GAME.-SAVE.A", + .error_pos = " ^ ", + .expected_error = CPAKFS_PARSE_ERR_PUBCODE_TOO_SHORT, + }, + { + .description = "Pubcode too long (5 chars, not valid hex)", + .utf8_fullname = "GAME.TOOLN-SAVE.A", + .error_pos = " ^ ", + .expected_error = CPAKFS_PARSE_ERR_PUBCODE_TOO_LONG, + }, + { + .description = "Filename too long (more than 16 N64 chars)", + .utf8_fullname = "GAME.01-VERYLONGFILENAMETHATEXCEEDSLIMIT.A", + .error_pos = " ^ ", + .expected_error = CPAKFS_PARSE_ERR_FILENAME_TOO_LONG, + }, + { + .description = "Extension too long (more than 4 N64 chars)", + .utf8_fullname = "GAME.01-SAVE.TOOLONG", + .error_pos = " ^ ", + .expected_error = CPAKFS_PARSE_ERR_EXTENSION_TOO_LONG, + }, + // Japanese character error tests - length based on N64 chars, not UTF-8 bytes + { + .description = "Japanese filename exactly 16 N64 chars (should pass)", + .utf8_fullname = "GAME.01-。ァィゥェォッャュョヲンアイウエ.TXT", // 16 N64 chars exactly + .error_pos = NULL, + .expected_error = CPAKFS_PARSE_OK, // This should actually pass + }, + { + .description = "Japanese filename too long (17 N64 chars)", + .utf8_fullname = "GAME.01-。ァィゥェォッャュョヲンアイウエカ.TXT", // 17 N64 chars + .error_pos = " ^ ", // Points to where overflow detected (actual offset: 56) + .expected_error = CPAKFS_PARSE_ERR_FILENAME_TOO_LONG, + }, + { + .description = "Japanese extension too long (5 N64 chars)", + .utf8_fullname = "GAME.01-SAVE.。ァィゥェ", // 5 N64 chars in extension + .error_pos = " ^ ", // Points to where overflow detected (actual offset: 25) + .expected_error = CPAKFS_PARSE_ERR_EXTENSION_TOO_LONG, + }, + { + .description = "Invalid Japanese character (unsupported UTF-8)", + .utf8_fullname = "GAME.01-SAVE表.TXT", // 表 is not in the N64 codepage + .error_pos = " ^ ", // Points to start of invalid UTF-8 sequence + .expected_error = CPAKFS_PARSE_ERR_FILENAME_CHAR, + }, + { + .description = "Invalid Japanese in extension", + .utf8_fullname = "GAME.01-SAVE.漢字", // 漢字 not in N64 codepage + .error_pos = " ^ ", // Points to start of invalid UTF-8 sequence + .expected_error = CPAKFS_PARSE_ERR_EXTENSION_CHAR, + }, + { + .description = "Truncated UTF-8 sequence in filename", + .utf8_fullname = "GAME.01-SAVE\xE3\x81.TXT", // Incomplete UTF-8 sequence + .error_pos = " ^ ", // Points to start of bad UTF-8 + .expected_error = CPAKFS_PARSE_ERR_FILENAME_CHAR, + }, + { + .description = "Valid Japanese then invalid character", + .utf8_fullname = "GAME.01-。表.TXT", // 。 is valid, 表 is not in N64 codepage + .error_pos = " ^ ", // Points to start of invalid UTF-8 (表) + .expected_error = CPAKFS_PARSE_ERR_FILENAME_CHAR, + } +}; + +// Test cases +TEST(valid_paths_roundtrip) { + const int num_tests = sizeof(valid_path_tests) / sizeof(valid_path_tests[0]); + + for (int i = 0; i < num_tests; i++) { + const path_test_case_t *test = &valid_path_tests[i]; + cpakfs_path_t path; + const char *error_pos; + char formatted[64]; + + printf(" Sub-test %d: %s\n", i + 1, test->description); + + // Test parsing + cpakfs_parse_err_t result = cpakfs_path_parse(test->utf8_fullname, &path, &error_pos); + if (result != CPAKFS_PARSE_OK) { + printf("FAILED\n Parse error: %d for '%s'\n", result, test->utf8_fullname); + tests_failed++; + return; + } + + // Check parsed values - just compare the entire structure + if (memcmp(&path, &test->expected_path, sizeof(cpakfs_path_t)) != 0) { + printf("FAILED\n Path structure mismatch for '%s'\n", test->utf8_fullname); + printf(" Expected gamecode: %.4s\n", test->expected_path.gamecode); + printf(" Got gamecode: %.4s\n", path.gamecode); + printf(" Expected pubcode: %.2s\n", test->expected_path.pubcode); + printf(" Got pubcode: %.2s\n", path.pubcode); + tests_failed++; + return; + } + + // Test formatting + cpakfs_path_format(&path, formatted, sizeof(formatted)); + + // For inputs with lowercase, the formatted result should be uppercase + const char *expected_formatted = test->utf8_fullname; + if (strcmp(test->utf8_fullname, "nsme.01-save.a") == 0) { + expected_formatted = "NSME.01-SAVE.A"; + } else if (strcmp(test->utf8_fullname, "deadbeef.cafe-SAVE.A") == 0) { + expected_formatted = "DEADBEEF.CAFE-SAVE.A"; + } + + if (strcmp(formatted, expected_formatted) != 0) { + printf("FAILED\n Format mismatch for '%s': expected '%s', got '%s'\n", + test->utf8_fullname, expected_formatted, formatted); + tests_failed++; + return; + } + + // Test roundtrip consistency + cpakfs_path_t path2; + cpakfs_parse_err_t result2 = cpakfs_path_parse(formatted, &path2, &error_pos); + if (result2 != CPAKFS_PARSE_OK) { + printf("FAILED\n Roundtrip parse error: %d for '%s'\n", result2, formatted); + tests_failed++; + return; + } + + if (memcmp(&path, &path2, sizeof(cpakfs_path_t)) != 0) { + printf("FAILED\n Roundtrip data mismatch for '%s'\n", test->utf8_fullname); + tests_failed++; + return; + } + } +} + +TEST(invalid_paths) { + const int num_tests = sizeof(error_path_tests) / sizeof(error_path_tests[0]); + + for (int i = 0; i < num_tests; i++) { + const path_error_test_case_t *test = &error_path_tests[i]; + cpakfs_path_t path; + const char *error_pos; + + printf(" Sub-test %d: %s\n", i + 1, test->description); + + cpakfs_parse_err_t result = cpakfs_path_parse(test->utf8_fullname, &path, &error_pos); + + // Special case: if expected_error is CPAKFS_PARSE_OK, this test should pass + if (test->expected_error == CPAKFS_PARSE_OK) { + if (result != CPAKFS_PARSE_OK) { + printf("FAILED\n Expected parsing to succeed but got error %d for '%s'\n", + result, test->utf8_fullname); + tests_failed++; + return; + } + // Test passed - continue to next test + continue; + } + + if (result != test->expected_error) { + printf("FAILED\n Expected error %d, got %d for '%s'\n", + test->expected_error, result, test->utf8_fullname); + tests_failed++; + return; + } + + // Check error position if specified + if (test->error_pos != NULL) { + // Find the position of '^' in the error_pos string + const char *caret_pos = strchr(test->error_pos, '^'); + if (caret_pos != NULL) { + int expected_offset = caret_pos - test->error_pos; + int actual_offset = error_pos - test->utf8_fullname; + + if (actual_offset != expected_offset) { + printf("FAILED\n Error position mismatch for '%s'\n", test->utf8_fullname); + printf(" Expected offset: %d, got: %d\n", expected_offset, actual_offset); + printf(" String: %s\n", test->utf8_fullname); + printf(" Expected: %s\n", test->error_pos); + printf(" Actual: "); + for (int j = 0; j < actual_offset; j++) printf(" "); + printf("^\n"); + tests_failed++; + return; + } + } + } + } +} + +TEST(null_inputs) { + cpakfs_path_t path; + const char *error_pos; + + // Test null input + cpakfs_parse_err_t result = cpakfs_path_parse(NULL, &path, &error_pos); + ASSERT_EQ(CPAKFS_PARSE_ERR_GAMECODE_TOO_SHORT, result); + + // Test null path + result = cpakfs_path_parse("GAME.01-SAVE.A", NULL, &error_pos); + ASSERT_EQ(CPAKFS_PARSE_ERR_GAMECODE_TOO_SHORT, result); + + // Test null formatting inputs + char output[64]; + cpakfs_path_format(NULL, output, sizeof(output)); + ASSERT_STR_EQ("", output); +} + +int main() { + printf("Running cpakfs path tests...\n\n"); + + run_test_valid_paths_roundtrip(); + run_test_invalid_paths(); + run_test_null_inputs(); + + printf("\nTests run: %d\n", tests_run); + printf("Tests failed: %d\n", tests_failed); + + if (tests_failed > 0) { + printf("SOME TESTS FAILED!\n"); + return 1; + } else { + printf("ALL TESTS PASSED!\n"); + return 0; + } +} diff --git a/tests/h264decoder_host/.gitignore b/tests/h264decoder_host/.gitignore new file mode 100644 index 000000000..1e91a966f --- /dev/null +++ b/tests/h264decoder_host/.gitignore @@ -0,0 +1,2 @@ +h264decoder_host +build/ diff --git a/tests/h264decoder_host/Makefile b/tests/h264decoder_host/Makefile new file mode 100644 index 000000000..0bd836312 --- /dev/null +++ b/tests/h264decoder_host/Makefile @@ -0,0 +1,57 @@ +BUILD_DIR := build +TARGET := h264decoder_host + +ROOT := ../.. +ROOT_ABS := $(abspath $(ROOT)) +DECODER_DIR := $(ROOT_ABS)/src/video/h264_decoder +OMXDL_DIR := $(DECODER_DIR)/omxdl +LODEPNG_DIR := $(ROOT_ABS)/tools/common + +CXX := c++ +CC := cc + +INCLUDES := -I$(DECODER_DIR) -I$(OMXDL_DIR) -I$(LODEPNG_DIR) \ + -I$(ROOT_ABS)/include -I$(ROOT_ABS)/include/newlib_overrides +CXXFLAGS := -O2 -g -std=c++17 -Wall -Wextra -MMD -MP $(INCLUDES) +CFLAGS := -O2 -g -std=c11 -Wall -Wextra -MMD -MP $(INCLUDES) -D_ERROR_PRINT +LDFLAGS := +LDLIBS := -lm + +CPP_SRCS := h264decoder_host.cpp +CPP_OBJS := $(patsubst %.cpp,$(BUILD_DIR)/%.o,$(CPP_SRCS)) + +DECODER_SRCS := $(filter-out $(DECODER_DIR)/h264_decoder.c,$(wildcard $(DECODER_DIR)/*.c)) +OMXDL_SRCS := $(wildcard $(OMXDL_DIR)/*.c) +EXTRA_C_SRCS := $(LODEPNG_DIR)/lodepng.c +C_SRCS := $(DECODER_SRCS) $(OMXDL_SRCS) $(EXTRA_C_SRCS) +C_OBJS := $(patsubst $(ROOT_ABS)/%,$(BUILD_DIR)/%,$(C_SRCS:.c=.o)) +DEPFILES := $(CPP_OBJS:.o=.d) $(C_OBJS:.o=.d) + +all: $(TARGET) + +$(TARGET): $(CPP_OBJS) $(C_OBJS) + @mkdir -p "$(dir $@)" + @echo " [LD] $@" + $(CXX) $(LDFLAGS) -o "$@" $^ $(LDLIBS) + +$(BUILD_DIR)/%.o: %.cpp + @mkdir -p "$(dir $@)" + @echo " [CXX] $<" + $(CXX) $(CXXFLAGS) -c "$<" -o "$@" + +$(BUILD_DIR)/%.o: $(ROOT_ABS)/%.c + @mkdir -p "$(dir $@)" + @echo " [CC] $<" + $(CC) $(CFLAGS) -c "$<" -o "$@" + +clean: + @echo " [CLEAN] $(BUILD_DIR)" + rm -rf "$(BUILD_DIR)" + +ifneq ($(V),1) +.SILENT: +endif + +-include $(DEPFILES) + +.PHONY: all clean diff --git a/tests/h264decoder_host/h264decoder_host.cpp b/tests/h264decoder_host/h264decoder_host.cpp new file mode 100644 index 000000000..d61dc6a5b --- /dev/null +++ b/tests/h264decoder_host/h264decoder_host.cpp @@ -0,0 +1,470 @@ +/* + h264decoder_host: host H.264 decoder validation tool for Libdragon + Written by Giovanni Bajo + + This tool is part of the Libdragon SDK. + + This is free and unencumbered software released into the public domain. + For more information, please refer to +*/ +/* + + This tool accepts a H.264 raw bitstream and decodes it using the PC version + of libdragon H.264 decoder. Then, it reencodes it in MP4 format using ffmpeg. + + The goal of this tool is to test the H.264 decoder in the PC, for debugging + purposes. If eg. this tool correctly decodes a video, but N64 fails to, + the bug is in the N64 port of the decoder (eg: RSP ucode). Otherwise, it has + to be a bug in the C portion of the decoder. + +*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#define LODEPNG_NO_COMPILE_CPP +#include "lodepng.h" +#include "h264bsd_decoder.h" +#include "h264bsd_storage.h" +#include "h264bsd_util.h" +} + +namespace fs = std::filesystem; + +struct Options { + fs::path input_path; + fs::path output_mp4; + std::string ffmpeg_path = "ffmpeg"; + bool verbose = false; + bool debug = false; + uint32_t max_frames = 0; // 0 = unlimited + uint32_t mb_budget = 0; // 0 = unlimited; >0 exercises mid-slice resume +}; + +static void usage() { + fprintf(stderr, + "Usage: h264decoder_host [options] \n" + "Options:\n" + " -o, --output Output MP4 path (default: .mp4)\n" + " --max-frames Decode at most N frames\n" + " --mb-budget Timeslice decode to N macroblocks per call\n" + " --debug Keep PNG frames in _frames/\n" + " -v, --verbose Verbose logs and progress\n" + " --ffmpeg-path ffmpeg executable (default: ffmpeg)\n" + " -h, --help Show this help message\n"); +} + +static void vlog(const Options &opt, const char *fmt, ...) { + if (!opt.verbose) return; + va_list args; + va_start(args, fmt); + vfprintf(stderr, fmt, args); + fprintf(stderr, "\n"); + va_end(args); +} + +static bool parse_u32(const char *s, uint32_t &out) { + if (!s || !*s) return false; + char *end = nullptr; + unsigned long v = strtoul(s, &end, 10); + if (*end != '\0' || v > 0xFFFFFFFFul) return false; + out = static_cast(v); + return true; +} + +static bool parse_args(int argc, char **argv, Options &opt) { + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "-o" || arg == "--output") { + if (++i >= argc) return false; + opt.output_mp4 = argv[i]; + } else if (arg == "--max-frames") { + if (++i >= argc) return false; + if (!parse_u32(argv[i], opt.max_frames) || opt.max_frames == 0) return false; + } else if (arg == "--mb-budget") { + if (++i >= argc) return false; + if (!parse_u32(argv[i], opt.mb_budget) || opt.mb_budget == 0) return false; + } else if (arg == "--debug") { + opt.debug = true; + } else if (arg == "-v" || arg == "--verbose") { + opt.verbose = true; + } else if (arg == "--ffmpeg-path") { + if (++i >= argc) return false; + opt.ffmpeg_path = argv[i]; + } else if (arg == "-h" || arg == "--help") { + usage(); + exit(0); + } else if (!arg.empty() && arg[0] == '-') { + fprintf(stderr, "Unknown option: %s\n", arg.c_str()); + return false; + } else { + if (!opt.input_path.empty()) { + fprintf(stderr, "Too many positional arguments\n"); + return false; + } + opt.input_path = arg; + } + } + + if (opt.input_path.empty()) return false; + + if (opt.output_mp4.empty()) { + fs::path base = opt.input_path; + base.replace_extension(".mp4"); + opt.output_mp4 = base; + } + + return true; +} + +static std::vector read_file(const fs::path &path) { + std::ifstream f(path, std::ios::binary); + if (!f) throw std::runtime_error("Cannot open input file"); + f.seekg(0, std::ios::end); + std::streamsize size = f.tellg(); + f.seekg(0, std::ios::beg); + if (size <= 0) throw std::runtime_error("Input file is empty"); + std::vector data(static_cast(size)); + if (!f.read(reinterpret_cast(data.data()), size)) { + throw std::runtime_error("Failed to read input file"); + } + return data; +} + +static inline uint8_t clip_u8(int v) { + if (v < 0) return 0; + if (v > 255) return 255; + return static_cast(v); +} + +static void yuv420_to_rgb24(const uint8_t *y, const uint8_t *cb, const uint8_t *cr, + int width, int height, std::vector &rgb) { + rgb.resize(static_cast(width) * static_cast(height) * 3u); + for (int j = 0; j < height; ++j) { + int cj = j >> 1; + for (int i = 0; i < width; ++i) { + int ci = i >> 1; + int yv = y[j * width + i]; + int uv = cb[cj * (width >> 1) + ci] - 128; + int vv = cr[cj * (width >> 1) + ci] - 128; + + int r = static_cast(yv + 1.402 * vv); + int g = static_cast(yv - 0.344136 * uv - 0.714136 * vv); + int b = static_cast(yv + 1.772 * uv); + + size_t o = (static_cast(j) * static_cast(width) + static_cast(i)) * 3u; + rgb[o + 0] = clip_u8(r); + rgb[o + 1] = clip_u8(g); + rgb[o + 2] = clip_u8(b); + } + } +} + +static std::string shell_quote(const std::string &s) { + std::string out = "'"; + for (char c : s) { + if (c == '\'') out += "'\\''"; + else out += c; + } + out += "'"; + return out; +} + +static double extract_framerate(storage_t *storage, bool *is_from_vui) { + if (storage->activeSps && + storage->activeSps->vuiParametersPresentFlag && + storage->activeSps->vuiParameters && + storage->activeSps->vuiParameters->timingInfoPresentFlag) { + uint32_t timeScale = storage->activeSps->vuiParameters->timeScale; + uint32_t numUnitsInTick = storage->activeSps->vuiParameters->numUnitsInTick; + if (numUnitsInTick > 0) { + if (is_from_vui) *is_from_vui = true; + return static_cast(timeScale) / (2.0 * static_cast(numUnitsInTick)); + } + } + if (is_from_vui) *is_from_vui = false; + return 20.0; +} + +static int run_ffmpeg_pack(const Options &opt, const fs::path &frame_dir, bool use_png, double fps) { + fs::path pattern = frame_dir / (use_png ? "frame_%06d.png" : "frame_%06d.ppm"); + std::ostringstream cmd; + cmd << shell_quote(opt.ffmpeg_path); + if (!opt.verbose) { + cmd << " -hide_banner -loglevel error -nostats"; + } + cmd << " -y -framerate " << fps + << " -i " << shell_quote(pattern.string()) + << " -c:v libx264 -preset veryfast -crf 10 -pix_fmt yuv420p " + << shell_quote(opt.output_mp4.string()); + vlog(opt, "Running ffmpeg: %s", cmd.str().c_str()); + return std::system(cmd.str().c_str()); +} + +static unsigned encode_png_fastish_file(const char *filename, const unsigned char *rgb, unsigned w, unsigned h) { + LodePNGState state; + lodepng_state_init(&state); + + // Keep real compression but strongly bias for speed. + state.encoder.auto_convert = 0; + state.encoder.filter_palette_zero = 0; + state.encoder.filter_strategy = LFS_ZERO; + state.encoder.zlibsettings.btype = 2; + state.encoder.zlibsettings.use_lz77 = 0; + state.encoder.zlibsettings.windowsize = 256; + state.encoder.zlibsettings.nicematch = 16; + state.encoder.zlibsettings.lazymatching = 0; + + state.info_raw = lodepng_color_mode_make(LCT_RGB, 8); + state.info_png.color = lodepng_color_mode_make(LCT_RGB, 8); + + unsigned char *out = NULL; + size_t out_size = 0; + unsigned error = lodepng_encode(&out, &out_size, rgb, w, h, &state); + if (!error) error = lodepng_save_file(out, out_size, filename); + if (out) free(out); + lodepng_state_cleanup(&state); + return error; +} + +static bool write_ppm_file(const char *filename, const unsigned char *rgb, unsigned w, unsigned h) { + FILE *f = fopen(filename, "wb"); + if (!f) return false; + if (fprintf(f, "P6\n%u %u\n255\n", w, h) < 0) { + fclose(f); + return false; + } + size_t size = (size_t)w * (size_t)h * 3u; + bool ok = fwrite(rgb, 1, size, f) == size; + ok = ok && (fclose(f) == 0); + return ok; +} + +static void progress_update_default(double pct, uint32_t frames) { + if (pct < 0.0) pct = 0.0; + if (pct > 100.0) pct = 100.0; + const int width = 40; + int filled = (int)((pct / 100.0) * width + 0.5); + if (filled < 0) filled = 0; + if (filled > width) filled = width; + char bar[41]; + for (int i = 0; i < width; i++) bar[i] = (i < filled) ? '#' : '-'; + bar[width] = '\0'; + fprintf(stdout, "\rProgress [%s] %6.1f%% frames=%u", bar, pct, frames); + fflush(stdout); +} + +static void progress_update_verbose(double pct, uint32_t frames, size_t consumed, size_t total) { + if (pct < 0.0) pct = 0.0; + if (pct > 100.0) pct = 100.0; + fprintf(stdout, "Progress: %6.1f%% frames=%u bytes=%zu/%zu\n", pct, frames, consumed, total); + fflush(stdout); +} + +struct PngDirGuard { + fs::path path; + bool keep = false; + ~PngDirGuard() { + if (!keep && !path.empty()) { + std::error_code ec; + fs::remove_all(path, ec); + } + } +}; + +static fs::path create_temp_png_dir() { + char tmpl[] = "/tmp/h264decoder_host_XXXXXX"; + char *dir = mkdtemp(tmpl); + if (!dir) throw std::runtime_error("Cannot create temporary directory"); + return fs::path(dir); +} + +int main(int argc, char **argv) { + Options opt; + if (!parse_args(argc, argv, opt)) { + usage(); + return 2; + } + + try { + std::vector bitstream = read_file(opt.input_path); + + PngDirGuard png_guard; + if (opt.debug) { + png_guard.path = opt.output_mp4.parent_path() / (opt.input_path.stem().string() + "_frames"); + png_guard.keep = true; + } else { + png_guard.path = create_temp_png_dir(); + png_guard.keep = false; + } + fs::create_directories(png_guard.path); + fs::path out_parent = opt.output_mp4.parent_path(); + if (!out_parent.empty()) { + fs::create_directories(out_parent); + } + + storage_t storage; + h264bsdInitStorage(&storage); + u32 init_ret = h264bsdInit(&storage, 0); + if (init_ret != HANTRO_OK) { + fprintf(stderr, "h264bsdInit failed: %u\n", init_ret); + return 1; + } + + const uint8_t *cur = bitstream.data(); + size_t remaining = bitstream.size(); + uint32_t frame_idx = 0; + int width = 0, height = 0; + std::vector rgb; + double input_fps = 0.0; + bool fps_from_vui = false; + double last_progress_printed = -1.0; + + auto stop_at_max = [&]() -> bool { + return opt.max_frames > 0 && frame_idx >= opt.max_frames; + }; + + auto dump_ready_pictures = [&]() -> bool { + for (;;) { + if (stop_at_max()) return true; + u32 picId = 0, isIdrPic = 0, numErrMbs = 0; + uint8_t *pic_data = h264bsdNextOutputPicture(&storage, &picId, &isIdrPic, &numErrMbs); + if (!pic_data) break; + + if (width == 0 || height == 0) { + width = static_cast(h264bsdPicWidth(&storage)) * 16; + height = static_cast(h264bsdPicHeight(&storage)) * 16; + input_fps = extract_framerate(&storage, &fps_from_vui); + vlog(opt, "Stream info: %dx%d @ %.6f fps (%s)", width, height, input_fps, fps_from_vui ? "from VUI" : "default"); + } + if (width <= 0 || height <= 0) { + fprintf(stderr, "Decoder dimensions unavailable\n"); + return false; + } + if (numErrMbs != 0) { + fprintf(stderr, "Decoded picture has concealed MBs: %u\n", numErrMbs); + } + + const uint8_t *base = reinterpret_cast(pic_data); + const uint8_t *y = base; + const uint8_t *cb = y + width * height; + const uint8_t *cr = cb + (width * height) / 4; + yuv420_to_rgb24(y, cb, cr, width, height, rgb); + + char name[64]; + snprintf(name, sizeof(name), opt.debug ? "frame_%06u.png" : "frame_%06u.ppm", frame_idx++); + fs::path out_path = png_guard.path / name; + if (opt.debug) { + unsigned enc = encode_png_fastish_file(out_path.string().c_str(), rgb.data(), (unsigned)width, (unsigned)height); + if (enc) { + fprintf(stderr, "PNG encode failed (%u): %s\n", enc, lodepng_error_text(enc)); + return false; + } + } else { + bool ok = write_ppm_file(out_path.string().c_str(), rgb.data(), (unsigned)width, (unsigned)height); + if (!ok) { + fprintf(stderr, "PPM write failed: %s\n", out_path.string().c_str()); + return false; + } + } + } + return true; + }; + + while (remaining > 0 && !stop_at_max()) { + u32 num_read = 0; + u32 status = h264bsdDecodePartial(&storage, const_cast(cur), + static_cast(remaining), frame_idx, &num_read, opt.mb_budget); + // Mid-slice timeslice returns H264BSD_RDY with 0 bytes consumed; HDRS_RDY + // likewise rewinds the NAL. Anything else with 0 bytes means we are stuck. + if (num_read == 0) { + if (status != H264BSD_HDRS_RDY && + !(status == H264BSD_RDY && h264bsdIsSlicePending(&storage))) { + fprintf(stderr, "Decoder consumed 0 bytes (stuck), status=%u\n", status); + h264bsdShutdown(&storage); + return 1; + } + } else { + cur += num_read; + remaining -= num_read; + } + + if (status == H264BSD_ERROR || status == H264BSD_PARAM_SET_ERROR || status == H264BSD_MEMALLOC_ERROR) { + fprintf(stderr, "h264bsdDecode failed: status=%u\n", status); + h264bsdShutdown(&storage); + return 1; + } + + if (!dump_ready_pictures()) { + h264bsdShutdown(&storage); + return 1; + } + + size_t consumed = bitstream.size() - remaining; + double decode_pct = bitstream.empty() ? 100.0 : (100.0 * (double)consumed / (double)bitstream.size()); + if (decode_pct > 99.0 && !stop_at_max()) decode_pct = 99.0; + if (opt.verbose) { + if (decode_pct >= last_progress_printed + 2.0 || stop_at_max()) { + progress_update_verbose(decode_pct, frame_idx, consumed, bitstream.size()); + last_progress_printed = decode_pct; + } + } else { + if (decode_pct >= last_progress_printed + 0.5 || stop_at_max()) { + progress_update_default(decode_pct, frame_idx); + last_progress_printed = decode_pct; + } + } + } + + if (!stop_at_max()) { + h264bsdFlushBuffer(&storage); + if (!dump_ready_pictures()) { + h264bsdShutdown(&storage); + return 1; + } + } + + h264bsdShutdown(&storage); + + if (frame_idx == 0) { + fprintf(stderr, "No frames decoded\n"); + return 1; + } + if (input_fps <= 0.0) input_fps = 20.0; + + if (opt.verbose) { + fprintf(stdout, "Progress: 100.0%% frames=%u decode complete, packing MP4...\n", frame_idx); + fflush(stdout); + } else { + progress_update_default(100.0, frame_idx); + fprintf(stdout, "\n"); + fflush(stdout); + } + + int ff_rc = run_ffmpeg_pack(opt, png_guard.path, opt.debug, input_fps); + if (ff_rc != 0) { + fprintf(stderr, "ffmpeg failed with code %d\n", ff_rc); + return 1; + } + + vlog(opt, "Wrote MP4: %s", opt.output_mp4.string().c_str()); + if (opt.debug) { + vlog(opt, "Kept PNG frames in: %s", png_guard.path.string().c_str()); + } + } catch (const std::exception &e) { + fprintf(stderr, "Fatal: %s\n", e.what()); + return 1; + } + + return 0; +} diff --git a/tests/hashtable/.gitignore b/tests/hashtable/.gitignore new file mode 100644 index 000000000..9650b4484 --- /dev/null +++ b/tests/hashtable/.gitignore @@ -0,0 +1,3 @@ +hashtest +hashtest.exe + diff --git a/tests/hashtable/Makefile b/tests/hashtable/Makefile new file mode 100644 index 000000000..833e1074f --- /dev/null +++ b/tests/hashtable/Makefile @@ -0,0 +1,21 @@ +EXE = $(if $(findstring Windows,$(OS)),.exe,) +CFLAGS += -Wall -Werror -std=gnu11 +CFLAGS += -MMD -MP + +all: hashtest$(EXE) + +hashtest$(EXE): hashtest.c ../../src/hashtable.c + @echo " [CC] $<" + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< + +test: hashtest$(EXE) + @echo " [RUN] $<" + ./$< + +ifneq ($(V),1) +.SILENT: +endif +.PHONY: all test + +-include hashtest.d + diff --git a/tests/hashtable/hashtest.c b/tests/hashtable/hashtest.c new file mode 100644 index 000000000..24bf47b50 --- /dev/null +++ b/tests/hashtable/hashtest.c @@ -0,0 +1,245 @@ +#include +#include +#include +#include +#include +#include "../../src/hashtable.c" + +void* my_loader(uint32_t key) { + return (void*)(uintptr_t)(0x1000 + key); +} + +/* Test data for hashtable_visit */ +typedef struct { + uint32_t expected_keys[10]; + int expected_refcounts[10]; + bool found_keys[10]; + size_t expected_count; + size_t actual_count; + bool error; +} visit_data_t; + +static visit_data_t g_visit_data; + +void visit_collector(uint32_t key, void *value, int refcount) { + g_visit_data.actual_count++; + + // Check if this key is expected + bool found = false; + for (size_t i = 0; i < g_visit_data.expected_count; i++) { + if (g_visit_data.expected_keys[i] == key) { + if (g_visit_data.expected_refcounts[i] != refcount) { + printf("[ERROR] Key %u has refcount %d, expected %d\n", key, refcount, g_visit_data.expected_refcounts[i]); + g_visit_data.error = true; + return; + } + if (g_visit_data.found_keys[i]) { + printf("[ERROR] Key %u visited multiple times\n", key); + g_visit_data.error = true; + return; + } + g_visit_data.found_keys[i] = true; + found = true; + break; + } + } + + if (!found) { + printf("[ERROR] Unexpected key %u visited\n", key); + g_visit_data.error = true; + } +} + +/* Check internal consistency via lookup only */ +static void check_consistency_keys(hashtable_t *h, uint32_t *keys, size_t n) { + for (size_t i = 0; i < n; i++) { + void *v = hashtable_lookup(h, keys[i]); + if (!v) { + printf("[ERROR] Key %u expected but not found!\n", keys[i]); + exit(1); + } + } +} + +/* Test basic insert / refcount / remove operations */ +static int test_basic_operations(hashtable_t *h) { + printf("[TEST] Basic insert / refcount / remove...\n"); + hashtable_insert(h, 42, NULL); + void *v = hashtable_lookup(h, 42); + if (v != (void*)(uintptr_t)(0x1000 + 42)) { + printf("[ERROR] Wrong value for key 42!\n"); + return 1; + } + hashtable_insert(h, 42, v); // increment refcount + hashtable_remove(h, 42); + hashtable_remove(h, 42); + if (hashtable_lookup(h, 42) != NULL) { + printf("[ERROR] Key 42 should be removed!\n"); + return 1; + } + return 0; +} + +/* Test bulk insert and remove operations */ +static int test_bulk_operations(hashtable_t *h) { + printf("[TEST] Bulk insert & remove...\n"); + const size_t N = 5000; + uint32_t *keys = malloc(sizeof(uint32_t) * N); + for (size_t i = 0; i < N; i++) { + keys[i] = (uint32_t)(i + 1000); + hashtable_insert(h, keys[i], NULL); + } + check_consistency_keys(h, keys, N); + for (size_t i = 0; i < N; i++) hashtable_remove(h, keys[i]); + for (size_t i = 0; i < N; i++) + if (hashtable_lookup(h, keys[i]) != NULL) { + printf("[ERROR] Key %u should be removed!\n", keys[i]); + free(keys); + return 1; + } + + printf("[TEST] Re-insert previously removed keys...\n"); + for (size_t i = 0; i < N; i++) + hashtable_insert(h, keys[i], (void*)(uintptr_t)(0x1000 + keys[i])); + check_consistency_keys(h, keys, N); + + printf("[TEST] Removing all reinserted keys...\n"); + for (size_t i = 0; i < N; i++) hashtable_remove(h, keys[i]); + free(keys); + return 0; +} + +/* Test massive resize stress */ +static int test_resize_stress(hashtable_t *h) { + printf("[TEST] Massive resize stress...\n"); + const size_t M = 20000; + for (size_t i = 0; i < M; i++) + hashtable_insert(h, (uint32_t)(i + 0xABCDE0), NULL); + for (size_t i = 0; i < M; i++) + hashtable_remove(h, (uint32_t)(i + 0xABCDE0)); + return 0; +} + +/* Test random stress with balanced operations */ +static int test_random_stress(hashtable_t *h) { + printf("[TEST] Random stress (balanced ops, bounded refcount)...\n"); + const size_t OPS = 50000, KEYSPACE = 2000; + size_t inserts = 0, removes = 0; + for (size_t i = 0; i < OPS; i++) { + uint32_t key = (rand() % KEYSPACE) + 1; // Avoid key 0 (EMPTY_KEY) + void *val = hashtable_lookup(h, key); + + // Bias towards insert when table is empty, remove when getting full + bool should_insert = (!val) || (h->size < KEYSPACE/4 && (rand() % 3 != 0)); + + if (should_insert && (!val || ((uintptr_t)val & 0xFFFFFF) == PhysicalAddr(val))) { + hashtable_insert(h, key, val); + inserts++; + } else if (val) { + hashtable_remove(h, key); + removes++; + } + } + printf("[INFO] Performed %zu inserts, %zu removes\n", inserts, removes); + return 0; +} + +/* Test hashtable_visit functionality */ +static int test_visit_functionality(hashtable_t *h) { + printf("[TEST] hashtable_visit functionality...\n"); + // Clear the table and add some known keys with different refcounts + for (size_t i = 0; i < h->capacity * 2; i += 2) { + if (h->entries[i] != 0 && h->entries[i] != 0xFFFFFFFF) { + hashtable_remove(h, h->entries[i]); + } + } + + // Add test keys with known refcounts + hashtable_insert(h, 100, NULL); // refcount 1 + hashtable_insert(h, 200, NULL); // refcount 1 + hashtable_insert(h, 200, hashtable_lookup(h, 200)); // refcount 2 + hashtable_insert(h, 300, NULL); // refcount 1 + hashtable_insert(h, 300, hashtable_lookup(h, 300)); // refcount 2 + hashtable_insert(h, 300, hashtable_lookup(h, 300)); // refcount 3 + + // Setup expected data for visit test + g_visit_data.expected_keys[0] = 100; + g_visit_data.expected_keys[1] = 200; + g_visit_data.expected_keys[2] = 300; + g_visit_data.expected_refcounts[0] = 1; + g_visit_data.expected_refcounts[1] = 2; + g_visit_data.expected_refcounts[2] = 3; + g_visit_data.expected_count = 3; + g_visit_data.actual_count = 0; + g_visit_data.error = false; + for (size_t i = 0; i < 10; i++) g_visit_data.found_keys[i] = false; + + // Call hashtable_visit + hashtable_visit(h, visit_collector); + + // Check results + if (g_visit_data.error) { + return 1; + } + + if (g_visit_data.actual_count != g_visit_data.expected_count) { + printf("[ERROR] Expected to visit %zu keys, but visited %zu\n", g_visit_data.expected_count, g_visit_data.actual_count); + return 1; + } + + for (size_t i = 0; i < g_visit_data.expected_count; i++) { + if (!g_visit_data.found_keys[i]) { + printf("[ERROR] Key %u was not visited\n", g_visit_data.expected_keys[i]); + return 1; + } + } + return 0; +} + +static int test_tombstone_full(hashtable_t *h) { + printf("[TEST] Tombstone full...\n"); + hashtable_t ht; + hashtable_init(&ht, 4, NULL); + + hashtable_insert(&ht, 1, (void*)123); + hashtable_remove(&ht, 1); + hashtable_insert(&ht, 1, (void*)123); + + hashtable_insert(&ht, 2, (void*)234); + hashtable_remove(&ht, 2); + hashtable_insert(&ht, 2, (void*)234); + + hashtable_insert(&ht, 3, (void*)345); + hashtable_remove(&ht, 3); + hashtable_insert(&ht, 3, (void*)345); + + hashtable_insert(&ht, 4, (void*)456); + hashtable_remove(&ht, 4); + hashtable_insert(&ht, 4, (void*)456); + + if (ht.size != 4) { + printf("[ERROR] Expected size 4, but got %zu\n", ht.size); + return 1; + } + + hashtable_free(&ht); + return 0; +} + +int main(void) { + srand((unsigned)time(NULL)); + hashtable_t h; + hashtable_init(&h, 8, my_loader); + + // Run all tests + if (test_basic_operations(&h) != 0) return 1; + if (test_bulk_operations(&h) != 0) return 1; + if (test_resize_stress(&h) != 0) return 1; + if (test_random_stress(&h) != 0) return 1; + if (test_visit_functionality(&h) != 0) return 1; + if (test_tombstone_full(&h) != 0) return 1; + + printf("[TEST] ALL TESTS PASSED. Final size=%zu\n", h.size); + hashtable_free(&h); + return 0; +} diff --git a/tests/preview/Makefile b/tests/preview/Makefile new file mode 100644 index 000000000..b9406a864 --- /dev/null +++ b/tests/preview/Makefile @@ -0,0 +1,21 @@ +# Do not set LIBDRAGON_PREVIEW here: the checks need N64_CFLAGS to be exactly +# what a stable project gets, and add -DLIBDRAGON_PREVIEW by themselves for the +# cases that are expected to opt in. +# FIXME: in the merge phase, preview has LIBDRAGON_PREVIEW=2, so let's force it +# back to 0 which is what tests expect +LIBDRAGON_PREVIEW=0 +include $(N64_INST)/include/n64.mk + +# -ffile-prefix-map carries an embedded quoted path, which would not survive +# being passed down and re-split by the shell. It is irrelevant here anyway, +# as nothing is actually code generated. +CHECK_CFLAGS = $(filter-out -ffile-prefix-map=%,$(N64_CFLAGS)) + +test: + @echo " [RUN] preview" + N64_CC="$(N64_CC)" N64_CFLAGS="$(CHECK_CFLAGS)" $(SOURCE_DIR)/check.sh + +ifneq ($(V),1) +.SILENT: +endif +.PHONY: test diff --git a/tests/preview/check.sh b/tests/preview/check.sh new file mode 100644 index 000000000..26283603e --- /dev/null +++ b/tests/preview/check.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash + +# Check that preview APIs are locked for a stable project, warned-about for +# LIBDRAGON_PREVIEW=1, and fully unlocked for LIBDRAGON_PREVIEW=2. +# +# This is what makes the single-branch model safe to rely on: a stable project +# must keep building even though the headers it includes also declare preview +# APIs, and it must not be able to reach those APIs by accident. + +set -uo pipefail + +: "${N64_CC:?}" +: "${N64_CFLAGS:?}" + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +failures=0 + +# build +# Compiles a snippet, leaving the diagnostics in $output and the compiler exit +# code in $status. This has to be a real compilation rather than -fsyntax-only, +# as the error attribute is only reported once the compiler knows that the call +# survived optimization. +build() { + local preview="" + case "$1" in + warning) preview=-DLIBDRAGON_PREVIEW=1 ;; + preview) preview=-DLIBDRAGON_PREVIEW=2 ;; + esac + printf '%s\n' "$2" > "$tmp/snippet.c" + output=$($N64_CC $N64_CFLAGS $preview -fdiagnostics-color=never \ + -c -o "$tmp/snippet.o" "$tmp/snippet.c" 2>&1) + status=$? +} + +fail() { + echo "FAIL: $*" + failures=$((failures + 1)) +} + +# expect +expect() { + build "$1" "$4" + case "$2" in + compiles) + if [ $status -ne 0 ]; then + fail "[$1] $3: expected to compile" + echo "$output" + fi + ;; + rejected) + if [ $status -eq 0 ]; then + fail "[$1] $3: expected to be rejected, but it compiled" + fi + ;; + esac +} + +# expect_diagnostic +# Like expect, but only requires the preview message to be reported, without +# caring whether it was reported as an error or as a warning. +expect_diagnostic() { + build "$1" "$3" + if ! grep -q 'libdragon preview API' <<< "$output"; then + fail "[$1] $2: expected a preview diagnostic" + echo "$output" + fi +} + +# expect_warning +# Warning mode must both compile and emit a preview warning (not an error). +expect_warning() { + build warning "$2" + if [ $status -ne 0 ]; then + fail "[warning] $1: expected to compile" + echo "$output" + elif ! grep -q 'warning:.*libdragon preview API' <<< "$output"; then + fail "[warning] $1: expected a preview warning" + echo "$output" + fi +} + +# A stable project must keep building even though libdragon.h also declares +# preview APIs. +expect stable compiles "including libdragon.h" ' +#include +' +expect preview compiles "including libdragon.h" ' +#include +' +expect warning compiles "including libdragon.h" ' +#include +' + +# Headers that the user includes explicitly are locked as a whole. +for header in GL/gl.h GL/glu.h GL/gl_integration.h; do + expect stable rejected "including $header" " +#include <$header> +" + expect preview compiles "including $header" " +#include <$header> +" + expect_warning "including $header" " +#include <$header> +" +done + +# Single entry points reached through libdragon.h are locked one at a time. +expect stable rejected "calling a preview function" ' +#include +float f(void) { return model64_anim_get_length(0, "anim"); } +' +expect preview compiles "using a preview API" ' +#include +float f(void) { model64_t *m = model64_load("x"); return model64_anim_get_length(m, "anim"); } +' + +# Warning mode: the call must succeed, but still emit a preview diagnostic. +expect warning compiles "calling a preview function" ' +#include +float f(void) { return model64_anim_get_length(0, "anim"); } +' +expect_diagnostic warning "calling a preview function warns" ' +#include +float f(void) { return model64_anim_get_length(0, "anim"); } +' + +# Taking the address of a preview function sidesteps the error attribute, so +# only the deprecated backstop reports it. +expect_diagnostic stable "taking the address of a preview function" ' +#include +void *p = (void*)model64_free; +' + +# LIBDRAGON_PREVIEW_SYM uses unavailable on fields: layout stays visible for +# ABI reasons, but reading the field is an error without preview. +expect stable rejected "accessing a preview struct field" ' +#include +typedef struct { + int a; + LIBDRAGON_PREVIEW_SYM int b; +} probe_t; +int f(probe_t *p) { return p->b; } +' +expect preview compiles "accessing a preview struct field" ' +#include +typedef struct { + int a; + LIBDRAGON_PREVIEW_SYM int b; +} probe_t; +int f(probe_t *p) { return p->b; } +' +expect warning compiles "accessing a preview struct field" ' +#include +typedef struct { + int a; + LIBDRAGON_PREVIEW_SYM int b; +} probe_t; +int f(probe_t *p) { return p->b; } +' +expect_diagnostic warning "accessing a preview struct field warns" ' +#include +typedef struct { + int a; + LIBDRAGON_PREVIEW_SYM int b; +} probe_t; +int f(probe_t *p) { return p->b; } +' + +if [ $failures -ne 0 ]; then + echo "$failures preview check(s) failed" + exit 1 +fi diff --git a/tests/test_backtrace.c b/tests/test_backtrace.c index 2cca2c3f7..be7dcf337 100644 --- a/tests/test_backtrace.c +++ b/tests/test_backtrace.c @@ -12,7 +12,7 @@ int (*bt_invalid_func_ptr)(void) = (int(*)(void))0xECECECEC; int (*bt_misaligned_func_ptr)(void) = (int(*)(void))0x80010002; // Test functions defined in backtrace_test.S -int btt_end(void) +NOINLINE int btt_end(void) { memset(bt_buf, 0, sizeof(bt_buf)); bt_buf_len = backtrace(bt_buf, 32); @@ -86,7 +86,7 @@ void btt_start(TestContext *ctx, int (*func)(void), const char *expected[]) int i = 0; void cb(void *user, backtrace_frame_t *frame) { - //backtrace_frame_print(frame, stderr); debugf("\n"); + // backtrace_frame_print(frame, stderr); debugf("\n"); if (ctx->result == TEST_FAILED) return; if (expected[i] == NULL) return; ASSERT_EQUAL_STR(expected[i], frame->func, "invalid backtrace entry"); diff --git a/tests/test_cache.c b/tests/test_cache.c index 428402c00..9fa56f2fd 100644 --- a/tests/test_cache.c +++ b/tests/test_cache.c @@ -1,3 +1,4 @@ +#include void test_cache_invalidate(TestContext *ctx) { // Interrupts causing other code to run can easily invalidate cache and make @@ -5,8 +6,20 @@ void test_cache_invalidate(TestContext *ctx) { disable_interrupts(); DEFER(enable_interrupts()); - uint8_t buf[128] __attribute__((aligned(16))); - const char *dfs_header = "\xde\xad\xbe\xef\xff\xff\xff\xff""DragonFS 2.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + const int BUF_SIZE = 64; + uint8_t *buf = memalign(16, BUF_SIZE); + DEFER(free(buf)); + + pi_addr_t rom = dfs_rom_addr("counter.dat"); + ASSERT(rom != 0, "counter.dat not found"); + + // First 32 bytes of counter.dat (incrementing sequence). + static const uint8_t counter32[32] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + }; const char *aaa = "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"; for (int i=0;i<32;i++) { @@ -14,20 +27,20 @@ void test_cache_invalidate(TestContext *ctx) { // Read/write the whole buffer through cache, // so it's all populated in D-Cache. - memset(buf, 0xA0, sizeof(buf)); - for (int i=0;i= i/16 && c <= (i+j-1)/16); @@ -39,8 +52,8 @@ void test_cache_invalidate(TestContext *ctx) { // ASSERT_EQUAL_MEM to provide error reporting. if (should_be_invalidated) { for (int k=0;k<16;k++) { - if (buf[c*16+k] != (uint8_t)dfs_header[c%2*16+k]) { - ASSERT_EQUAL_MEM(buf+c*16, (uint8_t*)dfs_header+c%2*16, 16, + if (buf[c*16+k] != counter32[c%2*16+k]) { + ASSERT_EQUAL_MEM(buf+c*16, counter32+c%2*16, 16, "unexpected data in invalidated cacheline %d (%d/%d)", c, i, j); } } diff --git a/tests/test_constructors.c b/tests/test_constructors.c index 921d6687a..996889562 100644 --- a/tests/test_constructors.c +++ b/tests/test_constructors.c @@ -1,6 +1,19 @@ extern unsigned int __global_cpp_constructor_test_value; unsigned int __global_constructor_test_value; +unsigned int __global_constructor_test_value_old; +unsigned int __global_constructor_test_prio; + +__attribute__((constructor(123))) void __global_constructor_test_prio1(void) +{ + __global_constructor_test_prio = 0xC0C70123; +} + +__attribute__((constructor(125))) void __global_constructor_test_prio2(void) +{ + __global_constructor_test_value_old = __global_constructor_test_value; + __global_constructor_test_prio = 0xE0C70125; +} __attribute__((constructor)) void __global_constructor_test() { @@ -10,4 +23,5 @@ __attribute__((constructor)) void __global_constructor_test() void test_constructors(TestContext *ctx) { ASSERT(__global_constructor_test_value == 0xC0C70125, "Global constructors did not get executed!"); ASSERT(__global_cpp_constructor_test_value == 0xD0C70125, "Global C++ constructors did not get executed!"); + ASSERT(__global_constructor_test_prio == 0xE0C70125 && __global_constructor_test_value_old == 0, "Global constructors with priority don't work correctly!"); } \ No newline at end of file diff --git a/tests/test_dfs.c b/tests/test_dfs.c index adef9c7f3..f598a315c 100644 --- a/tests/test_dfs.c +++ b/tests/test_dfs.c @@ -80,11 +80,92 @@ void test_dfs_rom_addr(TestContext *ctx) { uint32_t rom = dfs_rom_addr("counter.dat"); ASSERT(rom != 0, "counter.dat not found by dfs_rom_addr"); - ASSERT_EQUAL_HEX(io_read(rom), *(uint32_t*)buf1, "direct ROM address is different"); - ASSERT_EQUAL_HEX(io_read(rom+8), *(uint32_t*)(buf1+8), "direct ROM address is different"); + int offset = rom & 2; + ASSERT_EQUAL_HEX(io_read(rom+offset), *(u_uint32_t*)(buf1+offset), "direct ROM address is different"); + ASSERT_EQUAL_HEX(io_read(rom+offset+8), *(u_uint32_t*)(buf1+offset+8), "direct ROM address is different"); - dma_read(buf2, rom, 128); data_cache_hit_invalidate(buf2, sizeof(buf2)); + dma_read(buf2, rom, 128); ASSERT_EQUAL_MEM(buf1, buf2, 128, "DMA ROM access is different"); } + +void test_dfs_rom_size(TestContext *ctx) { + int fh = dfs_open("counter.dat"); + ASSERT(fh >= 0, "counter.dat not found"); + DEFER(dfs_close(fh)); + + int dfs_file_size = dfs_size(fh); + ASSERT(dfs_file_size >= 0, "Unable to get size of counter.dat"); + + int rom_file_size = dfs_rom_size("counter.dat"); + ASSERT_EQUAL_SIGNED(dfs_file_size, rom_file_size, "dfs_rom_size returns a different size from dfs_file_size"); +} + +void test_dfs_ioctl(TestContext *ctx) { + FILE *file = fopen("rom:/counter.dat", "rb"); + ASSERT(file, "counter.dat not found"); + DEFER(fclose(file)); + pi_addr_t rom_addr = 0; + int ret = ioctl(fileno(file), IODFS_GET_ROM_BASE, &rom_addr); + ASSERT(ret >= 0, "DFS ioctl failed"); + ASSERT(rom_addr == (dfs_rom_addr("counter.dat") & 0x1FFFFFFF), "IODFS_GET_ROM_BASE ioctl returns wrong address"); +} + +typedef struct { + char path[256]; + int type; +} list_files_data_t; + +static int list_files(const char *path, list_files_data_t *files, int *count) { + *count = 0; + int list_files(const char *path, dir_t *dir, void *data) { + list_files_data_t *list = (list_files_data_t *)data; + strcpy(list[*count].path, path); + list[*count].type = dir->d_type; + (*count)++; + return DIR_WALK_CONTINUE; + } + return dir_walk(path, list_files, files); +} + +static int list_files_compare(const void *a, const void *b) { + return strcmp(((list_files_data_t *)a)->path, ((list_files_data_t *)b)->path); +} + +void test_dfs_directory(TestContext *ctx) { + list_files_data_t files[1024]; + int count = 0; int err; + + err = list_files("nofs", files, &count); + ASSERT(err == -1, "list_files should have failed (ret:%d, errno:%d - %s)", err, errno, strerror(errno)); + + err = list_files("nofs:/test_dir", files, &count); + ASSERT(err == -1, "list_files should have failed (ret:%d, errno:%d - %s)", err, errno, strerror(errno)); + + err = list_files("rom:/test_dir", files, &count); + ASSERT(err == 0, "list_files failed (ret:%d, errno:%d - %s)", err, errno, strerror(errno)); + + for (int i=0;irelocs == NULL && ((dl_module_t *)handle)->num_relocs == 0, "load-time relocations still resident"); + scratch_get_stats(&after); + ASSERT(after.live_blocks == before.live_blocks, "scratch blocks leaked after DSO load"); //Find required symbols to test relocations uint32_t *hilo = dlsym(handle, "dl_test_hilo_reloc"); uint32_t *jump = dlsym(handle, "dl_test_jump_reloc"); diff --git a/tests/test_dma.c b/tests/test_dma.c index 0baab375e..2d41831e6 100644 --- a/tests/test_dma.c +++ b/tests/test_dma.c @@ -2,7 +2,7 @@ void test_dma_read_misalign(TestContext *ctx) { uint32_t rom = dfs_rom_addr("counter.dat"); - uint8_t rom_copy[4096] __attribute__((aligned(8))); + uint8_t rom_copy[4096] __attribute__((aligned(16))); uint8_t *ram = memalign(0x1000, 8192); DEFER(free(ram)); diff --git a/tests/test_exception.c b/tests/test_exception.c index 7736186a6..f9a3e2f68 100644 --- a/tests/test_exception.c +++ b/tests/test_exception.c @@ -68,8 +68,6 @@ ASSERT_REG_GP_HANDLER(no, value); \ }) -extern const void test_break_label; - void test_exception(TestContext *ctx) { // Bring FCR31 to a known state as some fp operations setting the inexact op flag uint32_t known_fcr31 = C1_FCR31(); @@ -79,7 +77,7 @@ void test_exception(TestContext *ctx) { uint64_t registers_after_ex[32]; uint64_t fp_registers_after_ex[32]; uint64_t lo, hi; - volatile int breakpoint_occured = 0; + volatile int exception_occurred = 0; reg_block_t exception_regs; // This is only used to make sure we break after setting all the registers @@ -127,9 +125,9 @@ void test_exception(TestContext *ctx) { exception_regs = *ex->regs; switch(ex->code) { - case EXCEPTION_CODE_BREAKPOINT: - breakpoint_occured++; - ex->regs->epc = ex->regs->epc + 4; + case EXCEPTION_CODE_TLB_LOAD_I_MISS: + exception_occurred++; + ex->regs->epc += 4; break; default: exception_default_handler(ex); @@ -140,7 +138,7 @@ void test_exception(TestContext *ctx) { register_exception_handler(ex_handler); DEFER(register_exception_handler(exception_default_handler)); - ASSERT_EQUAL_SIGNED(breakpoint_occured, 0, "Breakpoint triggered early"); + ASSERT_EQUAL_SIGNED(exception_occurred, 0, "Exception triggered early"); // Set as many registers as possible to known values before the ex. SET_REG(0, 00); @@ -202,7 +200,8 @@ void test_exception(TestContext *ctx) { // Make sure we trigger the exception after setting all the registers // dependency may not be necessary in practice but let's inform GCC // just in case. - asm volatile("test_break_label: \nbreak" ::"X"(dependency)); + extern const void test_exception_opcode; + asm volatile("test_exception_opcode: lw $0,0($0)" ::"X"(dependency)); // Read all registers to mem at once GET_REG(0, 00); @@ -263,7 +262,7 @@ void test_exception(TestContext *ctx) { :"$26" ); - ASSERT_EQUAL_SIGNED(breakpoint_occured, 1, "Breakpoint exception was not triggered"); + ASSERT_EQUAL_SIGNED(exception_occurred, 1, "Exception was not triggered"); ASSERT_REG(0, 00); ASSERT_REG(1, 01); @@ -313,11 +312,11 @@ void test_exception(TestContext *ctx) { ASSERT_EQUAL_HEX(exception_regs.hi, 0xBEEFF00DBEEFF00D, "hi not available to the handler"); // Other info - ASSERT_EQUAL_HEX(exception_regs.epc, (uint32_t)&test_break_label, "EPC not available to the handler"); + ASSERT_EQUAL_HEX(exception_regs.epc, (uint32_t)&test_exception_opcode, "EPC not available to the handler"); // If the other tests change SR these may fail unnecessarily, but we expect tests to do proper cleanup ASSERT_EQUAL_HEX(exception_regs.sr, 0x241014E3, "SR not available to the handler"); - ASSERT_EQUAL_HEX(exception_regs.cr, 0x24, "CR not available to the handler"); + ASSERT_EQUAL_HEX(exception_regs.cr, 0x8, "CR not available to the handler"); ASSERT_EQUAL_HEX(exception_regs.fc31, 0x0, "FCR31 not available to the handler"); } diff --git a/tests/test_fm.c b/tests/test_fm.c index c57d2fcb6..b290e2765 100644 --- a/tests/test_fm.c +++ b/tests/test_fm.c @@ -31,6 +31,16 @@ void test_fm_floorf(TestContext *ctx) { } } +void test_fm_roundf(TestContext *ctx) { + float vals[] = { + 1.0f, 1.2f, 1.5f, 1.7f, -1.0f, -1.2f, -1.5f, -1.7f, + }; + for (int i = 0; i < 8; i++) { + float x = vals[i]; + ASSERT_EQUAL_FLOAT(roundf(x), fm_roundf(x), "x=%f", x); + } +} + void test_fm_fmodf(TestContext *ctx) { struct { float x, y; @@ -103,3 +113,33 @@ void test_fm_atan2f(TestContext *ctx) { x, y); } } + +void test_fm_expf(TestContext *ctx) { + for (float x = -10.0f; x < 10; x += 0.2f) { + float ex = expf(x); + ASSERT(fabsf(ex - fm_expf(x)) / ex < 0.03f, "x=%f", x); + } +} + +void test_fm_lerp_angle(TestContext *ctx) { + struct { + float a, b, t, f; + } vals[] = { + {0.0f, 1.0f, 0.5f, 0.5f}, {0.0f, 2 * M_PI, 0.5f, 0.0f}, + {0.0f, -2 * M_PI, 0.5f, 0.0f}, {1.0f, 0.0f, 0.5f, 0.5f}, + {2 * M_PI, 0.0f, 0.5f, 2 * M_PI}, {-2 * M_PI, 0.0f, 0.5f, -2 * M_PI}, + }; + for (int i = 0; i < 6; i++) { + float fa = vals[i].a, fb = vals[i].b, t = vals[i].t, f = vals[i].f; + ASSERT_EQUAL_FLOAT(fm_lerp_angle(fa, fb, t), f, "a=%f b=%f t=%f", a, b, + t); + } +} + +void test_fm_wrap_angle(TestContext *ctx) { + for (float x = -10.0f; x < 10; x += 0.2f) { + ASSERT(fabsf(fmodf(fmodf(x, 2 * M_PI) + 2 * M_PI, 2 * M_PI) - + fm_wrap_angle(x)) < 1e-6f, + "x=%f", x); + } +} diff --git a/tests/test_gl.c b/tests/test_gl.c new file mode 100644 index 000000000..05e6734ee --- /dev/null +++ b/tests/test_gl.c @@ -0,0 +1,251 @@ +#include +#include +#include +#include + +#undef ABS +#include "../src/GL/gl_internal.h" + +#define GL_INIT_SIZE(w,h) \ + RDPQ_INIT(); \ + surface_t test_surf = surface_alloc(FMT_RGBA16, w, h); \ + DEFER(surface_free(&test_surf)); \ + surface_t test_z = surface_alloc(FMT_RGBA16, w, h); \ + DEFER(surface_free(&test_z)); \ + gl_init(); \ + DEFER(gl_close()); \ + rdpq_attach(&test_surf, &test_z); \ + DEFER(rdpq_detach_wait()); \ + gl_context_begin(); \ + DEFER(gl_context_end()); + +#define GL_INIT() GL_INIT_SIZE(64, 64) + +void test_gl_clear(TestContext *ctx) +{ + uint32_t rect_count; + + GL_INIT(); + + debug_rdp_stream_init(); + + glClear(GL_COLOR_BUFFER_BIT); + glFinish(); + + rect_count = debug_rdp_stream_count_cmd(RDPQ_CMD_FILL_RECTANGLE + 0xC0); + ASSERT_EQUAL_UNSIGNED(rect_count, 1, "Wrong number of rectangles!"); + + debug_rdp_stream_reset(); + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glFinish(); + + rect_count = debug_rdp_stream_count_cmd(RDPQ_CMD_FILL_RECTANGLE + 0xC0); + ASSERT_EQUAL_UNSIGNED(rect_count, 2, "Wrong number of rectangles!"); +} + +void test_gl_draw_arrays(TestContext *ctx) +{ + GL_INIT(); + + debug_rdp_stream_init(); + + static const GLfloat vertices[] = { + 0.0f, 0.0f, + 0.5f, 0.0f, + 0.5f, 0.5f + }; + + glEnableClientState(GL_VERTEX_ARRAY); + glVertexPointer(2, GL_FLOAT, 0, vertices); + glDrawArrays(GL_TRIANGLES, 0, 3); + glFinish(); + + uint32_t tri_count = debug_rdp_stream_count_cmd(RDPQ_CMD_TRI_SHADE + 0xC0); + ASSERT_EQUAL_UNSIGNED(tri_count, 1, "Wrong number of triangles!"); +} + +void test_gl_draw_elements(TestContext *ctx) +{ + GL_INIT(); + + debug_rdp_stream_init(); + + static const GLfloat vertices[] = { + 0.0f, 0.0f, + 0.5f, 0.0f, + 0.5f, 0.5f + }; + + static const GLushort indices[] = {0, 1, 2}; + + glEnableClientState(GL_VERTEX_ARRAY); + glVertexPointer(2, GL_FLOAT, 0, vertices); + glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, indices); + glFinish(); + + uint32_t tri_count = debug_rdp_stream_count_cmd(RDPQ_CMD_TRI_SHADE + 0xC0); + ASSERT_EQUAL_UNSIGNED(tri_count, 1, "Wrong number of triangles!"); +} + +void test_gl_texture_completeness(TestContext *ctx) +{ + GL_INIT(); + + void run_test(int width, int height) + { + LOG("Testing %dx%d texture\n", width, height); + surface_t tex = surface_alloc(FMT_RGBA16, width, height); + DEFER(surface_free(&tex)); + + glEnable(GL_TEXTURE_2D); + GLuint handle = 0; + glGenTextures(1, &handle); + DEFER(glDeleteTextures(1, &handle)); + + glBindTexture(GL_TEXTURE_2D, handle); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glSurfaceTexImageN64(GL_TEXTURE_2D, 0, &tex, NULL); + glFinish(); + gl_texture_object_t *texobj = gl_get_active_texture(); + ASSERT(texobj != NULL, "No active texture object!"); + + // Check that the texture is not complete. + glBindTexture(GL_TEXTURE_2D, 0); + glFinish(); + ASSERT(!gl_tex_is_complete(texobj), "Texture should not be complete!"); + + for (int i=1; iresult == TEST_FAILED) return; + + // rectangle, pow-2 + run_test(64, 4); + if (ctx->result == TEST_FAILED) return; + + // square, non-pow-2 + run_test(24, 24); + if (ctx->result == TEST_FAILED) return; + + // rectangle, non-pow-2 + run_test(57, 17); + if (ctx->result == TEST_FAILED) return; +} + +void test_gl_list(TestContext *ctx) +{ + GL_INIT(); + + ASSERT(!glIsList(1), "List index should not be used before glGenLists"); + GLuint tri_dlist = glGenLists(1); + + ASSERT_EQUAL_UNSIGNED(tri_dlist, 1, "First display list index is expected to be 1 in this test"); + ASSERT(glIsList(tri_dlist), "List index should be used after glGenLists"); + + glCallList(tri_dlist); // no-op according to spec, must not crash + ASSERT(glIsList(tri_dlist), "List index should still be used after glCallList"); + + glNewList(tri_dlist, GL_COMPILE); + ASSERT(glIsList(tri_dlist), "List index should still be used after glNewList"); + + glBegin(GL_TRIANGLES); + glNormal3f(1, 1, 0); + glVertex3f(5, 5, 0); + + glColor3f(0, 1, 0); + glNormal3f(-1, 1, 0); + glVertex3f(-5, 5, 0); + + glColor3f(0, 0, 1); + glNormal3f(1, -1, 0); + glVertex3f(5, -5, 0); + glEnd(); + + glEndList(); + + ASSERT(glIsList(tri_dlist), "List index should still be used after glEndList"); + + glCallList(100); // no-op according to spec, must not crash + + glNewList(100, GL_COMPILE); + ASSERT(!glIsList(100), "List index should not be used after glNewList without allocating it first with glGenLists"); + + glCallList(100); // no-op according to spec, must not crash + + glBegin(GL_TRIANGLES); + glVertex3f(0, 0, 0); + glVertex3f(1, 0, 0); + glVertex3f(0, 1, 0); + glEnd(); + + glEndList(); + ASSERT(glIsList(100), "List index should be used after glEndList without allocating it first with glGenLists"); +} + +void test_gl_cull(TestContext *ctx) +{ + GL_INIT(); + debug_rdp_stream_init(); + + void draw_tri(void) { + glBegin(GL_TRIANGLES); + glVertex3f(0, 0, 0); + glVertex3f(1, 0, 0); + glVertex3f(0, 1, 0); + glEnd(); + rspq_wait(); + } + + debug_rdp_stream_reset(); + glEnable(GL_CULL_FACE); + glCullFace(GL_FRONT); + draw_tri(); + uint32_t tri_count = debug_rdp_stream_count_cmd(RDPQ_CMD_TRI_SHADE + 0xC0); + ASSERT_EQUAL_UNSIGNED(tri_count, 0, "Triangle should not be drawn when culling front faces"); + + debug_rdp_stream_reset(); + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + draw_tri(); + tri_count = debug_rdp_stream_count_cmd(RDPQ_CMD_TRI_SHADE + 0xC0); + ASSERT_EQUAL_UNSIGNED(tri_count, 1, "Triangle should be drawn when culling back faces"); + + debug_rdp_stream_reset(); + glEnable(GL_CULL_FACE); + glCullFace(GL_FRONT_AND_BACK); + draw_tri(); + tri_count = debug_rdp_stream_count_cmd(RDPQ_CMD_TRI_SHADE + 0xC0); + ASSERT_EQUAL_UNSIGNED(tri_count, 0, "Triangle should not be drawn when culling front and back faces"); + + debug_rdp_stream_reset(); + glDisable(GL_CULL_FACE); + glCullFace(GL_BACK); + draw_tri(); + glCullFace(GL_FRONT); + draw_tri(); + glCullFace(GL_FRONT_AND_BACK); + draw_tri(); + tri_count = debug_rdp_stream_count_cmd(RDPQ_CMD_TRI_SHADE + 0xC0); + ASSERT_EQUAL_UNSIGNED(tri_count, 3, "Triangles should be drawn when culling disabled"); +} diff --git a/tests/test_h264.c b/tests/test_h264.c new file mode 100644 index 000000000..96828b938 --- /dev/null +++ b/tests/test_h264.c @@ -0,0 +1,1344 @@ +#define LIBDRAGON_PROFILE 0 +#include +#include +#include +#include +#include +#include "../src/video/fastcache.h" +#include "../src/video/rsph264_internal.h" +#include "../src/video/h264_decoder/h264bsd_macroblock_layer.h" +#include "../src/video/h264_decoder/h264bsd_util.h" +#include "../src/video/h264_decoder/h264bsd_stream.h" + +// Include files for the reference implementation. +#include "../src/video/h264_decoder/omxdl/armVC.h" +#if 1 +#include "../src/video/h264_decoder/omxdl/armCOMM.c" +#include "../src/video/h264_decoder/omxdl/armVCCOMM_Average.c" +#include "../src/video/h264_decoder/omxdl/armVCM4P10_Interpolate_Luma.c" +#include "../src/video/h264_decoder/omxdl/armVCM4P10_Interpolate_Chroma.c" +#include "../src/video/h264_decoder/omxdl/armVCM4P10_InterpolateHalfHor_Luma.c" +#include "../src/video/h264_decoder/omxdl/armVCM4P10_InterpolateHalfVer_Luma.c" +#include "../src/video/h264_decoder/omxdl/armVCM4P10_InterpolateHalfDiag_Luma.c" +#include "../src/video/h264_decoder/omxdl/omxVCM4P10_PredictIntra_4x4.c" +#include "../src/video/h264_decoder/omxdl/omxVCM4P10_PredictIntra_16x16.c" +#include "../src/video/h264_decoder/omxdl/omxVCM4P10_PredictIntraChroma_8x8.c" +#include "../src/video/h264_decoder/omxdl/omxVCM4P10_TransformDequantChromaDCFromPair.c" +#include "../src/video/h264_decoder/omxdl/omxVCM4P10_DequantTransformResidualFromPairAndAdd.c" +#endif + +#define SRC_SIZE 256 +#define SRC_PITCH (256+64*2) +#define DST_SIZE 512 + +#define INTERPOLATE_CHROMA 0 +#define INTERPOLATE_LUMA 1 + +#define INTRAPRED_CHROMA_8X8 0 +#define INTRAPRED_PROCESS_LUMA4 1 +#define INTRAPRED_PROCESS_LUMA16 2 + +#define OMX_LUMA_4x4 0 +#define OMX_CHROMADC_2x2 1 +#define PROCESS_LUMA_16x16 3 +#define PROCESS_CHROMA_8x8x2 4 +// Same as PROCESS_CHROMA_8x8x2, but with the intra overlay loaded: the command +// is assembled into both overlays and the CPU sends it to whichever one is +// current, so both instantiations need to be checked. +#define PROCESS_CHROMA_8x8x2_INTRA 5 + +typedef struct { + uint8_t *pSrc1, *pSrc2; // source buffers + uint8_t *pDst1, *pDst2; // destination buffers +} BufferTest; + +typedef struct { + BufferTest buf; + int func; + int x2, y2; + int qp; + uint8_t *src; + uint8_t ac[27] __attribute__((aligned(8))); + int16_t *dc; +} DequantTest; + +typedef struct { + BufferTest buf; // buffers + int func; // interpolation function to test + int x1, y1, x2, y2; // position in source/destination + int w, h; // macroblock size + int dx, dy; // fractional offset + int wp; // apply H.264 weightp after prediction + int wp_weight, wp_offset, wp_denom; +} InterpolationTest; + +typedef struct { + BufferTest buf; + int func; // intraprediction function to test + int x1, y1, x2, y2; // position in source/destination + int mode[16]; // intraprediction mode (for each partition) + int avail[16]; // neighbour availability (for each partition) + uint8_t *coeffs; // residual coefficients + uint8_t ac[27]; // residual presence + int qp; // quantization factor (for residuals) +} IntraPredictionTest; + +static uint32_t rand_state = 1; +static void my_srand(uint32_t val) { + if (val == 0) val = 1; + rand_state = val; +} +static uint32_t my_rand() { + uint32_t x = rand_state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + return rand_state = x; +} + +// Measure the time that takes to execute a statement. +#define TIME_STATEMENT(stmt) ({ \ + uint32_t start, stop; \ + start = TICKS_READ(); \ + stmt; \ + stop = TICKS_READ(); \ + TICKS_DISTANCE(start, stop); \ +}) + +// H.264 explicit weighted prediction (P-slice list0), matching ApplyWeightPart. +static void apply_weight(uint8_t *dst, int pitch, int w, int h, + int weight, int offset, int denom) +{ + int round = denom ? (1 << (denom - 1)) : 0; + for (int y = 0; y < h; y++) { + uint8_t *row = dst + y * pitch; + for (int x = 0; x < w; x++) { + int weighted = ((weight * row[x] + round) >> denom) + offset; + row[x] = CLIP1(weighted); + } + } +} + +// Configure the RSP with the weightp coefficients of the plane that the task +// under test will write. Standalone chroma tasks always use the Cb entry. +static void queue_test_weights(InterpolationTest *test) { + uint32_t w = test->wp ? + rsph264_weight_pack(test->wp_weight, test->wp_offset, test->wp_denom) : + RSPH264_WEIGHT_IDENTITY; + + if (test->func == INTERPOLATE_LUMA) + rsph264_queue_set_weights_if_changed(w, + RSPH264_WEIGHT_IDENTITY, RSPH264_WEIGHT_IDENTITY); + else + rsph264_queue_set_weights_if_changed(RSPH264_WEIGHT_IDENTITY, + w, RSPH264_WEIGHT_IDENTITY); +} + +bool interpolation_test(InterpolationTest* test, int verbose) { + uint8_t *src1 = test->buf.pSrc1+test->y1*SRC_PITCH+test->x1; + uint8_t *src2 = test->buf.pSrc2+test->y1*SRC_PITCH+test->x1; + + uint8_t *dst1 = test->buf.pDst1+test->y2*DST_SIZE+test->x2; + uint8_t *dst2 = test->buf.pDst2+test->y2*DST_SIZE+test->x2; + + int frame_width = SRC_SIZE; + int frame_height = SRC_SIZE; + + uint32_t rsp_time = 0, ref_time = 0; + queue_test_weights(test); + switch (test->func) { + case INTERPOLATE_LUMA: + rsph264_queue_debug_random_status(); + rsph264_sync(); + + rsp_time = TIME_STATEMENT({ + // Calculate the motion vector as the signed difference between + // x2/y2 and x1/y1. This is what happens in reality: the position + // is the one in the destination buffer, and the motion vector is + // the relative distance in the source buffer. + int16_t mvx = (((int16_t)test->x1 - (int16_t)test->x2) << 2) | test->dx; + int16_t mvy = (((int16_t)test->y1 - (int16_t)test->y2) << 2) | test->dy; + + rsph264_queue_interpolate_luma_overfill(RSPH264_CACHE_SKIP_SOURCE, + test->buf.pSrc1, SRC_PITCH, dst1, DST_SIZE, + (frame_width << 16) | frame_height, // frame size + (test->w << 16) | test->h, // block size + ((uint32_t)mvx << 16) | ((uint32_t)(mvy & 0xFFFF)), // motion vector + ((uint32_t)test->x2 << 16) | ((uint32_t)test->y2) // position + ); + + rsph264_sync(); + }); + + ref_time = TIME_STATEMENT({ + armVCM4P10_Interpolate_Luma( + src2, SRC_PITCH, dst2, DST_SIZE, + test->w, test->h, test->dx, test->dy); + if (test->wp) + apply_weight(dst2, DST_SIZE, test->w, test->h, + test->wp_weight, test->wp_offset, test->wp_denom); + }); + break; + + case INTERPOLATE_CHROMA: + rsph264_queue_debug_random_status(); + rsph264_sync(); + + rsp_time = TIME_STATEMENT({ + // Calculate the motion vector as the signed difference between + // x2/y2 and x1/y1. This is what happens in reality: the position + // is the one in the destination buffer, and the motion vector is + // the relative distance in the source buffer. + int16_t mvx = (((int16_t)test->x1 - (int16_t)test->x2) << 3) | test->dx; + int16_t mvy = (((int16_t)test->y1 - (int16_t)test->y2) << 3) | test->dy; + + rsph264_queue_interpolate_chroma_overfill(RSPH264_CACHE_SKIP_SOURCE, + test->buf.pSrc1, SRC_PITCH, dst1, DST_SIZE, + (frame_width << 16) | frame_height, // frame size + (test->w << 16) | test->h, // block size + ((uint32_t)mvx << 16) | ((uint32_t)(mvy & 0xFFFF)), // motion vector + ((uint32_t)test->x2 << 16) | ((uint32_t)test->y2) // position + ); + + rsph264_sync(); + }); + + ref_time = TIME_STATEMENT({ + armVCM4P10_Interpolate_Chroma( + src2, SRC_PITCH, dst2, DST_SIZE, + test->w, test->h, test->dx, test->dy); + if (test->wp) + apply_weight(dst2, DST_SIZE, test->w, test->h, + test->wp_weight, test->wp_offset, test->wp_denom); + }); + break; + + default: + assert(0); + } + + if (verbose >= 2) + debugf("sz:%d,%d d:%d,%d rsp:%ld ref:%ld\n", test->w, test->h, test->dx, test->dy, rsp_time, ref_time); + for (int j = -4; j < 32; j++) { + for (int i = -4; i < 32; i++) { + uint8_t *cdst = (uint8_t*)dst1; + if (cdst[j*DST_SIZE+i] != dst2[j*DST_SIZE+i]) { + if (verbose >= 1) { + printf("FAILED\n"); + printf("FAILED: sz:%d,%d d:%d,%d wp:%d,%d,%d\n", + test->w, test->h, test->dx, test->dy, + test->wp_weight, test->wp_offset, test->wp_denom); + printf("FAILED: difference at (%d,%d)\n", i, j); + printf("FAILED: src:(%d,%d) dst:(%d,%d)\n", test->x1, test->y1, test->x2, test->y2); + printf("FAILED: RSP=%02x REF=%02x\n", cdst[j*DST_SIZE+i], dst2[j*DST_SIZE+i]); + printf("SRC: RSP: REF:\n"); + printf("%02x %02x %02x %02x %02x %02x %02x %02x %02x\n", + src2[(j-1)*SRC_PITCH+i-1], src2[(j-1)*SRC_PITCH+i], src2[(j-1)*SRC_PITCH+i+1], + cdst[(j-1)*DST_SIZE+i-1], cdst[(j-1)*DST_SIZE+i], cdst[(j-1)*DST_SIZE+i+1], + dst2[(j-1)*DST_SIZE+i-1], dst2[(j-1)*DST_SIZE+i], dst2[(j-1)*DST_SIZE+i+1]); + printf("%02x %02x %02x %02x %02x %02x %02x %02x %02x\n", + src2[j*SRC_PITCH+i-1], src2[j*SRC_PITCH+i], src2[j*SRC_PITCH+i+1], + cdst[j*DST_SIZE+i-1], cdst[j*DST_SIZE+i], cdst[j*DST_SIZE+i+1], + dst2[j*DST_SIZE+i-1], dst2[j*DST_SIZE+i], dst2[j*DST_SIZE+i+1]); + printf("%02x %02x %02x %02x %02x %02x %02x %02x %02x\n", + src2[(j+1)*SRC_PITCH+i-1], src2[(j+1)*SRC_PITCH+i], src2[(j+1)*SRC_PITCH+i+1], + cdst[(j+1)*DST_SIZE+i-1], cdst[(j+1)*DST_SIZE+i], cdst[(j+1)*DST_SIZE+i+1], + dst2[(j+1)*DST_SIZE+i-1], dst2[(j+1)*DST_SIZE+i], dst2[(j+1)*DST_SIZE+i+1]); + } + return false; + } + } + } + return true; +} + +static uint32_t chroma_partitions[7][2] = { + { 8, 8 }, + { 8, 4 }, + { 4, 8 }, + { 4, 4 }, + { 2, 4 }, + { 4, 2 }, + { 2, 2 }, +}; + +void exhaustive_interpolation_test(InterpolationTest *test, int repetitions, int verbose) { + int max_dxy = (test->func == INTERPOLATE_LUMA) ? 4 : 8; + for (int p=0;p<7;p++) { + uint32_t w = chroma_partitions[p][0]; + uint32_t h = chroma_partitions[p][1]; + + if (test->func == INTERPOLATE_LUMA) { + w*=2; h*=2; + } + + for (uint32_t dy=0;dyw = w; + test->h = h; + + // Source coordinates can span also the overfill area (outside the screen). + // Technically, they could be any 12-bit signed value, but the reference + // implementation does not support overfilling and we fake it by pre-overfilling + // the source buffer (which has a limited size). + test->x1 = (my_rand() % (SRC_SIZE+64)) - 32; + test->y1 = (my_rand() % (SRC_SIZE+64)) - 32; + test->x2 = (my_rand() % (DST_SIZE-64)) + 32; + test->y2 = (my_rand() % (DST_SIZE-64)) + 32; + test->x2 &= ~(w-1); // destination buffer is always aligned to macroblock width + test->dx = dx; + test->dy = dy; + bool ok = interpolation_test(test, v); + if (!ok) { + printf("FAILED: nt=%d\n", nt); + abort(); + } + } + } + } + } +} + +int gen_coeff_delta(uint8_t *coeffs, int cmask, int max_coeff) { + int cidx = 0; + int ncoeffs = (my_rand() % (max_coeff-1)) + 1; + for (int i=0;i> 16; + } + } + return cidx; +} + +bool intrapred_test(IntraPredictionTest *test, int verbose) { + uint8_t *src1 = test->buf.pSrc1+test->y1*SRC_PITCH+test->x1; + uint8_t *src2 = test->buf.pSrc2+test->y1*SRC_PITCH+test->x1; + + uint8_t *dst1 = test->buf.pDst1+test->y2*DST_SIZE+test->x2; + uint8_t *dst2 = test->buf.pDst2+test->y2*DST_SIZE+test->x2; + + const uint8_t *csrc1 = test->coeffs; + const uint8_t *csrc2 = test->coeffs; + + uint32_t rsp_time = 0, ref_time = 0; + + switch (test->func) { + case INTRAPRED_CHROMA_8X8: + rsph264_queue_debug_random_status(); + rsph264_sync(); + + rsp_time = TIME_STATEMENT({ + rsph264_queue_intrapred_chroma_8x8(0, + src1-1, src1-SRC_PITCH, src1-SRC_PITCH-1, + dst1, SRC_PITCH, DST_SIZE, + test->mode[0], test->avail[0]); + rsph264_sync(); + }); + + ref_time = TIME_STATEMENT({ + int res = omxVCM4P10_PredictIntraChroma_8x8( + src2-1, src2-SRC_PITCH, src2-SRC_PITCH-1, + dst2, SRC_PITCH, DST_SIZE, + test->mode[0], test->avail[0]); + assert(res == 0); + }); + break; + + case INTRAPRED_PROCESS_LUMA4: + { + rsph264_queue_debug_random_status(); + rsph264_sync(); + + uint8_t modeAvail[16]; + for (int i=0;i<16;i++) { + // We only care about 4 availability bits: UPPER / LEFT / UPPER_LEFT / UPPER_RIGHT + uint8_t avail = test->avail[i] & 3; + if (test->avail[i] & (1<<5)) + avail |= (1<<2); + if (test->avail[i] & (1<<6)) + avail |= (1<<3); + modeAvail[i] = avail | (test->mode[i]<<4); + } + + rsp_time = TIME_STATEMENT({ + rsph264_queue_set_packed_delta_buffer(0, + csrc1); + rsph264_queue_process_luma_intra4_residual(0, + src1, dst1, SRC_PITCH, DST_SIZE, + modeAvail, test->qp, h264bsdTotalCoeffMask(test->ac)); + rsph264_sync(); + }); + + ref_time = TIME_STATEMENT({ + int res = HIGHFUNC_PredictIntraTransform_4x4( + src2, dst2, SRC_PITCH, DST_SIZE, + &csrc2, test->ac, + modeAvail, test->qp); + assert(res == 0); + }); + } + break; + + case INTRAPRED_PROCESS_LUMA16: + { + rsph264_queue_debug_random_status(); + rsph264_sync(); + + rsp_time = TIME_STATEMENT({ + rsph264_queue_set_packed_delta_buffer(0, + csrc1); + rsph264_queue_process_luma_intra16_residual(0, + src1, dst1, SRC_PITCH, DST_SIZE, + test->mode[0], test->avail[0], test->qp, h264bsdTotalCoeffMask(test->ac)); + rsph264_sync(); + }); + + ref_time = TIME_STATEMENT({ + int res = HIGHFUNC_ProcessLumaIntra16x16Residual( + src2, dst2, SRC_PITCH, DST_SIZE, + &csrc2, test->ac, + test->mode[0], test->avail[0], test->qp); + assert(res == 0); + }); + + break; + } + + default: + assert(0); + } + + if (verbose >= 2) + debugf("m:%d rsp:%ld ref:%ld\n", test->mode[0], rsp_time, ref_time); + for (int j = -4; j < 32; j++) { + for (int i = -4; i < 32; i++) { + uint8_t *cdst = (uint8_t*)dst1; + if (cdst[j*DST_SIZE+i] != dst2[j*DST_SIZE+i]) { + if (verbose >= 1) { + printf("FAILED\n"); + printf("FAILED: mode:%d avail:%x\n", test->mode[0], test->avail[0]); + printf("FAILED: difference at (%d,%d)\n", i, j); + printf("FAILED: src:(%d,%d) dst:(%d,%d)\n", test->x1, test->y1, test->x2, test->y2); + printf("FAILED: RSP=%02x REF=%02x\n", cdst[j*DST_SIZE+i], dst2[j*DST_SIZE+i]); + printf("SRC: RSP: REF:\n"); + printf("%02x %02x %02x %02x %02x %02x %02x %02x %02x\n", + src2[(j-1)*SRC_PITCH+i-1], src2[(j-1)*SRC_PITCH+i], src2[(j-1)*SRC_PITCH+i+1], + cdst[(j-1)*DST_SIZE+i-1], cdst[(j-1)*DST_SIZE+i], cdst[(j-1)*DST_SIZE+i+1], + dst2[(j-1)*DST_SIZE+i-1], dst2[(j-1)*DST_SIZE+i], dst2[(j-1)*DST_SIZE+i+1]); + printf("%02x %02x %02x %02x %02x %02x %02x %02x %02x\n", + src2[j*SRC_PITCH+i-1], src2[j*SRC_PITCH+i], src2[j*SRC_PITCH+i+1], + cdst[j*DST_SIZE+i-1], cdst[j*DST_SIZE+i], cdst[j*DST_SIZE+i+1], + dst2[j*DST_SIZE+i-1], dst2[j*DST_SIZE+i], dst2[j*DST_SIZE+i+1]); + printf("%02x %02x %02x %02x %02x %02x %02x %02x %02x\n", + src2[(j+1)*SRC_PITCH+i-1], src2[(j+1)*SRC_PITCH+i], src2[(j+1)*SRC_PITCH+i+1], + cdst[(j+1)*DST_SIZE+i-1], cdst[(j+1)*DST_SIZE+i], cdst[(j+1)*DST_SIZE+i+1], + dst2[(j+1)*DST_SIZE+i-1], dst2[(j+1)*DST_SIZE+i], dst2[(j+1)*DST_SIZE+i+1]); + } + return false; + } + } + } + return true; + +} + + +void exhaustive_intrapred_test(IntraPredictionTest *test, int repetitions, int verbose) { + static uint8_t coeffs[2048]; + int count = 0; + int nummodes = 0; + int numblocks = 0; + + switch (test->func) { + // Single-block intraprediction. + case INTRAPRED_CHROMA_8X8: nummodes = 4; numblocks = 1; break; + // This function covers the whole macroblock (with 16 4x4 blocks) + case INTRAPRED_PROCESS_LUMA4: nummodes = 9; numblocks = 16; break; + case INTRAPRED_PROCESS_LUMA16: nummodes = 4; numblocks = 16; break; + default: assert(0); + } + + for (int m=0;mx1 = (my_rand() % (SRC_SIZE-64)) + 32; + test->y1 = (my_rand() % (SRC_SIZE-64)) + 32; + test->x2 = (my_rand() % (DST_SIZE-64)) + 32; + test->y2 = (my_rand() % (DST_SIZE-64)) + 32; + + // Prepare data for residual coefficients + test->qp = my_rand()%48; // FIXME: should be 52 + int src_offset = my_rand() % 8; + int cidx = src_offset; + test->coeffs = coeffs + src_offset; + + // The bigger the shift factor, the smaller the coefficients, + // otherwise we risk overflow which is not correctly handled + // by the reference implementation (assuming it's permitted + // by the standard in the first place... I have not found + // information on this). + int cmask = 0x1FF >> (test->qp/6); + + memset(test->ac, 0, sizeof(test->ac)); + if (test->func == INTRAPRED_PROCESS_LUMA16) { + // For INTRAPRED_PROCESS_LUMA16, we first prepare also input data + // for LumaDC coefficients. + test->ac[24] = my_rand()%2; + if (test->ac[24] != 0) { + cidx += gen_coeff_delta(coeffs+cidx, cmask, 16); + } + } + + for (int b=0;bmode[b] = m; + if (b != 0 && my_rand() % 5 == 0) { + // Every once in a while, change mode for some block. + // This is actually quite common, so we want to cover + // the case in which not all blocks have the same mode. + // At the same time, the test is structured to test mode + // by mode, so don't do that very often. + test->mode[b] = my_rand()%nummodes; + } + test->avail[b] = OMX_VC_UPPER|OMX_VC_LEFT|OMX_VC_UPPER_LEFT; + + // 4x4 blocks within the macroblock are processed in this order: + // xx xx xx xx xx xx xx xx xx + // 0 1 8 9 + // 2 3 10 11 + // 4 5 12 13 + // 6 7 14 15 + // + // UPPER_RIGHT cannot be set: + // * On blocks 3 and 7 because when they are processed, + // their upper-right block (8 and 12) have not been + // processed yet. + // * On blocks 11, 13 and 15 because they are upper right + // data would be in the macroblock on the right which + // is not yet processed. + if (b != 3 && b != 7 && b != 11 && b != 13 && b != 15) { + if (my_rand()%2 == 0) + test->avail[b] |= OMX_VC_UPPER_RIGHT; + } + + if (test->func == INTRAPRED_CHROMA_8X8) { + test->x1 &= ~7; test->x2 &= ~7; + if (test->mode[b] == 0) { // OMX_VC_CHROMA_DC + if (my_rand()%2 == 0) + test->avail[b] &= ~OMX_VC_UPPER; + if (my_rand()%2 == 0) + test->avail[b] &= ~OMX_VC_LEFT; + } + } else if (test->func == INTRAPRED_PROCESS_LUMA4 || test->func == INTRAPRED_PROCESS_LUMA16) { + test->x1 &= ~15; test->x2 &= ~15; + if (test->mode[b] == 2) { // OMX_VC_4X4_DC + if (my_rand()%2 == 0) + test->avail[b] &= ~OMX_VC_UPPER; + if (my_rand()%2 == 0) + test->avail[b] &= ~OMX_VC_LEFT; + } + + // If there's LumaDC (only for INTRAPRED_PROCESS_LUMA16), + // then we have max 15 coefficients per block. The 16th + // is the (0,0) coeff we get from the LumaDC matrix. + int maxCoeff = test->ac[24] ? 15 : 16; + + // For INTRAPRED_PROCESS_LUMA4/16 we need to also + // create coefficients. + test->ac[b] = my_rand()%2; + if (test->ac[b] != 0) { + cidx += gen_coeff_delta(coeffs+cidx, cmask, maxCoeff); + } + } + } + + bool ok = intrapred_test(test, v); + if (!ok) { + printf("FAILED: test:%d\n", nt); + abort(); + } + } + } +} + +void overfill_interpolation_test(InterpolationTest *test, int verbose) { + int max_dxy = (test->func == INTERPOLATE_LUMA) ? 4 : 8; + for (int p=0;p<7;p++) { + for (int dx=0;dxfunc == INTERPOLATE_LUMA) { + w*=2; h*=2; + } + + test->w = w; + test->h = h; + test->dx = dx; + test->dy = dy; + test->x2 = 16; + test->y2 = 16; + + // Left border + for (int x = -24; x < 4; x++) { + test->x1 = x; + test->y1 = (x&3)-1; + bool ok = interpolation_test(test, verbose); + if (!ok) abort(); + } + + // Right border + for (int x = SRC_SIZE-w-4; x < SRC_SIZE+8; x++) { + test->x1 = x; + test->y1 = SRC_SIZE-1-(x&3); + bool ok = interpolation_test(test, verbose); + if (!ok) abort(); + } + + // Top border + for (int y = -24; y < 4; y++) { + test->x1 = SRC_SIZE-1-(y&3); + test->y1 = y; + bool ok = interpolation_test(test, verbose); + if (!ok) abort(); + } + + // Bottom border + for (int y = SRC_SIZE-w-4; y < SRC_SIZE+8; y++) { + test->x1 = (y&3)-1; + test->y1 = y; + bool ok = interpolation_test(test, verbose); + if (!ok) abort(); + } + } + } + } +} + +void weightp_interpolation_test(InterpolationTest *test, int verbose) { + static const int luma_sizes[][2] = { + {4,4}, {8,8}, {16,16}, {8,4}, {4,8}, {16,8}, {8,16}, + }; + static const int chroma_sizes[][2] = { + {2,2}, {4,4}, {8,8}, {4,2}, {2,4}, {8,4}, {4,8}, + }; + static const int params[][3] = { + { 1, 0, 0 }, // identity, denom 0 + { 64, 0, 6 }, // identity, denom 6 + { 128, 0, 7 }, // identity, denom 7 (default weight of a stream) + { 64, 10, 6 }, + { 80, -16, 6 }, + { 2, -32, 1 }, + { -1, 0, 7 }, + { 0, 50, 0 }, + {-128, 0, 0 }, + { 127, 0, 0 }, + { 127, 127, 0 }, // clip high + {-128, -128, 0 }, // clip low + { 127, 127, 7 }, + }; + const int (*sizes)[2] = (test->func == INTERPOLATE_LUMA) ? luma_sizes : chroma_sizes; + + test->wp = 1; + test->x1 = 40; + test->y1 = 40; + test->x2 = 32; + test->y2 = 32; + + int max_dxy = (test->func == INTERPOLATE_LUMA) ? 4 : 8; + + for (int s = 0; s < 7; s++) { + test->w = sizes[s][0]; + test->h = sizes[s][1]; + + // All the weight combinations, on a plain copy and on an interpolation + for (unsigned p = 0; p < sizeof(params)/sizeof(params[0]); p++) { + test->wp_weight = params[p][0]; + test->wp_offset = params[p][1]; + test->wp_denom = params[p][2]; + + test->dx = 0; + test->dy = 0; + if (!interpolation_test(test, verbose)) + abort(); + + test->dx = max_dxy/2; + test->dy = max_dxy/4; + if (!interpolation_test(test, verbose)) + abort(); + } + + // All the interpolation paths, with a single weight combination: + // the weights are applied after interpolation, so the two are + // independent, but each path writes the output buffer differently. + test->wp_weight = 80; + test->wp_offset = -16; + test->wp_denom = 6; + for (test->dy = 0; test->dy < max_dxy; test->dy++) { + for (test->dx = 0; test->dx < max_dxy; test->dx++) { + if (!interpolation_test(test, verbose)) + abort(); + } + } + } + test->wp = 0; +} + +static bool weightp_rect_equal(uint8_t *rsp, uint8_t *ref, int pitch, int w, int h, int verbose) { + for (int j = 0; j < h; j++) { + for (int i = 0; i < w; i++) { + if (rsp[j*pitch + i] != ref[j*pitch + i]) { + if (verbose >= 1) { + printf("FAILED: difference at (%d,%d) RSP=%02x REF=%02x\n", + i, j, rsp[j*pitch + i], ref[j*pitch + i]); + } + return false; + } + } + } + return true; +} + +// Weights of the three planes for one all-overfill test case. A denominator +// is shared by the two chroma planes, as in a slice header. +typedef struct { + int luma_w, luma_o, luma_d; + int cb_w, cb_o, cr_w, cr_o, chroma_d; + int dx, dy; // fractional part of the luma motion vector +} WeightpCase; + +static void weightp_all_overfill_case(InterpolationTest *test, + const WeightpCase *wc, int verbose) +{ + const int w = 16, h = 16; + const int x1 = 40, y1 = 40; + const int x2 = 32, y2 = 32; + const int luma_w = wc->luma_w, luma_o = wc->luma_o, luma_d = wc->luma_d; + const int cb_w = wc->cb_w, cb_o = wc->cb_o; + const int cr_w = wc->cr_w, cr_o = wc->cr_o, chroma_d = wc->chroma_d; + + // The RSP finds the chroma planes right after the luma one, that is at + // frame_width*frame_height. This frame size is much smaller than the + // source buffer, and is chosen so that the chroma planes fall within the + // area where the two source buffers are identical: only pSrc2 has been + // pre-overfilled (the RSP does the overfilling by itself). + int frame_width = 192; + int frame_height = 72; + int16_t mvx = ((int16_t)(x1 - x2) << 2) | wc->dx; + int16_t mvy = ((int16_t)(y1 - y2) << 2) | wc->dy; + + // The chroma planes see the same motion vector with one more fractional + // bit, because of subsampling. + int chroma_dx = mvx & 7, chroma_dy = mvy & 7; + + uint8_t *src_luma_ref = test->buf.pSrc2 + y1*SRC_PITCH + x1; + uint8_t *src_cb_ref = test->buf.pSrc2 + frame_width*frame_height + (y1/2)*(SRC_PITCH/2) + (x1/2); + uint8_t *src_cr_ref = src_cb_ref + (frame_width/2)*(frame_height/2); + + uint8_t *dst_luma_rsp = test->buf.pDst1 + y2*DST_SIZE + x2; + uint8_t *dst_luma_ref = test->buf.pDst2 + y2*DST_SIZE + x2; + uint8_t *dst_cb_rsp = test->buf.pDst1 + 200*DST_SIZE + x2; + uint8_t *dst_cb_ref = test->buf.pDst2 + 200*DST_SIZE + x2; + uint8_t *dst_cr_rsp = test->buf.pDst1 + 220*DST_SIZE + x2; + uint8_t *dst_cr_ref = test->buf.pDst2 + 220*DST_SIZE + x2; + + // Fill the chroma destinations with a known pattern, so that pixels left + // untouched by the RSP are easy to spot. + memset(test->buf.pDst1 + 200*DST_SIZE, 0xAA, 24*DST_SIZE); + memset(test->buf.pDst2 + 200*DST_SIZE, 0xAA, 24*DST_SIZE); + + rsph264_queue_set_weights_if_changed( + rsph264_weight_pack(luma_w, luma_o, luma_d), + rsph264_weight_pack(cb_w, cb_o, chroma_d), + rsph264_weight_pack(cr_w, cr_o, chroma_d)); + + rsph264_queue_interpolate_all_overfill(RSPH264_CACHE_SKIP_ALL, + test->buf.pSrc1, SRC_PITCH, + dst_luma_rsp, dst_cb_rsp, dst_cr_rsp, DST_SIZE, + (frame_width << 16) | frame_height, + (w << 16) | h, + ((uint32_t)mvx << 16) | ((uint32_t)(mvy & 0xFFFF)), + ((uint32_t)x2 << 16) | ((uint32_t)y2)); + rsph264_sync(); + + armVCM4P10_Interpolate_Luma(src_luma_ref, SRC_PITCH, dst_luma_ref, DST_SIZE, + w, h, wc->dx, wc->dy); + apply_weight(dst_luma_ref, DST_SIZE, w, h, luma_w, luma_o, luma_d); + armVCM4P10_Interpolate_Chroma(src_cb_ref, SRC_PITCH/2, dst_cb_ref, DST_SIZE/2, + w/2, h/2, chroma_dx, chroma_dy); + apply_weight(dst_cb_ref, DST_SIZE/2, w/2, h/2, cb_w, cb_o, chroma_d); + armVCM4P10_Interpolate_Chroma(src_cr_ref, SRC_PITCH/2, dst_cr_ref, DST_SIZE/2, + w/2, h/2, chroma_dx, chroma_dy); + apply_weight(dst_cr_ref, DST_SIZE/2, w/2, h/2, cr_w, cr_o, chroma_d); + + const char *plane = NULL; + if (!weightp_rect_equal(dst_luma_rsp, dst_luma_ref, DST_SIZE, w, h, verbose)) + plane = "luma"; + else if (!weightp_rect_equal(dst_cb_rsp, dst_cb_ref, DST_SIZE/2, w/2, h/2, verbose)) + plane = "cb"; + else if (!weightp_rect_equal(dst_cr_rsp, dst_cr_ref, DST_SIZE/2, w/2, h/2, verbose)) + plane = "cr"; + if (plane) { + printf("FAILED: weightp all-overfill %s\n", plane); + printf("FAILED: d:%d,%d luma:%d,%d,%d cb:%d,%d cr:%d,%d,%d\n", + wc->dx, wc->dy, luma_w, luma_o, luma_d, + cb_w, cb_o, cr_w, cr_o, chroma_d); + abort(); + } +} + +void weightp_all_overfill_test(InterpolationTest *test, int verbose) { + static const WeightpCase cases[] = { + // No weighting at all: verifies the reference of the test itself + { 1, 0, 0, 1, 0, 1, 0, 0, 0, 0 }, + // Only one plane at a time, to check that the RSP does not mix up + // the coefficients of the three planes. + { 80, -16, 6, 1, 0, 1, 0, 0, 0, 0 }, + { 1, 0, 0, 32, 8, 1, 0, 5, 0, 0 }, + { 1, 0, 0, 1, 0, -40, 12, 5, 0, 0 }, + // All planes, integer and fractional motion vectors + { 80, -16, 6, 32, 8, -40, 12, 5, 0, 0 }, + { 80, -16, 6, 32, 8, -40, 12, 5, 2, 1 }, + { 80, -16, 6, 32, 8, -40, 12, 5, 3, 3 }, + { 127, 127, 0, 127, 127, -128, -128, 0, 1, 2 }, + }; + + for (int i = 0; i < (int)(sizeof(cases) / sizeof(cases[0])); i++) + weightp_all_overfill_case(test, &cases[i], verbose); + + rsph264_queue_set_weights_if_changed(RSPH264_WEIGHT_IDENTITY, + RSPH264_WEIGHT_IDENTITY, RSPH264_WEIGHT_IDENTITY); +} + +bool dequant_test(DequantTest *test, int verbose) { + #define MAX_DEQUANT_CHECK_SIZE 64 + int check_size = MAX_DEQUANT_CHECK_SIZE; + uint8_t *dst1 = test->buf.pDst1 + test->y2*DST_SIZE + test->x2; + uint8_t *dst2 = test->buf.pDst2 + test->y2*DST_SIZE + test->x2; + + const uint8_t *csrc1 = test->src; + const uint8_t *csrc2 = test->src; + + uint32_t rsp_time = 0, ref_time = 0; + + switch (test->func) { + case OMX_LUMA_4x4: + rsph264_queue_debug_random_status(); + rsph264_sync(); + + rsp_time = TIME_STATEMENT({ + rsph264_queue_set_packed_delta_buffer(0, + csrc1); + rsph264_queue_dequant_transform_residual(0, + dst1, DST_SIZE, + test->dc, test->qp, test->ac[0]); + rsph264_sync(); + }); + + ref_time = TIME_STATEMENT({ + OMXResult err = omxVCM4P10_DequantTransformResidualFromPairAndAdd( + &csrc2, + dst2, test->dc, dst2, DST_SIZE, DST_SIZE, + test->qp, test->ac[0]); + assert(err == OMX_Sts_NoErr); + }); + break; + + case PROCESS_LUMA_16x16: + rsph264_queue_debug_random_status(); + rsph264_sync(); + + rsp_time = TIME_STATEMENT({ + rsph264_queue_set_packed_delta_buffer(0, + csrc1); + rsph264_queue_process_luma_inter_residual(0, + dst1, DST_SIZE, + 0, test->qp, h264bsdTotalCoeffMask(test->ac)); + rsph264_sync(); + }); + + ref_time = TIME_STATEMENT({ + OMXResult err = HIGHFUNC_ProcessLumaInterResidual( + &csrc2, + dst2, dst2, DST_SIZE, DST_SIZE, + test->qp, test->ac); + assert(err == OMX_Sts_NoErr); + }); + break; + + case PROCESS_CHROMA_8x8x2: + case PROCESS_CHROMA_8x8x2_INTRA: + rsph264_queue_debug_random_status(); + rsph264_queue_debug_load_overlay( + test->func == PROCESS_CHROMA_8x8x2_INTRA ? "intra" : "inter"); + rsph264_sync(); + + rsp_time = TIME_STATEMENT({ + rsph264_queue_set_packed_delta_buffer(0, + csrc1); + rsph264_queue_process_chroma_residual(0, + dst1, dst1+16, DST_SIZE, + test->qp, h264bsdTotalCoeffMask(test->ac)); + rsph264_sync(); + }); + + ref_time = TIME_STATEMENT({ + OMXResult err = HIGHFUNC_ProcessChromaResidual( + &csrc2, + dst2, dst2+16, + DST_SIZE, DST_SIZE, + test->qp, test->ac); + assert(err == OMX_Sts_NoErr); + }); + break; + + + default: + assert(0); + } + + if (verbose >= 2) { + debugf("rsp=%ld ref=%ld\n", rsp_time, ref_time); + } + for (int j = -check_size/2; j < check_size/2; j++) { + for (int i = -check_size/2; i < check_size/2; i++) { + uint8_t *cdst = (uint8_t*)dst1; + if (cdst[j*DST_SIZE+i] != dst2[j*DST_SIZE+i]) { + if (verbose >= 1) { + printf("FAILED\n"); + //printf("FAILED: sz:%d,%d d:%d,%d\n", test->w, test->h, test->dx, test->dy); + printf("FAILED: difference at (%d,%d)\n", i, j); + printf("FAILED: dst:(%d,%d) qp:(%d,%d)\n", test->x2, test->y2, test->qp/6, test->qp%6); + printf("FAILED: RSP=%02x REF=%02x\n", cdst[j*DST_SIZE+i], dst2[j*DST_SIZE+i]); + printf("RSP: REF:\n"); + printf("%02x %02x %02x %02x %02x %02x\n", + cdst[(j-1)*DST_SIZE+i-1], cdst[(j-1)*DST_SIZE+i], cdst[(j-1)*DST_SIZE+i+1], + dst2[(j-1)*DST_SIZE+i-1], dst2[(j-1)*DST_SIZE+i], dst2[(j-1)*DST_SIZE+i+1]); + printf("%02x %02x %02x %02x %02x %02x\n", + cdst[j*DST_SIZE+i-1], cdst[j*DST_SIZE+i], cdst[j*DST_SIZE+i+1], + dst2[j*DST_SIZE+i-1], dst2[j*DST_SIZE+i], dst2[j*DST_SIZE+i+1]); + printf("%02x %02x %02x %02x %02x %02x\n", + cdst[(j+1)*DST_SIZE+i-1], cdst[(j+1)*DST_SIZE+i], cdst[(j+1)*DST_SIZE+i+1], + dst2[(j+1)*DST_SIZE+i-1], dst2[(j+1)*DST_SIZE+i], dst2[(j+1)*DST_SIZE+i+1]); + } + return false; + } + } + } + return true; +} + +bool dequant_dc_test(DequantTest *test, int verbose) { + static uint16_t dc1[16] __attribute__((aligned(8))); + static uint16_t dc2[16] __attribute__((aligned(8))); + const uint8_t *csrc2 = test->src; + + for (int i=0;i<16;i++) + dc1[i] = dc2[i] = (uint16_t)my_rand(); + + rsph264_queue_debug_random_status(); + rsph264_sync(); + + uint32_t rsp_time = 0, ref_time = 0; + switch (test->func) { + case OMX_CHROMADC_2x2: + ref_time = TIME_STATEMENT({ + OMXResult err = omxVCM4P10_TransformDequantChromaDCFromPair( + &csrc2, + (int16_t*)dc2, test->qp); + assert(err == OMX_Sts_NoErr); + }); + rsp_time = TIME_STATEMENT({ + rsph264_queue_set_packed_delta_buffer(0, + test->src); + rsph264_queue_transform_dequant_chromadc(0, + (int16_t*)dc1, test->qp); + rsph264_sync(); + }); + + break; + + default: + assert(0); + } + + if (verbose >= 2) { + debugf("rsp=%ld ref=%ld\n", rsp_time, ref_time); + } + + for (int i=0;i<16;i++) { + if (dc1[i] != dc2[i]) { + if (verbose >= 1) { + printf("FAILED\n"); + printf("FAILED: difference at %d\n", i); + printf("RSP: %04x %04x %04x %04x\n", + dc1[0],dc1[1],dc1[2],dc1[3]); + printf("RSP: %04x %04x %04x %04x\n", + dc1[4],dc1[5],dc1[6],dc1[7]); + printf("REF: %04x %04x %04x %04x\n", + dc2[0],dc2[1],dc2[2],dc2[3]); + printf("REF: %04x %04x %04x %04x\n", + dc2[4],dc2[5],dc2[6],dc2[7]); + } + return false; + } + } + return true; +} + +bool exhaustive_dequant_test(BufferTest *buf, int func, int numtests, int verbose) { + static uint8_t coeffs[1024]; + DequantTest test; + test.buf = *buf; + + for (int i = 0; i < numtests; i++) { + my_srand(i+1024); + test.x2 = (my_rand() % (DST_SIZE-MAX_DEQUANT_CHECK_SIZE)) + MAX_DEQUANT_CHECK_SIZE/2; + test.y2 = (my_rand() % (DST_SIZE-MAX_DEQUANT_CHECK_SIZE)) + MAX_DEQUANT_CHECK_SIZE/2; + if (func == PROCESS_LUMA_16x16 || func == PROCESS_CHROMA_8x8x2 || + func == PROCESS_CHROMA_8x8x2_INTRA) + test.x2 &= ~7; + else + test.x2 &= ~3; + test.func = func; + + int src_offset = my_rand() % 8; + int cidx = src_offset; + + // The bigger the shift factor, the smaller the coefficients, + // otherwise we risk overflow which is not correctly handled + // by the reference implementation (assuming it's permitted + // by the standard in the first place... I have not found + // information on this). + test.qp = my_rand()%48; // FIXME: should be 52 + int cmask = 0x1FF >> (test.qp/6); + + // Test with or without DC + int16_t dc0 = (my_rand() % 512) - 256; + test.dc = &dc0; + if ((my_rand()%2) == 0) test.dc = 0; + + if (test.dc && (my_rand()%32) == 0) { + for (int i=0;i<27;i++) + test.ac[i] = 0; + } else { + for (int i=0;i<27;i++) + test.ac[i] = my_rand() % 2; + if (test.dc == 0) + test.ac[0] = 1; + } + + switch (func) { + case OMX_CHROMADC_2x2: + cidx += gen_coeff_delta(coeffs+cidx, cmask, 4); + break; + + case OMX_LUMA_4x4: + cidx += gen_coeff_delta(coeffs+cidx, cmask, 16); + break; + + case PROCESS_LUMA_16x16: + for (int i=0;i<16;i++) + cidx += gen_coeff_delta(coeffs+cidx, cmask, 16); + break; + + case PROCESS_CHROMA_8x8x2: + case PROCESS_CHROMA_8x8x2_INTRA: + if (test.ac[25]) + cidx += gen_coeff_delta(coeffs+cidx, cmask, 4); + if (test.ac[26]) + cidx += gen_coeff_delta(coeffs+cidx, cmask, 4); + for (int i=0;i<16;i++) + cidx += gen_coeff_delta(coeffs+cidx, cmask, 16); + break; + } + + assert(cidx < sizeof(coeffs)); + data_cache_hit_writeback(coeffs, 1024); + test.src = &coeffs[src_offset]; + + if (func == OMX_CHROMADC_2x2) { + if (!dequant_dc_test(&test, verbose)) { + printf("FAILED TEST: #%d\n", i); + abort(); + } + } else { + if (!dequant_test(&test, verbose)) { + printf("FAILED TEST: #%d\n", i); + abort(); + } + } + } + return true; +} + +// TransformDequantLumaDC, exercised through Task_ProcessLumaIntra16x16Residual +// with large DC coefficients. +// +// exhaustive_intrapred_test masks residual coefficients to ±256, so it never +// reaches the values where the dequantization overflows an s16 intermediate. +// This test drives them directly: a 1-coefficient packed delta places value T +// at position 0 of the LumaDC block and no AC coefficient anywhere, so the +// inverse Hadamard fans T to all 16 blocks and the DC term is the only thing +// the residual contributes. +// +// The neighbours are flat mid-gray, so DC prediction is exactly 128 and the +// residual is the only thing that moves the output: a DC that comes out with +// the wrong sign moves every pixel of the macroblock to the opposite side of +// 128, which clamping cannot hide. +static const uint8_t LumaDC_VMatrix[6] = { 10, 11, 13, 14, 16, 18 }; + +static bool lumadc_overflow_one(BufferTest *buf, int16_t T, int qp, int verbose) { + static uint8_t delta_buf[16] __attribute__((aligned(8))); + + memset(delta_buf, 0, sizeof(delta_buf)); + delta_buf[0] = 0x10 | 0x20; // 16-bit | last | position 0 + delta_buf[1] = (uint8_t)(T & 0xFF); + delta_buf[2] = (uint8_t)((T >> 8) & 0xFF); + data_cache_hit_writeback_invalidate(delta_buf, sizeof(delta_buf)); + + // Both offsets must keep the 16-byte alignment that the command requires. + enum { X = 32, Y = 32 }; + uint8_t *src1 = buf->pSrc1 + Y*SRC_PITCH + X; + uint8_t *src2 = buf->pSrc2 + Y*SRC_PITCH + X; + uint8_t *dst1 = buf->pDst1 + Y*DST_SIZE + X; + uint8_t *dst2 = buf->pDst2 + Y*DST_SIZE + X; + + for (int y = -1; y < 16; y++) + for (int x = -1; x < 16; x++) + src1[y*SRC_PITCH+x] = src2[y*SRC_PITCH+x] = 128; + + uint8_t ac[27] = {0}; + ac[24] = 1; // LumaDC present, no AC blocks + + const uint32_t mode = 2; // OMX_VC_16X16_DC + const uint32_t avail = OMX_VC_UPPER | OMX_VC_LEFT | OMX_VC_UPPER_LEFT; + + rsph264_queue_set_packed_delta_buffer(0, delta_buf); + rsph264_queue_process_luma_intra16_residual(0, + src1, dst1, SRC_PITCH, DST_SIZE, + mode, avail, qp, h264bsdTotalCoeffMask(ac)); + rsph264_sync(); + + const uint8_t *p = delta_buf; + OMXResult err = HIGHFUNC_ProcessLumaIntra16x16Residual( + src2, dst2, SRC_PITCH, DST_SIZE, &p, ac, mode, avail, qp); + assert(err == OMX_Sts_NoErr); + + for (int y = 0; y < 16; y++) { + for (int x = 0; x < 16; x++) { + if (dst1[y*DST_SIZE+x] != dst2[y*DST_SIZE+x]) { + if (verbose >= 1) { + printf("FAILED qp=%d T=%d at (%d,%d): rsp=%d ref=%d\n", + qp, T, x, y, + dst1[y*DST_SIZE+x], dst2[y*DST_SIZE+x]); + } + return false; + } + } + } + return true; +} + +bool lumadc_overflow_test(BufferTest *buf, int verbose) { + // qp < 6 dequantizes as (T*Scale + 2) >> 2, so a T just past 32767/Scale + // overflows the s16 intermediate while leaving the DC at ±8192, well + // within the ±16384 that the residual IDCT accepts (it pre-shifts the + // coefficients by 1 to use vmulf). Truncating T*Scale to its low 16 bits + // flips the sign of the DC, which is the failure the symptom report + // describes: qp=5, Scale=18, T=-1821 saturated I_16x16 macroblocks to + // luma 0xFF. Both signs, as the truncation is not symmetric. + for (int qp = 0; qp < 6; qp++) { + int Scale = LumaDC_VMatrix[qp % 6]; + int16_t T = 32767 / Scale + 1; + if (!lumadc_overflow_one(buf, -T, qp, verbose) || + !lumadc_overflow_one(buf, T, qp, verbose)) { + printf("FAILED LumaDC overflow case: qp=%d T=%d\n", qp, T); + return false; + } + } + + // The higher qp buckets scale the DC up instead of down, so a T that + // overflows the s16 intermediate also produces a DC past what the IDCT + // accepts, and the two failures cannot be told apart through the pixels. + // Sweep them with the largest T that keeps the DC in range instead, which + // still covers the dequantization factors of every qp. + for (int qp = 6; qp < 52; qp++) { + int Scale = LumaDC_VMatrix[qp % 6]; + int16_t T = 60000 / (Scale << (qp / 6)); + if (!lumadc_overflow_one(buf, -T, qp, verbose) || + !lumadc_overflow_one(buf, T, qp, verbose)) { + printf("FAILED LumaDC dequant case: qp=%d T=%d\n", qp, T); + return false; + } + } + + return true; +} + + +int main(void) +{ + debug_init_emulog(); + debug_init_usblog(); + emux_ioctl_fast(); + + timer_init(); + console_init(); + + printf("H264 RSP decoder tests\n\n"); + + my_srand(0); + uint8_t *pSrc1 = (uint8_t*)malloc_uncached(SRC_PITCH*SRC_PITCH); + uint8_t *pSrc2 = (uint8_t*)malloc_uncached(SRC_PITCH*SRC_PITCH); + uint8_t *pDst1 = (uint8_t*)malloc_uncached(DST_SIZE*DST_SIZE); + uint8_t *pDst2 = (uint8_t*)malloc_uncached(DST_SIZE*DST_SIZE); + + debugf("Buffers:\n"); + debugf(" pSrc1: %p - %p\n", pSrc1, pSrc1+SRC_PITCH*SRC_PITCH+16); + debugf(" pSrc2: %p - %p\n", pSrc2, pSrc2+SRC_PITCH*SRC_PITCH+16); + debugf(" pDst1: %p - %p\n", pDst1, pDst1+DST_SIZE*DST_SIZE+16); + debugf(" pDst2: %p - %p\n", pDst2, pDst2+DST_SIZE*DST_SIZE+16); + + // Create random source buffer + for (int y = 0; y < SRC_PITCH; y++) { + for (int x = 0; x < SRC_PITCH; x++) + pSrc1[y*SRC_PITCH+x] = pSrc2[y*SRC_PITCH+x] = my_rand(); + } + + const int OVERFILL = (SRC_PITCH-SRC_SIZE)/2; + + // RSP code automatically handles overfilling. To test it, + // pre-overfill the source buffer used by the reference implementation. + for (int y=OVERFILL;y +#include + +_Thread_local int tls_var = 5; +_Thread_local int tls_var_bss; + +void test_kernel_basic(TestContext *ctx) { + kernel_init(); + DEFER(kernel_close()); + + uint8_t thcalled[16] = {0}; + uint8_t thcalled_idx = 0; + + int func_th(void *arg) + { + kthread_detach(NULL); + int thid = (int)arg; + + thcalled[thcalled_idx++] = thid; + kthread_yield(); + thcalled[thcalled_idx++] = thid; + kthread_yield(); + thcalled[thcalled_idx++] = thid; + return 0; + } + + // Create two threads. Pause their execution by making + // sure they have lower priority than main thread. + kthread_set_pri(NULL, 5); + kthread_new("test1", 2048, 3, func_th, (void*)1); + kthread_new("test2", 2048, 3, func_th, (void*)2); + + // Now lower the priority of the main thread. This will + // immediately force a switch to the two threads that + // have now higher priority. + kthread_set_pri(NULL, 1); + + // Once we get there, the two threads have already finished + // execution. Check that they were called in the expected order. + uint8_t exp[] = { 1,2,1,2,1,2,0 }; + ASSERT_EQUAL_MEM(thcalled, exp, sizeof(exp), "invalid order of threads"); +} + +void test_kernel_priority(TestContext *ctx) { + kernel_init(); + DEFER(kernel_close()); + + uint8_t thcalled[16] = {0}; + uint8_t thcalled_idx = 0; + + int func_th1(void *arg) + { + kthread_detach(NULL); + thcalled[thcalled_idx++] = 1; + kthread_yield(); + thcalled[thcalled_idx++] = 1; + kthread_yield(); + thcalled[thcalled_idx++] = 1; + return 0; + } + + int func_th2(void *arg) + { + kthread_detach(NULL); + thcalled[thcalled_idx++] = 2; + kthread_new("test1", 2048, 5, func_th1, 0); + thcalled[thcalled_idx++] = 2; + return 0; + } + + int func_th3(void *arg) + { + kthread_detach(NULL); + thcalled[thcalled_idx++] = 3; + kthread_new("test2", 2048, 6, func_th2, 0); + thcalled[thcalled_idx++] = 3; + kthread_yield(); + thcalled[thcalled_idx++] = 3; + return 0; + } + + kthread_set_pri(NULL, 1); + kthread_new("test3", 2048, 5, func_th3, 0); + + uint8_t exp[] = { 3,2,2,3,1,3,1,1,0 }; + ASSERT_EQUAL_MEM(thcalled, exp, sizeof(exp), "invalid order of threads"); +} + +void test_kernel_sleep(TestContext *ctx) { + timer_init(); + DEFER(timer_close()); + + kernel_init(); + DEFER(kernel_close()); + + uint8_t thcalled[16] = {0}; + uint8_t thcalled_idx = 0; + volatile uint8_t thexit = 0; + + int func_th1(void *arg) + { + kthread_detach(NULL); + LOG("func_th1 called\n"); + thcalled[thcalled_idx++] = 1; + kthread_sleep(TICKS_FROM_MS(5)); + thcalled[thcalled_idx++] = 1; + kthread_sleep(TICKS_FROM_MS(5)); + thcalled[thcalled_idx++] = 1; + ++thexit; + return 0; + } + + int func_th2(void *arg) + { + kthread_detach(NULL); + LOG("func_th2 called\n"); + thcalled[thcalled_idx++] = 2; + kthread_sleep(TICKS_FROM_MS(8)); + thcalled[thcalled_idx++] = 2; + ++thexit; + return 0; + } + + kthread_set_pri(NULL, 6); + kthread_new("test1", 2048, 4, func_th1, 0); + kthread_new("test2", 2048, 5, func_th2, 0); + + LOG("sleeping\n"); + kthread_set_pri(NULL, 1); + kthread_sleep(TICKS_FROM_MS(15)); + + uint8_t exp[] = { 2,1,1,2,1,0 }; + ASSERT_EQUAL_MEM(thcalled, exp, sizeof(exp), "invalid order of threads"); +} + +void test_kernel_mutex_1(TestContext *ctx) { + timer_init(); + DEFER(timer_close()); + + kernel_init(); + DEFER(kernel_close()); + + kmutex_t mtx; + kmutex_init(&mtx, KMUTEX_RECURSIVE); + DEFER(kmutex_destroy(&mtx)); + + uint8_t thcalled[16] = {0}; + int thcalled_idx = 0; + + int func_th(void* arg) + { + LOG("func_th1 called\n"); + + kmutex_lock(&mtx); + thcalled[thcalled_idx++] = (int)arg; + kmutex_unlock(&mtx); + return 0; + } + + kmutex_lock(&mtx); + + kthread_set_pri(NULL, 1); + kthread_t *th1 = kthread_new("test1", 2560, 4, func_th, (void*)1); + kthread_t *th2 = kthread_new("test2", 2560, 5, func_th, (void*)2); + kthread_t *th3 = kthread_new("test3", 2560, 7, func_th, (void*)3); + kthread_t *th4 = kthread_new("test4", 2560, 6, func_th, (void*)4); + + kmutex_unlock(&mtx); + + kthread_join(th1); + kthread_join(th2); + kthread_join(th3); + kthread_join(th4); + + uint8_t exp[] = { 3,4,2,1 }; + ASSERT_EQUAL_MEM(thcalled, exp, sizeof(exp), "invalid order of threads"); +} + +// Check that errno is a thread local variable +void test_kernel_libc1(TestContext *ctx) { + kernel_init(); + DEFER(kernel_close()); + + kcond_t stepper; + kcond_init(&stepper); + + int func_mkdir(void *arg) + { + kcond_wait(&stepper, NULL); + mkdir(arg, 0777); + kcond_wait(&stepper, NULL); + return errno; + } + + kthread_t *th1 = kthread_new("test1", 4096, 4, func_mkdir, "abc"); + kthread_t *th2 = kthread_new("test2", 4096, 4, func_mkdir, "rom:/abc"); + + kcond_broadcast(&stepper); + kcond_broadcast(&stepper); + int errno1 = kthread_join(th1); + int errno2 = kthread_join(th2); + + ASSERT_EQUAL_SIGNED(errno1, EINVAL, "invalid error code for mkdir1"); + ASSERT_EQUAL_SIGNED(errno2, ENOSYS, "invalid error code for mkdir2"); +} + +// Check that errno is a thread local variable +void test_kernel_libc2(TestContext *ctx) { + kernel_init(); + DEFER(kernel_close()); + + kcond_t stepper; + kcond_init(&stepper); + + int func_strtok(void *arg) + { + kcond_wait(&stepper, NULL); + int n=1; + strtok(arg, ","); + kthread_yield(); + while (strtok(NULL, ",") != NULL) { + kthread_yield(); + ++n; + } + return n; + } + + char str1[] = "a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z"; + char str2[] = "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z"; + + kthread_t* th1 = kthread_new("test3", 4096, 4, func_strtok, str1); + kthread_t* th2 = kthread_new("test4", 4096, 4, func_strtok, str2); + + kcond_broadcast(&stepper); + int n1 = kthread_join(th1); + int n2 = kthread_join(th2); + + ASSERT_EQUAL_SIGNED(n1, 26, "invalid number of tokens for strtok1"); + ASSERT_EQUAL_SIGNED(n2, 26, "invalid number of tokens for strtok2"); +} + +void test_kernel_thread_local(TestContext *ctx) { + + kernel_init(); + DEFER(kernel_close()); + + // Test that thread-local storage works also in the main thread + ASSERT_EQUAL_SIGNED(tls_var, 5, "tls_var not set"); + ASSERT_EQUAL_SIGNED(tls_var_bss, 0, "tls_var_bss not set"); + tls_var = 0x1234; + tls_var_bss = 0x5678; + ASSERT_EQUAL_SIGNED(tls_var, 0x1234, "tls_var not set"); + ASSERT_EQUAL_SIGNED(tls_var_bss, 0x5678, "tls_var_bss not set"); + + kcond_t stepper; + kcond_init(&stepper); + + uint8_t thval[16] = {0}; + int thval_idx = 0; + + int func_th(void *arg) + { + tls_var = (int)arg; + thval[thval_idx++] = tls_var; + kcond_wait(&stepper, NULL); + tls_var++; + thval[thval_idx++] = tls_var; + kcond_wait(&stepper, NULL); + tls_var++; + thval[thval_idx++] = tls_var; + return 0; + } + + kthread_t *th1 = kthread_new("test1", 2048, 3, func_th, (void*)10); + kthread_t *th2 = kthread_new("test2", 2048, 2, func_th, (void*)20); + kthread_t *th3 = kthread_new("test2", 2048, 1, func_th, (void*)30); + + kcond_broadcast(&stepper); + kcond_broadcast(&stepper); + kthread_join(th1); + kthread_join(th2); + kthread_join(th3); + + uint8_t expected[] = { 10, 20, 30, 11, 21, 31, 12, 22, 32 }; + ASSERT_EQUAL_MEM(thval, expected, sizeof(expected), "invalid order of threads"); +} diff --git a/tests/test_magma.c b/tests/test_magma.c new file mode 100644 index 000000000..db28d5407 --- /dev/null +++ b/tests/test_magma.c @@ -0,0 +1,587 @@ +#include +#include +#include "../src/rspq/rspq_internal.h" +#include "../src/magma/rsp_magma.h" +#include "../src/utils.h" + +#define RSPQ_INIT() \ + rspq_init(); DEFER(rspq_close()); + +#define MG_INIT() \ + RSPQ_INIT(); \ + mg_init(); DEFER(mg_close()); + +static uint32_t get_command_count(const uint32_t *block, uint32_t block_size) +{ + const uint32_t *block_end = block + block_size - 1; + uint32_t guard_cmd = *block_end; + while (*--block_end != guard_cmd) {} + return block_end - block; +} + +void assert_block_contents(const uint32_t *expected_commands, uint32_t expected_commands_count, const rspq_block_t *block, TestContext *ctx) +{ + const uint32_t *current_cmd = block->cmds; + uint32_t block_size = RSPQ_BLOCK_MIN_SIZE; + + uint32_t commands_left = get_command_count(current_cmd, block_size); + + for (size_t i = 0; i < expected_commands_count; i++) + { + uint32_t expected_cmd = expected_commands[i]; + uint32_t actual_cmd = *(current_cmd++); + ASSERT_EQUAL_HEX(actual_cmd, expected_cmd, "unexpected block content at word %d", i); + + // Check if we need to jump to the next buffer + if (--commands_left == 0) { + uint32_t cmd = *current_cmd; + if ((cmd>>24) == RSPQ_CMD_JUMP) { + current_cmd = (const uint32_t*)VirtualUncachedAddr(cmd & 0xFFFFFF); + if (block_size < RSPQ_BLOCK_MAX_SIZE) block_size *= 2; + commands_left = get_command_count(current_cmd, block_size); + } else { + break; + } + } + } + + uint32_t last_cmd = *current_cmd; + const uint32_t return_cmd = RSPQ_CMD_RET<<24; + ASSERT_EQUAL_HEX(last_cmd, return_cmd, "block is not %ld words long", expected_commands_count); +} + +typedef struct { + uint32_t buffer[0x1000]; + uint32_t count; +} cmd_buffer; + +#define CMD_BUFFER_APPEND(buf, cmd) buf.buffer[buf.count++] = (cmd) + +#define VTX(cmd_buf, cnt, off, buf) CMD_BUFFER_APPEND(cmd_buf, mg_overlay_id | (MG_CMD_LOAD_VERTICES<<24) | (buf)); CMD_BUFFER_APPEND(cmd_buf, ((off)<<16) | (cnt)); +#define INDEX(i) ((i) * MG_VTX_SIZE + RSP_MAGMA_MG_VERTEX_CACHE) +#define TRI(cmd_buf, i0, i1, i2) CMD_BUFFER_APPEND(cmd_buf, mg_overlay_id | (MG_CMD_DRAW_INDICES<<24) | (INDEX(i0))); CMD_BUFFER_APPEND(cmd_buf, (INDEX(i1)<<16) | INDEX(i2)); + +void assert_draw(const uint32_t *expected_commands, uint32_t expected_commands_count, const mg_input_assembly_parms_t *input_assembly_parms, uint32_t count, uint32_t offset, TestContext *ctx) +{ + rspq_block_begin(); + mg_draw(input_assembly_parms, count, offset); + rspq_block_t *block = rspq_block_end(); + DEFER(rspq_block_free(block)); + + assert_block_contents(expected_commands, expected_commands_count, block, ctx); +} + +void assert_draw_indexed(const uint32_t *expected_commands, uint32_t expected_commands_count, const mg_input_assembly_parms_t *input_assembly_parms, const uint16_t *indices, uint32_t count, uint32_t offset, TestContext *ctx) +{ + rspq_block_begin(); + mg_draw_indexed(input_assembly_parms, indices, count, offset); + rspq_block_t *block = rspq_block_end(); + DEFER(rspq_block_free(block)); + + assert_block_contents(expected_commands, expected_commands_count, block, ctx); +} + +void test_mg_draw_triangle_list(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST + }; + + const uint32_t count = 6; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, 6, 0, 0); + TRI(cmd_buf, 0, 1, 2); + TRI(cmd_buf, 3, 4, 5); + + assert_draw(cmd_buf.buffer, cmd_buf.count, &parms, count, 0, ctx); +} + +void test_mg_draw_triangle_list_non_div3(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST + }; + + const uint32_t count = 8; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, 6, 0, 0); + TRI(cmd_buf, 0, 1, 2); + TRI(cmd_buf, 3, 4, 5); + + assert_draw(cmd_buf.buffer, cmd_buf.count, &parms, count, 0, ctx); +} + +void test_mg_draw_triangle_list_full_cache(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST + }; + + const uint32_t max_vtx_count = ROUND_DOWN(MG_VERTEX_CACHE_COUNT, 3); + const uint32_t vtx_count = max_vtx_count + 6; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, max_vtx_count, 0, 0); + for (size_t i = 0; i < max_vtx_count / 3; i++) + { + TRI(cmd_buf, i*3+0, i*3+1, i*3+2); + } + VTX(cmd_buf, 6, 0, max_vtx_count); + TRI(cmd_buf, 0, 1, 2); + TRI(cmd_buf, 3, 4, 5); + + assert_draw(cmd_buf.buffer, cmd_buf.count, &parms, vtx_count, 0, ctx); +} + +void test_mg_draw_triangle_strip(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP + }; + + const uint32_t count = 6; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, 6, 0, 0); + TRI(cmd_buf, 0, 1, 2); + TRI(cmd_buf, 1, 3, 2); + TRI(cmd_buf, 2, 3, 4); + TRI(cmd_buf, 3, 5, 4); + + assert_draw(cmd_buf.buffer, cmd_buf.count, &parms, count, 0, ctx); +} + +void test_mg_draw_triangle_strip_full_cache(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP + }; + + const uint32_t count = MG_VERTEX_CACHE_COUNT + 4; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, MG_VERTEX_CACHE_COUNT, 0, 0); + for (size_t i = 0; i < MG_VERTEX_CACHE_COUNT - 2; i++) + { + TRI(cmd_buf, i, i+(1+i%2), i+(2-i%2)); + } + VTX(cmd_buf, 6, 0, MG_VERTEX_CACHE_COUNT-2); + TRI(cmd_buf, 0, 1, 2); + TRI(cmd_buf, 1, 3, 2); + TRI(cmd_buf, 2, 3, 4); + TRI(cmd_buf, 3, 5, 4); + + assert_draw(cmd_buf.buffer, cmd_buf.count, &parms, count, 0, ctx); +} + +void test_mg_draw_triangle_fan(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN + }; + + const uint32_t count = 6; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, 6, 0, 0); + TRI(cmd_buf, 1, 2, 0); + TRI(cmd_buf, 2, 3, 0); + TRI(cmd_buf, 3, 4, 0); + TRI(cmd_buf, 4, 5, 0); + + assert_draw(cmd_buf.buffer, cmd_buf.count, &parms, count, 0, ctx); +} + +void test_mg_draw_triangle_fan_full_cache(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN + }; + + const uint32_t count = MG_VERTEX_CACHE_COUNT*2 + 2; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, MG_VERTEX_CACHE_COUNT, 0, 0); + for (size_t i = 0; i < MG_VERTEX_CACHE_COUNT - 2; i++) + { + TRI(cmd_buf, i+1, i+2, 0); + } + VTX(cmd_buf, MG_VERTEX_CACHE_COUNT-1, 1, MG_VERTEX_CACHE_COUNT-1); + for (size_t i = 0; i < MG_VERTEX_CACHE_COUNT - 2; i++) + { + TRI(cmd_buf, i+1, i+2, 0); + } + VTX(cmd_buf, 5, 1, 1 + (MG_VERTEX_CACHE_COUNT-2)*2); + TRI(cmd_buf, 1, 2, 0); + TRI(cmd_buf, 2, 3, 0); + TRI(cmd_buf, 3, 4, 0); + TRI(cmd_buf, 4, 5, 0); + + assert_draw(cmd_buf.buffer, cmd_buf.count, &parms, count, 0, ctx); +} + +void test_mg_draw_indexed_one_tri(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST + }; + + const uint16_t indices[] = { + 0, 1, 2 + }; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, 3, 0, 0); + TRI(cmd_buf, 0, 1, 2); + + assert_draw_indexed(cmd_buf.buffer, cmd_buf.count, &parms, indices, sizeof(indices)/sizeof(indices[0]), 0, ctx); +} + +void test_mg_draw_indexed_two_tris(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST + }; + + const uint16_t indices[] = { + 0, 1, 2, 3, 2, 1 + }; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, 4, 0, 0); + TRI(cmd_buf, 0, 1, 2); + TRI(cmd_buf, 3, 2, 1); + + assert_draw_indexed(cmd_buf.buffer, cmd_buf.count, &parms, indices, sizeof(indices)/sizeof(indices[0]), 0, ctx); +} + +void test_mg_draw_indexed_full_cache(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST + }; + + const uint32_t max_vtx_count = ROUND_DOWN(MG_VERTEX_CACHE_COUNT, 3); + const uint32_t vtx_count = max_vtx_count + 6; + + uint16_t indices[vtx_count]; + for (size_t i = 0; i < vtx_count; i++) indices[i] = i; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, max_vtx_count, 0, 0); + for (size_t i = 0; i < max_vtx_count / 3; i++) + { + TRI(cmd_buf, i*3+0, i*3+1, i*3+2); + } + VTX(cmd_buf, 6, 0, max_vtx_count); + TRI(cmd_buf, 0, 1, 2); + TRI(cmd_buf, 3, 4, 5); + + assert_draw_indexed(cmd_buf.buffer, cmd_buf.count, &parms, indices, sizeof(indices)/sizeof(indices[0]), 0, ctx); +} + +void test_mg_draw_indexed_full_one_extra(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST + }; + + // TODO: This test is incorrect if MG_VERTEX_CACHE_COUNT is not a multiple of 3 + const uint32_t max_vtx_count = ROUND_DOWN(MG_VERTEX_CACHE_COUNT, 3); + + uint16_t indices[max_vtx_count]; + for (size_t i = 0; i < max_vtx_count-1; i++) indices[i] = i; + indices[max_vtx_count-1] = 50; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, max_vtx_count-1, 0, 0); + VTX(cmd_buf, 1, max_vtx_count-1, 50); + for (size_t i = 0; i < max_vtx_count / 3; i++) + { + TRI(cmd_buf, i*3+0, i*3+1, i*3+2); + } + + assert_draw_indexed(cmd_buf.buffer, cmd_buf.count, &parms, indices, sizeof(indices)/sizeof(indices[0]), 0, ctx); +} + +void test_mg_draw_indexed_fragmented_batch(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST + }; + + const uint16_t indices[] = { + 0, 1, 2, 41, 42, 43 + }; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, 3, 0, 0); + VTX(cmd_buf, 3, 3, 41); + TRI(cmd_buf, 0, 1, 2); + TRI(cmd_buf, 3, 4, 5); + + assert_draw_indexed(cmd_buf.buffer, cmd_buf.count, &parms, indices, sizeof(indices)/sizeof(indices[0]), 0, ctx); +} + +void test_mg_draw_indexed_frag_backwards(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST + }; + + const uint16_t indices[] = { + 41, 42, 43, 0, 1, 2 + }; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, 3, 0, 0); + VTX(cmd_buf, 3, 3, 41); + TRI(cmd_buf, 3, 4, 5); + TRI(cmd_buf, 0, 1, 2); + + assert_draw_indexed(cmd_buf.buffer, cmd_buf.count, &parms, indices, sizeof(indices)/sizeof(indices[0]), 0, ctx); +} + +void test_mg_draw_indexed_holes(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST + }; + + const uint16_t indices[] = { + 0, 4, 15 + }; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, 1, 0, 0); + VTX(cmd_buf, 1, 1, 4); + VTX(cmd_buf, 1, 2, 15); + TRI(cmd_buf, 0, 1, 2); + + assert_draw_indexed(cmd_buf.buffer, cmd_buf.count, &parms, indices, sizeof(indices)/sizeof(indices[0]), 0, ctx); +} + +void test_mg_draw_indexed_out_of_order(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST + }; + + const uint16_t indices[] = { + 0, 2, 1, 0, 3, 2 + }; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, 4, 0, 0); + TRI(cmd_buf, 0, 2, 1); + TRI(cmd_buf, 0, 3, 2); + + assert_draw_indexed(cmd_buf.buffer, cmd_buf.count, &parms, indices, sizeof(indices)/sizeof(indices[0]), 0, ctx); +} + +void test_mg_draw_indexed_coalescing(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST + }; + + const uint16_t indices[] = { + 5, 0, 3, 1, 4, 2 + }; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, 6, 0, 0); + TRI(cmd_buf, 5, 0, 3); + TRI(cmd_buf, 1, 4, 2); + + assert_draw_indexed(cmd_buf.buffer, cmd_buf.count, &parms, indices, sizeof(indices)/sizeof(indices[0]), 0, ctx); +} + +void test_mg_draw_indexed_strip(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, + .primitive_restart_enabled = false + }; + + const uint16_t indices[] = { + 0, 1, 2, 3, 4, 5 + }; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, 6, 0, 0); + TRI(cmd_buf, 0, 1, 2); + TRI(cmd_buf, 1, 3, 2); + TRI(cmd_buf, 2, 3, 4); + TRI(cmd_buf, 3, 5, 4); + + assert_draw_indexed(cmd_buf.buffer, cmd_buf.count, &parms, indices, sizeof(indices)/sizeof(indices[0]), 0, ctx); +} + +void test_mg_draw_indexed_strip_full(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, + .primitive_restart_enabled = false + }; + + const size_t count = MG_VERTEX_CACHE_COUNT + 4; + uint16_t indices[count]; + for (size_t i = 0; i < count; i++) indices[i] = i; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, MG_VERTEX_CACHE_COUNT, 0, 0); + for (size_t i = 0; i < MG_VERTEX_CACHE_COUNT - 2; i++) + { + TRI(cmd_buf, i, i+(1+i%2), i+(2-i%2)); + } + VTX(cmd_buf, 6, 0, MG_VERTEX_CACHE_COUNT-2); + TRI(cmd_buf, 0, 1, 2); + TRI(cmd_buf, 1, 3, 2); + TRI(cmd_buf, 2, 3, 4); + TRI(cmd_buf, 3, 5, 4); + + assert_draw_indexed(cmd_buf.buffer, cmd_buf.count, &parms, indices, sizeof(indices)/sizeof(indices[0]), 0, ctx); +} + +void test_mg_draw_indexed_fan(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, + .primitive_restart_enabled = false + }; + + const uint16_t indices[] = { + 0, 1, 2, 3, 4, 5 + }; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, 6, 0, 0); + TRI(cmd_buf, 1, 2, 0); + TRI(cmd_buf, 2, 3, 0); + TRI(cmd_buf, 3, 4, 0); + TRI(cmd_buf, 4, 5, 0); + + assert_draw_indexed(cmd_buf.buffer, cmd_buf.count, &parms, indices, sizeof(indices)/sizeof(indices[0]), 0, ctx); +} + +void test_mg_draw_indexed_fan_full(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, + .primitive_restart_enabled = false + }; + + const uint32_t count = MG_VERTEX_CACHE_COUNT*2 + 2; + uint16_t indices[count]; + for (size_t i = 0; i < count; i++) indices[i] = i; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, MG_VERTEX_CACHE_COUNT, 0, 0); + for (size_t i = 0; i < MG_VERTEX_CACHE_COUNT - 2; i++) + { + TRI(cmd_buf, i+1, i+2, 0); + } + VTX(cmd_buf, MG_VERTEX_CACHE_COUNT-1, 1, MG_VERTEX_CACHE_COUNT-1); + for (size_t i = 0; i < MG_VERTEX_CACHE_COUNT - 2; i++) + { + TRI(cmd_buf, i+1, i+2, 0); + } + VTX(cmd_buf, 5, 1, 1 + (MG_VERTEX_CACHE_COUNT-2)*2); + TRI(cmd_buf, 1, 2, 0); + TRI(cmd_buf, 2, 3, 0); + TRI(cmd_buf, 3, 4, 0); + TRI(cmd_buf, 4, 5, 0); + + assert_draw_indexed(cmd_buf.buffer, cmd_buf.count, &parms, indices, sizeof(indices)/sizeof(indices[0]), 0, ctx); +} + +void test_mg_draw_indexed_restart_strip(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, + .primitive_restart_enabled = true + }; + + const uint16_t indices[] = { + 0, 1, 2, 3, 4, -1, 5, 6, 7, 8 + }; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, 9, 0, 0); + TRI(cmd_buf, 0, 1, 2); + TRI(cmd_buf, 1, 3, 2); + TRI(cmd_buf, 2, 3, 4); + TRI(cmd_buf, 5, 6, 7); + TRI(cmd_buf, 6, 8, 7); + + assert_draw_indexed(cmd_buf.buffer, cmd_buf.count, &parms, indices, sizeof(indices)/sizeof(indices[0]), 0, ctx); +} + +void test_mg_draw_indexed_restart_fan(TestContext *ctx) +{ + MG_INIT(); + + mg_input_assembly_parms_t parms = { + .primitive_topology = MG_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, + .primitive_restart_enabled = true + }; + + const uint16_t indices[] = { + 0, 1, 2, 3, 4, -1, 5, 6, 7, 8 + }; + + cmd_buffer cmd_buf = {0}; + VTX(cmd_buf, 9, 0, 0); + TRI(cmd_buf, 1, 2, 0); + TRI(cmd_buf, 2, 3, 0); + TRI(cmd_buf, 3, 4, 0); + TRI(cmd_buf, 6, 7, 5); + TRI(cmd_buf, 7, 8, 5); + + assert_draw_indexed(cmd_buf.buffer, cmd_buf.count, &parms, indices, sizeof(indices)/sizeof(indices[0]), 0, ctx); +} diff --git a/tests/test_math.c b/tests/test_math.c new file mode 100644 index 000000000..4ce21e51a --- /dev/null +++ b/tests/test_math.c @@ -0,0 +1,150 @@ +#include +#include + +#define ASSERT_EQUAL_MAT4(a, b) ({ \ + for (int i = 0; i < 4; i++) \ + { \ + for (int j = 0; j < 4; j++) \ + { \ + ASSERT_EQUAL_FLOAT(a.m[i][j], b.m[i][j], "m[%d][%d] does not match!", i, j); \ + } \ + } \ +}) + +#define ASSERT_EQUAL_MAT3(a, b) ({ \ + for (int i = 0; i < 3; i++) \ + { \ + for (int j = 0; j < 3; j++) \ + { \ + ASSERT_EQUAL_FLOAT(a.m[i][j], b.m[i][j], "m[%d][%d] does not match!", i, j); \ + } \ + } \ +}) + +#define ASSERT_NEAR_FLOAT(_a, _b, _eps, msg, ...) ({ \ + float a = (_a); \ + float b = (_b); \ + float eps = (_eps); \ + float diff = fabsf(a - b); \ + if (diff > eps) { \ + ERR("ASSERTION FAILED (%s:%d):\n", __FILE__, __LINE__); \ + ERR("|%s - %s| <= %s (%f > %f)\n", #_a, #_b, #_eps, diff, eps); \ + ERR(msg "\n", ##__VA_ARGS__); \ + ctx->result = TEST_FAILED; \ + return; \ + } \ +}) + +#define ASSERT_EQUAL_QUAT_NEAR(a, b, eps) ({ \ + ASSERT_NEAR_FLOAT((a).x, (b).x, eps, "x does not match!"); \ + ASSERT_NEAR_FLOAT((a).y, (b).y, eps, "y does not match!"); \ + ASSERT_NEAR_FLOAT((a).z, (b).z, eps, "z does not match!"); \ + ASSERT_NEAR_FLOAT((a).w, (b).w, eps, "w does not match!"); \ +}) + +void test_mat3_mul_two_identities(TestContext *ctx) +{ + fm_mat3_t a, b, c; + + fm_mat3_identity(&a); + fm_mat3_identity(&b); + fm_mat3_mul(&c, &a, &b); + + fm_mat3_t expected; + fm_mat3_identity(&expected); + + ASSERT_EQUAL_MAT3(expected, c); +} + +void test_mat4_mul_two_identities(TestContext *ctx) +{ + fm_mat4_t a, b, c; + + fm_mat4_identity(&a); + fm_mat4_identity(&b); + fm_mat4_mul(&c, &a, &b); + + fm_mat4_t expected; + fm_mat4_identity(&expected); + + ASSERT_EQUAL_MAT4(expected, c); +} + +void test_mat4_mul_scale_translation(TestContext *ctx) +{ + fm_mat4_t a, b, c; + + fm_mat4_identity(&a); + fm_mat4_translate(&a, &(fm_vec3_t){{1, 2, 3}}); + fm_mat4_identity(&b); + fm_mat4_scale(&b, &(fm_vec3_t){{4, 4, 4}}); + fm_mat4_mul(&c, &a, &b); + + fm_mat4_t expected; + fm_mat4_identity(&expected); + fm_mat4_scale(&expected, &(fm_vec3_t){{4, 4, 4}}); + fm_mat4_translate(&expected, &(fm_vec3_t){{1, 2, 3}}); + + ASSERT_EQUAL_MAT4(expected, c); +} + +void test_mat3_mul_scale_translation(TestContext *ctx) +{ + fm_mat3_t a, b, c; + + fm_mat3_identity(&a); + fm_mat3_translate(&a, &(fm_vec2_t){{1, 2}}); + fm_mat3_identity(&b); + fm_mat3_scale(&b, &(fm_vec2_t){{4, 4}}); + fm_mat3_mul(&c, &a, &b); + + fm_mat3_t expected; + fm_mat3_identity(&expected); + fm_mat3_scale(&expected, &(fm_vec2_t){{4, 4}}); + fm_mat3_translate(&expected, &(fm_vec2_t){{1, 2}}); + + ASSERT_EQUAL_MAT3(expected, c); +} + +void test_quat_from_euler_zero_identity(TestContext *ctx) +{ + fm_quat_t q; + + fm_quat_from_euler_zyx(&q, 0.0f, 0.0f, 0.0f); + + ASSERT_NEAR_FLOAT(q.x, 0.0f, 1e-6f, "identity quaternion x mismatch"); + ASSERT_NEAR_FLOAT(q.y, 0.0f, 1e-6f, "identity quaternion y mismatch"); + ASSERT_NEAR_FLOAT(q.z, 0.0f, 1e-6f, "identity quaternion z mismatch"); + ASSERT_NEAR_FLOAT(q.w, 1.0f, 1e-6f, "identity quaternion w mismatch"); +} + +void test_quat_slerp_same_quaternion(TestContext *ctx) +{ + fm_quat_t q; + fm_quat_from_axis_angle(&q, &(fm_vec3_t){{0.0f, 1.0f, 0.0f}}, 0.73f); + + const float t_values[] = { 0.0f, 0.25f, 0.5f, 1.0f }; + for (int i = 0; i < 4; i++) { + fm_quat_t out; + fm_quat_slerp(&out, &q, &q, t_values[i]); + + ASSERT(isfinite(out.x) && isfinite(out.y) && isfinite(out.z) && isfinite(out.w), + "slerp generated non-finite quaternion for t=%f", t_values[i]); + ASSERT_EQUAL_QUAT_NEAR(out, q, 1e-5f); + } +} + +void test_quat_slerp_near_identical_is_finite(TestContext *ctx) +{ + fm_quat_t a, b, out; + fm_quat_from_axis_angle(&a, &(fm_vec3_t){{0.0f, 0.0f, 1.0f}}, 1.1f); + + b = (fm_quat_t){{ a.x + 1e-6f, a.y - 2e-6f, a.z + 3e-6f, a.w - 1e-6f }}; + fm_quat_norm(&b, &b); + + fm_quat_slerp(&out, &a, &b, 0.5f); + + ASSERT(isfinite(out.x) && isfinite(out.y) && isfinite(out.z) && isfinite(out.w), + "slerp generated non-finite quaternion for nearly-identical inputs"); + ASSERT_NEAR_FLOAT(fm_quat_dot(&out, &out), 1.0f, 1e-4f, "slerp output quaternion is not normalized"); +} diff --git a/tests/test_mpeg1.c b/tests/test_mpeg1.c new file mode 100644 index 000000000..92e04b29c --- /dev/null +++ b/tests/test_mpeg1.c @@ -0,0 +1,330 @@ +#include "../src/video/mpeg1_internal.h" + +void test_mpeg1_idct(TestContext *ctx) { + rspq_init(); DEFER(rspq_close()); + rsp_mpeg1_init(); DEFER(rsp_mpeg1_close()); + + int16_t matrix1[8*8] __attribute__((aligned(16))); + uint8_t out1[8*8] __attribute__((aligned(16))); + int16_t matrix2[8*8] __attribute__((aligned(16))); + + for (int nt=0;nt<256;nt++) { + SRAND(nt+1); + for (int j=0;j<8;j++) { + for (int i=0;i<8;i++) { + matrix1[j*8+i] = RANDN(128)-64; + matrix2[j*8+i] = matrix1[j*8+i]; + } + } + + data_cache_hit_writeback_invalidate(out1, sizeof(out1)); + rsp_mpeg1_block_begin(RSP_MPEG1_BLOCK_CB, out1, 8); + rsp_mpeg1_load_matrix(matrix1); + rsp_mpeg1_idct(); + rsp_mpeg1_store_pixels(); + rspq_wait(); + + // Reference implementation + extern void plm_video_idct(int16_t *block); + plm_video_idct(matrix2); + + for (int j=0;j<8;j++) { + for (int i=0;i<8;i++) { + ASSERT_EQUAL_SIGNED((int8_t)out1[j*8+i], matrix2[j*8+i], + "IDCT failure at %d,%d (nt:%d)", j, i, nt); + } + } + } +} + +void test_mpeg1_block_decode(TestContext *ctx) { + rspq_init(); DEFER(rspq_close()); + rsp_mpeg1_init(); DEFER(rsp_mpeg1_close()); + + int16_t matrix1[8*8] __attribute__((aligned(16))); + uint8_t pixels1[8*8] __attribute__((aligned(16))); + + int16_t matrix2[8*8] __attribute__((aligned(16))); + uint8_t pixels2[8*8] __attribute__((aligned(16))); + + for (int intra=0;intra<2;intra++) { + for (int ncoeffs=1;ncoeffs<3;ncoeffs++) { + for (int nt=0;nt<256;nt++) { + for (int j=0;j<8;j++) { + for (int i=0;i<8;i++) { + if (ncoeffs==1) { + // DC coefficient: already a delta + // for pixels + if (i==0 && j == 0) + matrix2[j*8+i] = matrix1[j*8+i] = RANDN(65536)-32768; + else + matrix2[j*8+i] = matrix1[j*8+i] = 0; + } + else + // AC coefficient: must go through IDCT + matrix2[j*8+i] = matrix1[j*8+i] = RANDN(256)-128; + pixels2[j*8+i] = pixels1[j*8+i] = RANDN(256); + } + } + + data_cache_hit_writeback_invalidate(pixels1, sizeof(pixels1)); + rsp_mpeg1_block_begin(RSP_MPEG1_BLOCK_CB, pixels1, 8); + rsp_mpeg1_load_matrix(matrix1); + if (intra) + rsp_mpeg1_zero_pixels(); + else + rsp_mpeg1_load_pixels(); + rsp_mpeg1_block_decode(ncoeffs, intra!=0); + rsp_mpeg1_store_pixels(); + + extern void plm_video_decode_block_residual(int16_t *s, int si, uint8_t *d, int di, int dw, int n, int intra); + plm_video_decode_block_residual(matrix2, 0, pixels2, 0, 8, ncoeffs, intra); + rspq_wait(); + + + for (int j=0;j<8;j++) { + for (int i=0;i<8;i++) { + ASSERT_EQUAL_HEX(pixels1[j*8+i], pixels2[j*8+i], + "Block decode failure at %d,%d (intra=%d, ncoeffs=%d, nt=%d)", j, i, intra, ncoeffs, nt); + } + } + } + } + } +} + + +void test_mpeg1_block_dequant(TestContext *ctx) { + static const uint8_t PLM_VIDEO_ZIG_ZAG[] = { + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63 + }; + static const uint8_t PLM_VIDEO_NON_INTRA_QUANT_MATRIX[] = { + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16 + }; + static const uint8_t PLM_VIDEO_INTRA_QUANT_MATRIX[] = { + 8, 16, 19, 22, 26, 27, 29, 34, + 16, 16, 22, 24, 27, 29, 34, 37, + 19, 22, 26, 27, 29, 34, 34, 38, + 22, 22, 26, 27, 29, 34, 37, 40, + 22, 26, 27, 29, 32, 35, 40, 48, + 26, 27, 29, 32, 35, 40, 48, 58, + 26, 27, 29, 34, 38, 46, 56, 69, + 27, 29, 35, 38, 46, 56, 69, 83 + }; + static const uint8_t PLM_VIDEO_PREMULTIPLIER_MATRIX[] = { + 32, 44, 42, 38, 32, 25, 17, 9, + 44, 62, 58, 52, 44, 35, 24, 12, + 42, 58, 55, 49, 42, 33, 23, 12, + 38, 52, 49, 44, 38, 30, 20, 10, + 32, 44, 42, 38, 32, 25, 17, 9, + 25, 35, 33, 30, 25, 20, 14, 7, + 17, 24, 23, 20, 17, 14, 9, 5, + 9, 12, 12, 10, 9, 7, 5, 2 + }; + + // Reference C implementation (from pl_mpeg, slightly adjusted). + int dequant_level(int idx, int level, int scale, int intra) { + idx = PLM_VIDEO_ZIG_ZAG[idx]; + + level <<= 1; + if (!intra) { + level += (level < 0 ? -1 : 1); + } + // debugf(" rnd: %04x\n", level); + level = (level * scale * (intra ? PLM_VIDEO_INTRA_QUANT_MATRIX : PLM_VIDEO_NON_INTRA_QUANT_MATRIX)[idx]); + // debugf(" dequant: %04x (scale:%x, quant:%x)\n", level, scale, (intra ? PLM_VIDEO_INTRA_QUANT_MATRIX : PLM_VIDEO_NON_INTRA_QUANT_MATRIX)[idx]); + level >>= 4; + // debugf(" scale: %04x\n", level); + if ((level & 1) == 0) { + level += level > 0 ? -1 : 1; + } + // debugf(" oddify: %04x\n", level); + if (level > 2047) { + level = 2047; + } + else if (level < -2048) { + level = -2048; + } + // debugf(" clamp: %04x\n", level); + level = (level * PLM_VIDEO_PREMULTIPLIER_MATRIX[idx]) >> RSP_IDCT_SCALER; + // debugf(" premult: %04x (pf: %x)\n", level, PLM_VIDEO_PREMULTIPLIER_MATRIX[idx]); + return level; + } + + rspq_init(); DEFER(rspq_close()); + rsp_mpeg1_init(); DEFER(rsp_mpeg1_close()); + + uint8_t pixels1[8*8] __attribute__((aligned(16))); + int16_t matrix1[8*8] __attribute__((aligned(16))); + int16_t matrix2[8*8] __attribute__((aligned(16))); + + rsp_mpeg1_set_quant_matrix(false, PLM_VIDEO_NON_INTRA_QUANT_MATRIX); + rsp_mpeg1_set_quant_matrix(true, PLM_VIDEO_INTRA_QUANT_MATRIX); + + for (int nt=0;nt<1024;nt++) { + SRAND(nt+1); + int intra = RANDN(2); + int ncoeffs = RANDN(64)+1; + int scale = RANDN(31)+1; + + rsp_mpeg1_block_begin(RSP_MPEG1_BLOCK_CB, pixels1, 8); + + // debugf("----------------------\n"); + memset(matrix1, 0, sizeof(matrix1)); + for (int i=0;i(%d,%d) = %04x\n", idx, PLM_VIDEO_ZIG_ZAG[idx]/8, PLM_VIDEO_ZIG_ZAG[idx]%8, (uint16_t)c); + if (idx == 0 && intra) + matrix1[idx] = c; + else + matrix1[PLM_VIDEO_ZIG_ZAG[idx]] = dequant_level(idx, c, scale, intra); + } + rsp_mpeg1_block_dequant(intra, scale); + rsp_mpeg1_store_matrix(matrix2); + rspq_wait(); + + for (int j=0;j<8;j++) { + for (int i=0;i<8;i++) { + ASSERT_EQUAL_HEX((uint16_t)matrix2[j*8+i], (uint16_t)matrix1[j*8+i], + "Dequant failure at %d,%d (intra=%d, ncoeffs=%d, scale=%d, nt=%d)", j, i, intra, ncoeffs, scale, nt); + } + } + } +} + +/**************************** + * + * C REFERNCE IMPLEMENTATION OF BLOCK PREDICTION + * + ****************************/ + +#define PLM_BLOCK_SET(DEST, DEST_INDEX, DEST_WIDTH, SOURCE_INDEX, SOURCE_WIDTH, BLOCK_SIZE, OP) do { \ + int dest_scan = DEST_WIDTH - BLOCK_SIZE; \ + int source_scan = SOURCE_WIDTH - BLOCK_SIZE; \ + for (int y = 0; y < BLOCK_SIZE; y++) { \ + for (int x = 0; x < BLOCK_SIZE; x++) { \ + DEST[DEST_INDEX] = OP; \ + SOURCE_INDEX++; DEST_INDEX++; \ + } \ + SOURCE_INDEX += source_scan; \ + DEST_INDEX += dest_scan; \ + }} while(0) + +static void plm_video_process_macroblock( + uint8_t *s, int si, uint8_t *d, int di, int dw, + int block_size, int odd_h, int odd_v, int interpolate +) { + #define PLM_MB_CASE(INTERPOLATE, ODD_H, ODD_V, OP) \ + case ((INTERPOLATE << 2) | (ODD_H << 1) | (ODD_V)): \ + PLM_BLOCK_SET(d, di, dw, si, dw, block_size, OP); \ + break + + switch ((interpolate << 2) | (odd_h << 1) | (odd_v)) { + PLM_MB_CASE(0, 0, 0, (s[si])); + PLM_MB_CASE(0, 0, 1, (s[si] + s[si + dw] + 1) >> 1); + PLM_MB_CASE(0, 1, 0, (s[si] + s[si + 1] + 1) >> 1); + PLM_MB_CASE(0, 1, 1, (s[si] + s[si + 1] + s[si + dw] + s[si + dw + 1] + 2) >> 2); + + PLM_MB_CASE(1, 0, 0, (d[di] + (s[si]) + 1) >> 1); + PLM_MB_CASE(1, 0, 1, (d[di] + ((s[si] + s[si + dw] + 1) >> 1) + 1) >> 1); + PLM_MB_CASE(1, 1, 0, (d[di] + ((s[si] + s[si + 1] + 1) >> 1) + 1) >> 1); + PLM_MB_CASE(1, 1, 1, (d[di] + ((s[si] + s[si + 1] + s[si + dw] + s[si + dw + 1] + 2) >> 2) + 1) >> 1); + } + + #undef PLM_MB_CASE +} + + +void test_mpeg1_block_predict(TestContext *ctx) { + rspq_init(); DEFER(rspq_close()); + rsp_mpeg1_init(); DEFER(rsp_mpeg1_close()); + + enum { BUFFER_SIZE = 128 }; + + uint8_t *src_buffer = malloc_uncached(BUFFER_SIZE*BUFFER_SIZE); + DEFER(free_uncached(src_buffer)); + uint8_t *dst_buffer1 = malloc_uncached(BUFFER_SIZE*BUFFER_SIZE); + DEFER(free_uncached(dst_buffer1)); + uint8_t *dst_buffer2 = malloc_uncached(BUFFER_SIZE*BUFFER_SIZE); + DEFER(free_uncached(dst_buffer2)); + + // Random pixel buffer + for (int i=0;i=BUFFER_SIZE || i<0 || i>=BUFFER_SIZE) + continue; + + ASSERT_EQUAL_HEX( + dst_buffer1[j*BUFFER_SIZE+i], + dst_buffer2[j*BUFFER_SIZE+i], + "Prediction failure at %d,%d (nt:%d bs:%d d:%d,%d odds:%d/%d/%d)", + i, j, nt, bs, dx, dy, odd_h, odd_v, interpolate); + } + } + } +} diff --git a/tests/test_rdpq.c b/tests/test_rdpq.c index dc28296c2..192e3beed 100644 --- a/tests/test_rdpq.c +++ b/tests/test_rdpq.c @@ -52,6 +52,15 @@ uint64_t debug_rdp_stream_last_cc(void) { return rdp_stream[rdp_stream_ctx.last_cc]; } +const char* debug_rdp_stream_last_cc_disasm(void) { + uint64_t cmds[1] = { debug_rdp_stream_last_cc() }; + static char buf[256]; + FILE *out = fmemopen(buf, sizeof(buf), "w"); + rdpq_debug_disasm(cmds, out); + fclose(out); + return buf; +} + uint32_t debug_rdp_stream_count_cmd(uint32_t cmd_id) { uint32_t count = 0; for (int i=0;irdp_block != NULL, "expected RDP static buffer"); + void *rdp_head = block->rdp_block; + + rspq_block_begin_reuse(block); + rdpq_set_mode_fill(RGBA32(0, 0, 0, 0)); + rdpq_set_fill_color(RGBA16(5, 6, 7, 8)); + rdpq_set_scissor(0, 0, 8, 8); + rdpq_fill_rectangle(0, 0, 32, 32); + rspq_block_t *b2 = rspq_block_end(); + + ASSERT(block == b2, "rspq block pointer must be stable after begin_reuse"); + ASSERT(block->rdp_block == rdp_head, "RDP static buffer head must be reused"); +} + void test_rdpq_block_coalescing(TestContext *ctx) { RDPQ_INIT(); @@ -464,12 +499,14 @@ void test_rdpq_block_nested(TestContext *ctx) rdpq_set_blend_color(RGBA32(0x22,0x22,0x22,0x22)); rdpq_set_prim_color(RGBA32(0x11,0x11,0x11,0x11)); rspq_block_t *block1 = rspq_block_end(); + DEFER(rspq_block_free(block1)); rspq_block_begin(); rdpq_set_fog_color(RGBA32(0x33,0x33,0x33,0x33)); rspq_block_run(block1); rdpq_set_env_color(RGBA32(0x44,0x44,0x44,0x44)); rspq_block_t *block2 = rspq_block_end(); + DEFER(rspq_block_free(block2)); rspq_block_run(block2); rspq_wait(); @@ -921,7 +958,7 @@ void test_rdpq_lookup_address_offset(TestContext *ctx) { RDPQ_INIT(); - const int WIDTH = 15; + const int WIDTH = 16; surface_t fb = surface_alloc(FMT_RGBA16, WIDTH, WIDTH); DEFER(surface_free(&fb)); surface_clear(&fb, 0); @@ -1321,6 +1358,17 @@ void test_rdpq_automode(TestContext *ctx) { ASSERT_EQUAL_MEM((uint8_t*)fb.buffer, (uint8_t*)expected_fb, FBWIDTH*FBWIDTH*2, "Wrong data in framebuffer (comb=2pass, blender=1pass)"); + // Set 1pass combiner with TEX1 + rdpq_debug_log_msg("1pass combiner w/ TEX1 => 2 cycle"); + surface_clear(&fb, 0xFF); + rdpq_mode_combiner(RDPQ_COMBINER1((ZERO, ZERO, ZERO, TEX1), (ZERO, ZERO, ZERO, ZERO))); + rdpq_texture_rectangle(0, 4, 4, FBWIDTH-4, FBWIDTH-4, 0, 0); + rspq_wait(); + som = rdpq_get_other_modes_raw(); + ASSERT_EQUAL_HEX(som & SOM_CYCLE_MASK, SOM_CYCLE_2, "invalid cycle type"); + ASSERT_EQUAL_MEM((uint8_t*)fb.buffer, (uint8_t*)expected_fb, FBWIDTH*FBWIDTH*2, + "Wrong data in framebuffer (comb=1pass w/ TEX1)"); + // Set simple combiner => 1 cycle rdpq_debug_log_msg("1pass combiner => 1 cycle"); surface_clear(&fb, 0xFF); @@ -1419,7 +1467,7 @@ void test_rdpq_blender(TestContext *ctx) { ASSERT_EQUAL_MEM((uint8_t*)fb.buffer, (uint8_t*)expected_fb_tex, FBWIDTH*FBWIDTH*2, "Wrong data in framebuffer (blender=none)"); - // Enable two-pass bleder + // Enable two-pass blender rdpq_mode_blender(RDPQ_BLENDER2( (IN_RGB, 0, BLEND_RGB, INV_MUX_ALPHA), (CYCLE1_RGB, FOG_ALPHA, BLEND_RGB, 1) @@ -1429,12 +1477,31 @@ void test_rdpq_blender(TestContext *ctx) { ASSERT_EQUAL_MEM((uint8_t*)fb.buffer, (uint8_t*)expected_fb_blend2, FBWIDTH*FBWIDTH*2, "Wrong data in framebuffer (blender=pass0+1)"); - // Disable blend + // Switch from two-pass to one-pass blending. + // This should clear the internal 2-pass blender flag and return to 1-cycle mode. + rdpq_mode_blender(RDPQ_BLENDER((IN_RGB, ZERO, BLEND_RGB, INV_MUX_ALPHA))); + rdpq_texture_rectangle(0, 4, 4, FBWIDTH-4, FBWIDTH-4, 0, 0); + rspq_wait(); + uint64_t som = rdpq_get_other_modes_raw(); + ASSERT_EQUAL_HEX(som & SOMX_BLEND_2PASS, 0, "two-pass blender flag was not cleared"); + ASSERT_EQUAL_HEX(som & SOM_CYCLE_MASK, SOM_CYCLE_1, "invalid cycle type after 2pass->1pass transition"); + ASSERT_EQUAL_MEM((uint8_t*)fb.buffer, (uint8_t*)expected_fb_blend, FBWIDTH*FBWIDTH*2, + "Wrong data in framebuffer (blender=pass1 after pass0+1)"); + + // Re-enable two-pass blender, then disable directly. + // This exercises the 2pass -> 0 transition path. + rdpq_mode_blender(RDPQ_BLENDER2( + (IN_RGB, 0, BLEND_RGB, INV_MUX_ALPHA), + (CYCLE1_RGB, FOG_ALPHA, BLEND_RGB, 1) + )); rdpq_mode_blender(0); rdpq_texture_rectangle(0, 4, 4, FBWIDTH-4, FBWIDTH-4, 0, 0); rspq_wait(); + som = rdpq_get_other_modes_raw(); + ASSERT_EQUAL_HEX(som & SOMX_BLEND_2PASS, 0, "two-pass blender flag was not cleared after 2pass->0 transition"); + ASSERT_EQUAL_HEX(som & SOM_CYCLE_MASK, SOM_CYCLE_1, "invalid cycle type after 2pass->0 transition"); ASSERT_EQUAL_MEM((uint8_t*)fb.buffer, (uint8_t*)expected_fb_tex, FBWIDTH*FBWIDTH*2, - "Wrong data in framebuffer (blender=pass0)"); + "Wrong data in framebuffer (blender=none after pass0+1)"); } void test_rdpq_blender_memory(TestContext *ctx) { @@ -1789,6 +1856,16 @@ void test_rdpq_mode_alphacompare(TestContext *ctx) { SOM_ALPHACOMPARE_NONE | SOM_BLALPHA_CVG_TIMES_CC, "invalid SOM configuration: %08llx", som); + rdpq_debug_log_msg("aa+threshold>0+tlut"); + rdpq_mode_tlut(TLUT_RGBA16); + draw_tri(); + som = rdpq_get_other_modes_raw(); + ASSERT_EQUAL_HEX(som & + (SOM_ALPHACOMPARE_MASK | SOM_BLALPHA_MASK), + SOM_ALPHACOMPARE_NONE | SOM_BLALPHA_CVG_TIMES_CC, + "invalid SOM configuration after tlut: %08llx", som); + rdpq_mode_tlut(TLUT_NONE); + rdpq_debug_log_msg("aa+threshold<0"); rdpq_mode_alphacompare(-1); draw_tri(); @@ -1799,6 +1876,57 @@ void test_rdpq_mode_alphacompare(TestContext *ctx) { "invalid SOM configuration: %08llx", som); } +void test_rdpq_mode_zmode(TestContext *ctx) { + RDPQ_INIT(); + + rdpq_debug_log_msg("standard mode"); + rdpq_set_mode_standard(); + uint64_t som = rdpq_get_other_modes_raw(); + ASSERT_EQUAL_HEX(som & SOM_ZMODE_MASK, SOM_ZMODE_OPAQUE, "invalid zmode"); + + rdpq_debug_log_msg("AA standard"); + rdpq_mode_antialias(AA_STANDARD); + som = rdpq_get_other_modes_raw(); + ASSERT_EQUAL_HEX(som & SOM_ZMODE_MASK, SOM_ZMODE_OPAQUE, "invalid zmode"); + + rdpq_debug_log_msg("blending+AA"); + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + som = rdpq_get_other_modes_raw(); + ASSERT_EQUAL_HEX(som & SOM_ZMODE_MASK, SOM_ZMODE_TRANSPARENT, "invalid zmode"); + + rdpq_debug_log_msg("blending"); + rdpq_mode_antialias(AA_NONE); + som = rdpq_get_other_modes_raw(); + ASSERT_EQUAL_HEX(som & SOM_ZMODE_MASK, SOM_ZMODE_TRANSPARENT, "invalid zmode"); + + rdpq_debug_log_msg("Interpenetrating+blending"); + rdpq_mode_zmode(ZMODE_INTERPENETRATING); + som = rdpq_get_other_modes_raw(); + ASSERT_EQUAL_HEX(som & SOM_ZMODE_MASK, SOM_ZMODE_INTERPENETRATING, "invalid zmode"); + + rdpq_debug_log_msg("Decal+blending"); + rdpq_mode_zmode(ZMODE_DECAL); + som = rdpq_get_other_modes_raw(); + ASSERT_EQUAL_HEX(som & SOM_ZMODE_MASK, SOM_ZMODE_DECAL, "invalid zmode"); + + rdpq_debug_log_msg("Standard+blending"); + rdpq_mode_zmode(ZMODE_STANDARD); + som = rdpq_get_other_modes_raw(); + ASSERT_EQUAL_HEX(som & SOM_ZMODE_MASK, SOM_ZMODE_TRANSPARENT, "invalid zmode"); + + rdpq_debug_log_msg("Decal"); + rdpq_mode_zmode(ZMODE_DECAL); + rdpq_mode_blender(0); + som = rdpq_get_other_modes_raw(); + ASSERT_EQUAL_HEX(som & SOM_ZMODE_MASK, SOM_ZMODE_DECAL, "invalid zmode"); + + rdpq_debug_log_msg("Standard"); + rdpq_mode_zmode(ZMODE_STANDARD); + som = rdpq_get_other_modes_raw(); + ASSERT_EQUAL_HEX(som & SOM_ZMODE_MASK, SOM_ZMODE_OPAQUE, "invalid zmode"); +} + + void test_rdpq_mode_freeze(TestContext *ctx) { RDPQ_INIT(); debug_rdp_stream_init(); @@ -2011,6 +2139,87 @@ void test_rdpq_mipmap(TestContext *ctx) { } } +void test_rdpq_mipmap_interpolate(TestContext *ctx) { + RDPQ_INIT(); + debug_rdp_stream_init(); + + const int FBWIDTH = 16; + surface_t fb = surface_alloc(FMT_RGBA16, FBWIDTH, FBWIDTH); + DEFER(surface_free(&fb)); + surface_clear(&fb, 0); + + rdpq_set_color_image(&fb); + rdpq_set_mode_standard(); + rdpq_mode_combiner(RDPQ_COMBINER_TEX_SHADE); + rspq_wait(); + + ASSERT_EQUAL_HEX(debug_rdp_stream_last_som() & + (SOM_CYCLE_MASK | SOM_TEXTURE_LOD | SOMX_LOD_INTERP_MASK), + SOM_CYCLE_1 , + "invalid SOM configuration: %08llx", debug_rdp_stream_last_som()); + ASSERT_EQUAL_HEX(debug_rdp_stream_last_cc() & RDPQ_COMB1_MASK, + RDPQ_COMBINER1((TEX0, 0, SHADE, 0), (TEX0, 0, SHADE, 0)) & RDPQ_COMB1_MASK, + "invalid combiner configuration:\n%s", debug_rdp_stream_last_cc_disasm()); + + rdpq_mode_mipmap(MIPMAP_NEAREST, 4); + rspq_wait(); + + #define CC_PASSTHROUGH1 (COMBINED,COMBINED,COMBINED,COMBINED) + #define CC_PASSTHROUGH2 (COMBINED,COMBINED,LOD_FRAC,COMBINED) + + ASSERT_EQUAL_HEX(debug_rdp_stream_last_som() & + (SOM_CYCLE_MASK | SOM_TEXTURE_LOD | SOMX_LOD_INTERP_MASK), + SOM_CYCLE_2 | SOM_TEXTURE_LOD , + "invalid SOM configuration: %08llx", debug_rdp_stream_last_som()); + ASSERT_EQUAL_HEX(debug_rdp_stream_last_cc() & (RDPQ_COMB0_MASK|RDPQ_COMB1_MASK), + RDPQ_COMBINER2((TEX0,0,SHADE,0), (TEX0,0,SHADE,0), CC_PASSTHROUGH1, CC_PASSTHROUGH2) & (RDPQ_COMB0_MASK|RDPQ_COMB1_MASK), + "invalid combiner configuration:\n%s", debug_rdp_stream_last_cc_disasm()); + + rdpq_mode_mipmap(MIPMAP_INTERPOLATE, 4); + rspq_wait(); + + ASSERT_EQUAL_HEX(debug_rdp_stream_last_som() & + (SOM_CYCLE_MASK | SOM_TEXTURE_LOD | SOMX_LOD_INTERP_MASK), + SOM_CYCLE_2 | SOM_TEXTURE_LOD | SOMX_LOD_INTERPOLATE , + "invalid SOM configuration: %08llx", debug_rdp_stream_last_som()); + ASSERT_EQUAL_HEX(debug_rdp_stream_last_cc() & (RDPQ_COMB0_MASK|RDPQ_COMB1_MASK), + RDPQ_COMBINER2((TEX1,TEX0,LOD_FRAC,TEX0), (TEX1,TEX0,LOD_FRAC,TEX0), (COMBINED,0,SHADE,0),(COMBINED,0,SHADE,0)) & (RDPQ_COMB0_MASK|RDPQ_COMB1_MASK), + "invalid combiner configuration:\n%s", debug_rdp_stream_last_cc_disasm()); + + rdpq_mode_combiner(RDPQ_COMBINER_TEX_FLAT); + rspq_wait(); + + ASSERT_EQUAL_HEX(debug_rdp_stream_last_som() & + (SOM_CYCLE_MASK | SOM_TEXTURE_LOD | SOMX_LOD_INTERP_MASK), + SOM_CYCLE_2 | SOM_TEXTURE_LOD | SOMX_LOD_INTERPOLATE , + "invalid SOM configuration: %08llx", debug_rdp_stream_last_som()); + ASSERT_EQUAL_HEX(debug_rdp_stream_last_cc() & (RDPQ_COMB0_MASK|RDPQ_COMB1_MASK), + RDPQ_COMBINER2((TEX1,TEX0,LOD_FRAC,TEX0), (TEX1,TEX0,LOD_FRAC,TEX0), (COMBINED,0,PRIM,0),(COMBINED,0,PRIM,0)) & (RDPQ_COMB0_MASK|RDPQ_COMB1_MASK), + "invalid combiner configuration:\n%s", debug_rdp_stream_last_cc_disasm()); + + rdpq_mode_mipmap(MIPMAP_INTERPOLATE_SHQ, 4); + rspq_wait(); + + ASSERT_EQUAL_HEX(debug_rdp_stream_last_som() & + (SOM_CYCLE_MASK | SOM_TEXTURE_LOD | SOMX_LOD_INTERP_MASK), + SOM_CYCLE_2 | SOMX_LOD_INTERPOLATE_SHQ, + "invalid SOM configuration: %08llx", debug_rdp_stream_last_som()); + ASSERT_EQUAL_HEX(debug_rdp_stream_last_cc() & (RDPQ_COMB0_MASK|RDPQ_COMB1_MASK), + RDPQ_COMBINER2((TEX0,TEX1,K5,0), (0,0,0,TEX1), (COMBINED,0,PRIM,0),(COMBINED,0,PRIM,0)) & (RDPQ_COMB0_MASK|RDPQ_COMB1_MASK), + "invalid combiner configuration:\n%s", debug_rdp_stream_last_cc_disasm()); + + rdpq_mode_mipmap(MIPMAP_NONE, 0); + rspq_wait(); + + ASSERT_EQUAL_HEX(debug_rdp_stream_last_som() & + (SOM_CYCLE_MASK | SOM_TEXTURE_LOD | SOMX_LOD_INTERP_MASK), + SOM_CYCLE_1 , + "invalid SOM configuration: %08llx", debug_rdp_stream_last_som()); + ASSERT_EQUAL_HEX(debug_rdp_stream_last_cc() & (RDPQ_COMB0_MASK), + RDPQ_COMBINER1((TEX0,0,PRIM,0),(TEX0,0,PRIM,0)) & (RDPQ_COMB0_MASK), + "invalid combiner configuration:\n%s", debug_rdp_stream_last_cc_disasm()); +} + void test_rdpq_autotmem(TestContext *ctx) { RDPQ_INIT(); debug_rdp_stream_init(); diff --git a/tests/test_rdpq_mat.c b/tests/test_rdpq_mat.c new file mode 100644 index 000000000..ef0dbf920 --- /dev/null +++ b/tests/test_rdpq_mat.c @@ -0,0 +1,165 @@ + + +#define ASSERT_MAT_SOM(mat, mask, value) ({ \ + uint64_t som = rdpq_get_other_modes_raw() & (mask); \ + ASSERT_EQUAL_HEX(som, value, "invalid SOM value for material " mat); \ +}) + +#define ASSERT_MAT_CC(mat, exp) ({ \ + uint64_t cc = rdpq_get_combiner_raw() & ~0x7F00000000000000ull; \ + if (cc != exp) { \ + ERR("ERROR: Combiner found: %s\n", rdpq_debug_disasm_cc(cc)); \ + ERR("ERROR: Combiner expected: %s\n", rdpq_debug_disasm_cc(exp)); \ + } \ + ASSERT_EQUAL_HEX(cc, exp, "invalid CC value for material " mat); \ +}) + +void test_rdpq_mat_basic(TestContext *ctx) +{ + RDPQ_INIT(); + rdpq_set_mode_standard(); + rdpq_mode_antialias(AA_STANDARD); + rdpq_mat_set_texture_path("rom:/texdb"); + + rdpq_matdb_t *mdb = rdpq_matdb_open("rom:/basic.mdb"); + DEFER(rdpq_matdb_close(mdb)); + + rdpq_mat_t *glass = rdpq_matdb_load(mdb, "glass"); + ASSERT(glass != NULL, "Failed to load material 'glass'"); + + ASSERT_MAT_SOM("", SOM_ALPHACOMPARE_MASK | SOM_AA_ENABLE, SOM_AA_ENABLE); + rdpq_mat_draw_begin(glass); + ASSERT_MAT_SOM("glass", SOM_ALPHACOMPARE_MASK | SOM_AA_ENABLE, SOM_ALPHACOMPARE_THRESHOLD); + ASSERT_MAT_CC("glass", RDPQ_COMBINER1((TEX0, PRIM, K5, 0), (0, 0, 0, TEX0))); + rdpq_mat_draw_end(glass); + ASSERT_MAT_SOM("", SOM_ALPHACOMPARE_MASK | SOM_AA_ENABLE, SOM_AA_ENABLE); + + // Verify we can successfully record a block for both the begin and end + rspq_block_begin(); + rdpq_mat_draw_begin(glass); + rspq_block_t *bl_mat_begin = rspq_block_end(); + DEFER(rspq_block_free(bl_mat_begin)); + + rspq_block_begin(); + rdpq_mat_draw_end(glass); + rspq_block_t *bl_mat_end = rspq_block_end(); + DEFER(rspq_block_free(bl_mat_end)); + + ASSERT_MAT_SOM("", SOM_ALPHACOMPARE_MASK | SOM_AA_ENABLE, SOM_AA_ENABLE); + rspq_block_run(bl_mat_begin); + ASSERT_MAT_SOM("glass", SOM_ALPHACOMPARE_MASK | SOM_AA_ENABLE, SOM_ALPHACOMPARE_THRESHOLD); + ASSERT_MAT_CC("glass", RDPQ_COMBINER1((TEX0, PRIM, K5, 0), (0, 0, 0, TEX0))); + rspq_block_run(bl_mat_end); + ASSERT_MAT_SOM("", SOM_ALPHACOMPARE_MASK | SOM_AA_ENABLE, SOM_AA_ENABLE); + + // Mix-match block and non block + ASSERT_MAT_SOM("", SOM_ALPHACOMPARE_MASK | SOM_AA_ENABLE, SOM_AA_ENABLE); + rspq_block_run(bl_mat_begin); + ASSERT_MAT_SOM("glass", SOM_ALPHACOMPARE_MASK | SOM_AA_ENABLE, SOM_ALPHACOMPARE_THRESHOLD); + ASSERT_MAT_CC("glass", RDPQ_COMBINER1((TEX0, PRIM, K5, 0), (0, 0, 0, TEX0))); + rdpq_mat_draw_end(glass); + ASSERT_MAT_SOM("", SOM_ALPHACOMPARE_MASK | SOM_AA_ENABLE, SOM_AA_ENABLE); +} + +void test_rdpq_mat_empty(TestContext *ctx) +{ + RDPQ_INIT(); + rdpq_set_mode_standard(); + + rdpq_matdb_t *mdb = rdpq_matdb_open("rom:/basic.mdb"); + DEFER(rdpq_matdb_close(mdb)); + + rdpq_mat_t *empty = rdpq_matdb_load(mdb, "empty"); + ASSERT(empty != NULL, "Failed to load material 'empty'"); + + rdpq_mat_draw_begin(empty); + ASSERT_MAT_CC("empty", RDPQ_COMBINER1((0, 0, 0, TEX0), (0, 0, 0, TEX0))); + rdpq_mat_draw_end(empty); +} + +inline rdpq_blender_t get_expected_blender(rdpq_blender_t bl) +{ + rdpq_set_mode_standard(); + rdpq_mode_blender(bl); + return rdpq_get_other_modes_raw() & SOM_BLEND_MASK; +} + +void test_rdpq_mat_blender(TestContext *ctx) +{ + RDPQ_INIT(); + rdpq_blender_t exp_multiply = get_expected_blender(RDPQ_BLENDER_MULTIPLY); + rdpq_blender_t exp_multiply_const = get_expected_blender(RDPQ_BLENDER_MULTIPLY_CONST); + rdpq_blender_t exp_additive = get_expected_blender(RDPQ_BLENDER_ADDITIVE); + rdpq_matdb_t *mdb = rdpq_matdb_open("rom:/basic.mdb"); + DEFER(rdpq_matdb_close(mdb)); + + rdpq_set_mode_standard(); + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + rdpq_mat_t *blender_none = rdpq_matdb_load(mdb, "blender_none"); + ASSERT(blender_none != NULL, "Failed to load material 'blender_none'"); + + ASSERT_MAT_SOM("", SOM_BLEND_MASK, exp_multiply); + rdpq_mat_draw_begin(blender_none); + ASSERT_MAT_SOM("blender_none", SOM_BLEND_MASK, 0); + rdpq_mat_draw_end(blender_none); + ASSERT_MAT_SOM("", SOM_BLEND_MASK, exp_multiply); + + rdpq_set_mode_standard(); + + rdpq_mat_t *blender_multiply = rdpq_matdb_load(mdb, "blender_multiply"); + ASSERT(blender_multiply != NULL, "Failed to load material 'blender_multiply'"); + + ASSERT_MAT_SOM("", SOM_BLEND_MASK, 0); + rdpq_mat_draw_begin(blender_multiply); + ASSERT_MAT_SOM("blender_multiply", SOM_BLEND_MASK, exp_multiply); + rdpq_mat_draw_end(blender_multiply); + ASSERT_MAT_SOM("", SOM_BLEND_MASK, 0); + + rdpq_mat_t *blender_multiply_const = rdpq_matdb_load(mdb, "blender_multiply_const"); + ASSERT(blender_multiply_const != NULL, "Failed to load material 'blender_multiply_const'"); + + ASSERT_MAT_SOM("", SOM_BLEND_MASK, 0); + rdpq_mat_draw_begin(blender_multiply_const); + ASSERT_MAT_SOM("blender_multiply_const", SOM_BLEND_MASK, exp_multiply_const); + rdpq_mat_draw_end(blender_multiply_const); + ASSERT_MAT_SOM("", SOM_BLEND_MASK, 0); + + rdpq_mat_t *blender_additive = rdpq_matdb_load(mdb, "blender_additive"); + ASSERT(blender_additive != NULL, "Failed to load material 'blender_additive'"); + + ASSERT_MAT_SOM("", SOM_BLEND_MASK, 0); + rdpq_mat_draw_begin(blender_additive); + ASSERT_MAT_SOM("blender_additive", SOM_BLEND_MASK, exp_additive); + rdpq_mat_draw_end(blender_additive); + ASSERT_MAT_SOM("", SOM_BLEND_MASK, 0); +} + +void test_rdpq_mat_ext(TestContext *ctx) +{ + RDPQ_INIT(); + rdpq_set_mode_standard(); + rdpq_mode_antialias(AA_STANDARD); + rdpq_mat_set_texture_path("rom:/texdb"); + + rdpq_matdb_t *mdb = rdpq_matdb_open("rom:/basic.mdb"); + DEFER(rdpq_matdb_close(mdb)); + + rdpq_mat_t *mat = rdpq_matdb_load(mdb, "ext_test"); + ASSERT(mat != NULL, "Failed to load material 'ext_test'"); + + bool bval; + uint32_t ival; + float fval; + char *sval; + + ASSERT(rdpq_mat_ext_get_bool(mat, "test.b", &bval), "ext.test.b not found"); + ASSERT(bval, "ext.test.b is false, expected true"); + ASSERT(rdpq_mat_ext_get_int(mat, "test.i1", &ival), "ext.test.i1 not found"); + ASSERT_EQUAL_UNSIGNED(ival, 0x12345678, "ext.test.i1 value mismatch"); + ASSERT(rdpq_mat_ext_get_int(mat, "test.i2", &ival), "ext.test.i2 not found"); + ASSERT_EQUAL_SIGNED((int32_t)ival, -1234567, "ext.test.i2 value mismatch"); + ASSERT(rdpq_mat_ext_get_float(mat, "test.f", &fval), "ext.test.f not found"); + ASSERT_EQUAL_FLOAT(fval, 12345.67f, "ext.test.f value mismatch"); + ASSERT(rdpq_mat_ext_get_string(mat, "test.s", &sval), "ext.test.s not found"); + ASSERT_EQUAL_STR(sval, "this is a string", "ext.test.s value mismatch"); +} diff --git a/tests/test_rdpq_tex.c b/tests/test_rdpq_tex.c index 9a8aa00ad..43e092e9b 100644 --- a/tests/test_rdpq_tex.c +++ b/tests/test_rdpq_tex.c @@ -132,6 +132,7 @@ void test_rdpq_tex_upload(TestContext *ctx) { surface_clear(&fb, 0); uint16_t* tlut = malloc_uncached(256*2); + DEFER(free_uncached(tlut)); for (int i=0;i<256;i++) { tlut[i] = color_to_packed16(palette_debug_color(i)); } @@ -289,6 +290,7 @@ void test_rdpq_tex_upload_multi(TestContext *ctx) { rdpq_tex_upload(TILE2, &tex2, NULL); rdpq_tex_multi_end(); rspq_block_t *tex1_tex2_loader = rspq_block_end(); + DEFER(rspq_block_free(tex1_tex2_loader)); // Load them both via block loading rdpq_tex_upload(TILE0, &empty, NULL); @@ -341,6 +343,76 @@ void test_rdpq_tex_multi_i4(TestContext *ctx) { }); } +void test_rdpq_tex_can_upload(TestContext *ctx) +{ + // RGBA16 path: 4KB TMEM budget, no 4bpp alignment, no pitch shift. + // Use a non-8 width to verify pitch ROUND_UP to 8-byte boundary. + { + surface_t surf = surface_alloc(FMT_RGBA16, 5, 256); + DEFER(surface_free(&surf)); + ASSERT(rdpq_tex_can_upload(&surf), "RGBA16 5x256 should fit in TMEM"); + } + { + surface_t surf = surface_alloc(FMT_RGBA16, 5, 257); + DEFER(surface_free(&surf)); + ASSERT(!rdpq_tex_can_upload(&surf), "RGBA16 5x257 should not fit in TMEM"); + } + + // RGBA32 path: 2KB TMEM budget + pitch_shift branch. + // Use a non-8 width to verify pitch ROUND_UP after pitch_shift. + { + surface_t surf = surface_alloc(FMT_RGBA32, 3, 256); + DEFER(surface_free(&surf)); + ASSERT(rdpq_tex_can_upload(&surf), "RGBA32 3x256 should fit in TMEM"); + } + { + surface_t surf = surface_alloc(FMT_RGBA32, 3, 257); + DEFER(surface_free(&surf)); + ASSERT(!rdpq_tex_can_upload(&surf), "RGBA32 3x257 should not fit in TMEM"); + } + + // CI8 path: 2KB TMEM budget, no pitch_shift. + // Use a non-8 width to verify pitch ROUND_UP to 8-byte boundary. + { + surface_t surf = surface_alloc(FMT_CI8, 9, 128); + DEFER(surface_free(&surf)); + ASSERT(rdpq_tex_can_upload(&surf), "CI8 9x128 should fit in TMEM"); + } + { + surface_t surf = surface_alloc(FMT_CI8, 9, 129); + DEFER(surface_free(&surf)); + ASSERT(!rdpq_tex_can_upload(&surf), "CI8 9x129 should not fit in TMEM"); + } + + // YUV16 path: same branch family as RGBA32 (2KB + pitch_shift). + { + surface_t surf = surface_alloc(FMT_YUV16, 5, 256); + DEFER(surface_free(&surf)); + ASSERT(rdpq_tex_can_upload(&surf), "YUV16 5x256 should fit in TMEM"); + } + { + surface_t surf = surface_alloc(FMT_YUV16, 5, 257); + DEFER(surface_free(&surf)); + ASSERT(!rdpq_tex_can_upload(&surf), "YUV16 5x257 should not fit in TMEM"); + } + + // 4bpp alignment path: odd width must be rounded up to even, + // then line pitch must still be rounded up to 8-byte boundary. + // For I4 17px width, aligned width becomes 18: + // tmem_pitch = ROUND_UP(18/2,8) = ROUND_UP(9,8) = 16. + // So max height is floor(4096 / 16) = 256. + { + surface_t surf = surface_alloc(FMT_I4, 17, 256); + DEFER(surface_free(&surf)); + ASSERT(rdpq_tex_can_upload(&surf), "I4 17x256 should fit in TMEM"); + } + { + surface_t surf = surface_alloc(FMT_I4, 17, 257); + DEFER(surface_free(&surf)); + ASSERT(!rdpq_tex_can_upload(&surf), "I4 17x257 should not fit in TMEM"); + } +} + void test_rdpq_tex_blit_normal(TestContext *ctx) { RDPQ_INIT(); @@ -352,7 +424,7 @@ void test_rdpq_tex_blit_normal(TestContext *ctx) FMT_CI4, FMT_I4, FMT_IA4, }; - const int FBWIDTH = 32; + const int FBWIDTH = 80; surface_t fb = surface_alloc(FMT_RGBA32, FBWIDTH, FBWIDTH); DEFER(surface_free(&fb)); surface_clear(&fb, 0); @@ -372,8 +444,11 @@ void test_rdpq_tex_blit_normal(TestContext *ctx) SRAND(i); tex_format_t fmt = fmts[i]; - // Create the random surface - for (int tex_width = 72; tex_width < 75; tex_width++) { + // The special value 48 exercises RGBA32 strip loads where + // LOAD_BLOCK must account for the split TMEM layout. + static const int tex_widths[] = { 48, 72, 73, 74 }; + for (int tw=0; twtex_width-s0-3; width--) { LOG(" s0/t0/w: %d %d %d\n", s0, t0, width); + surface_clear(&fb, 0); rdpq_tex_blit(&surf_full, 0, 0, &(rdpq_blitparms_t){ .s0 = s0, .width = width, .t0 = t0, .height = tex_width-t0, }); rspq_wait(); ASSERT_SURFACE(&fb, { - return surface_debug_expected_color(&surf_full, x+s0, y+t0); + if (x < width && y < tex_width-t0) + return surface_debug_expected_color(&surf_full, x+s0, y+t0); + else + return color_from_packed32(0); }); } } @@ -468,6 +547,158 @@ void test_rdpq_tex_upload_tlut(TestContext *ctx) } } +void test_rdpq_tex_upload_tlut_alignments(TestContext *ctx) +{ + RDPQ_INIT(); + + const int FBWIDTH = 16; + surface_t fb = surface_alloc(FMT_RGBA32, FBWIDTH, FBWIDTH); + DEFER(surface_free(&fb)); + surface_clear(&fb, 0xFF); + + surface_t tex = surface_alloc(FMT_CI8, FBWIDTH, FBWIDTH); + DEFER(surface_free(&tex)); + for (int y = 0; y < FBWIDTH; y++) + for (int x = 0; x < FBWIDTH; x++) + surface_set_pixel(&tex, x, y, y * FBWIDTH + x); + + /* Buffer: 8-byte aligned from malloc_uncached; we use offsets 0,2,4,6 to get all four alignments. */ + char *buf = malloc_uncached(256 * 2 + 8); + DEFER(free_uncached(buf)); + uint16_t *base = (uint16_t *)buf; + + uint16_t *tlut_black = malloc_uncached(256 * 2); + DEFER(free_uncached(tlut_black)); + memset(tlut_black, 0, 256 * 2); + + color_t expected_fb[256]; + for (int i = 0; i < 256; i++) { + uint16_t c16 = color_to_packed16(palette_debug_color(i)); + expected_fb[i] = color_from_packed16(c16); + expected_fb[i].a = 0xE0; + } + + rdpq_set_color_image(&fb); + rdpq_set_mode_standard(); + rdpq_mode_tlut(TLUT_RGBA16); + + /* Exhaustive test: all four possible 2-byte alignments (0,2,4,6 mod 8). */ + for (int align = 0; align <= 6; align += 2) { + LOG("TLUT full palette: align=%d (byte offset mod 8)\n", align); + uint16_t *tlut = (uint16_t *)((char *)base + align); + memset(base, 0, 256 * 2 + 8); + for (int i = 0; i < 256; i++) + tlut[i] = color_to_packed16(palette_debug_color(i)); + + surface_clear(&fb, 0xFF); + rdpq_tex_upload_tlut(tlut_black, 0, 256); + rdpq_tex_upload_tlut(tlut, 0, 256); + rdpq_tex_blit(&tex, 0, 0, NULL); + rspq_wait(); + + ASSERT_SURFACE(&fb, { + int pos = y * 16 + x; + return expected_fb[pos]; + }); + if (ctx->result == TEST_FAILED) + return; + } + + /* Sub-palette cases for each alignment: first_color + num_colors. */ + for (int align = 0; align <= 6; align += 2) { + uint16_t *tlut = (uint16_t *)((char *)base + align); + for (int first_color = 8; first_color < 16; first_color++) { + for (int i = 1; i < 9; i++) { + LOG("TLUT sub-palette: align=%d first_color=%d num_colors=%d\n", align, first_color, i); + memset(base, 0, 256 * 2 + 8); + for (int j = 0; j < i; j++) + tlut[j] = color_to_packed16(palette_debug_color(first_color + j)); + + surface_clear(&fb, 0xFF); + rdpq_tex_upload_tlut(tlut_black, 0, 256); + rdpq_tex_upload_tlut(tlut, first_color, i); + rdpq_tex_blit(&tex, 0, 0, NULL); + rspq_wait(); + + ASSERT_SURFACE(&fb, { + int pos = y * 16 + x; + if (pos >= first_color && pos < first_color + i) + return expected_fb[pos]; + else + return color_from_packed32(0xE0); + }); + if (ctx->result == TEST_FAILED) + return; + } + } + } +} + +void test_rdpq_tex_blit_filtering(TestContext *ctx) +{ + RDPQ_INIT(); + debug_rdp_stream_init(); + + const int FBW = 8; + const int TEXW = 4; + surface_t fb = surface_alloc(FMT_RGBA32, FBW, FBW); + DEFER(surface_free(&fb)); + surface_t tex = surface_alloc(FMT_RGBA16, TEXW, 4); + DEFER(surface_free(&tex)); + + // Alternating black/white columns: bilinear at a half-texel offset blends neighbors. + uint16_t black = color_to_packed16(RGBA32(0, 0, 0, 255)); + uint16_t white = color_to_packed16(RGBA32(255, 255, 255, 255)); + for (int y = 0; y < 4; y++) + for (int x = 0; x < TEXW; x++) + ((uint16_t*)tex.buffer)[y * TEXW + x] = (x & 1) ? white : black; + + + rdpq_set_color_image(&fb); + rdpq_set_mode_standard(); + rdpq_mode_filter(FILTER_BILINEAR); + + // filtering=true must use the triangle xblit path (rectangles don't bilinear-filter interiors). + debug_rdp_stream_reset(); + surface_clear(&fb, 0); + rdpq_tex_blit(&tex, 0, 0, &(rdpq_blitparms_t){ .filtering = true }); + rspq_wait(); + ASSERT(debug_rdp_stream_count_cmd(0xCA) > 0, + "filtering blit should use RDP triangles"); + ASSERT_EQUAL_UNSIGNED(debug_rdp_stream_count_cmd(0xE4), 0, + "filtering blit should not use RDP texture rectangles"); + if (ctx->result == TEST_FAILED) + return; + + // filtering=false on a simple blit should stay on the fast rectangle path. + debug_rdp_stream_reset(); + surface_clear(&fb, 0); + rdpq_tex_blit(&tex, 0, 0, NULL); + rspq_wait(); + ASSERT(debug_rdp_stream_count_cmd(0xE4) > 0, + "non-filtering blit should use RDP texture rectangles"); + ASSERT_EQUAL_UNSIGNED(debug_rdp_stream_count_cmd(0xCA), 0, + "non-filtering blit should not use RDP triangles"); + if (ctx->result == TEST_FAILED) + return; + + // Half-texel offset with filtering: interior pixels are bilinear blends (~gray). + surface_clear(&fb, 0); + rdpq_tex_blit(&tex, 0.5f, 0, &(rdpq_blitparms_t){ .filtering = true }); + rspq_wait(); + for (int y = 0; y < 4; y++) { + uint32_t *line = (uint32_t*)(fb.buffer + y * fb.stride); + for (int x = 1; x < TEXW; x++) { + uint32_t px = line[x]; + uint8_t r = px & 0xFF, g = (px >> 8) & 0xFF, b = (px >> 16) & 0xFF; + ASSERT(r > 0x10 && r < 0xF0 && g > 0x10 && g < 0xF0 && b > 0x10 && b < 0xF0, + "filtering blit should bilinear-filter at (%d,%d), got %08lx", x, y, (unsigned long)px); + if (ctx->result == TEST_FAILED) + return; + } + } +} + void test_rdpq_tex_4bpp_odd(TestContext *ctx) { // Make sure loading a 4bpp texture with odd starting coordinates work // correctly. We used to have a bug in this case. diff --git a/tests/test_rdpq_text_layout.c b/tests/test_rdpq_text_layout.c new file mode 100644 index 000000000..f87c1193b --- /dev/null +++ b/tests/test_rdpq_text_layout.c @@ -0,0 +1,1025 @@ +/* + * Layout tests for rdpq_paragraph.c / rdpq_text.c (registry, printf buffers). + */ + +#include +#include +#include +#include + +/* Builtin font load uses rdpq_font_load_buf(), which requires rdpq_init() for atlas upload. */ +#define RDPQ_TEXT_FONT_CTX() \ + RDPQ_INIT(); \ + rdpq_font_t *mono = rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_MONO); \ + ASSERT(mono != NULL, "builtin mono"); \ + rdpq_text_register_font(1, mono); \ + DEFER(rdpq_text_unregister_font(1); rdpq_font_free(mono);) + +static int cmp_paragraph_reading_order(const void *a, const void *b) +{ + const rdpq_paragraph_char_t *ca = a, *cb = b; + int ya = (int)roundf((float)ca->y); + int yb = (int)roundf((float)cb->y); + if (ya != yb) + return ya - yb; + if (ca->x < cb->x) + return -1; + if (ca->x > cb->x) + return 1; + return (int)ca->glyph - (int)cb->glyph; +} + +static void layout_dump_glyphs_reading(const rdpq_paragraph_t *p, char *out, size_t cap) +{ + if (p->nchars <= 0) { + if (cap) + out[0] = '\0'; + return; + } + rdpq_paragraph_char_t *tmp = alloca(sizeof(rdpq_paragraph_char_t) * (size_t)p->nchars); + memcpy(tmp, p->chars, sizeof(rdpq_paragraph_char_t) * (size_t)p->nchars); + qsort(tmp, (size_t)p->nchars, sizeof(rdpq_paragraph_char_t), cmp_paragraph_reading_order); + + size_t o = 0; + for (int i = 0; i < p->nchars && o + 1 < cap; i++) { + const rdpq_paragraph_char_t *ch = &tmp[i]; + int n = snprintf(out + o, cap - o, "g=%d,x=%d,y=%d,f=%u,s=%u|", + (int)ch->glyph, (int)roundf((float)ch->x), (int)roundf((float)ch->y), + (unsigned)ch->font_id, (unsigned)ch->style_id); + if (n < 0 || (size_t)n >= cap - o) + break; + o += (size_t)n; + } + if (o < cap) + out[o] = '\0'; + else if (cap) + out[cap - 1] = '\0'; +} + +/** UTF-8: collect glyphs for non-whitespace (skip space/tab/newline). */ +static int utf8_plain_glyph_expect(const rdpq_font_t *fnt, const char *s, int *glyphs_out, int max_g) +{ + int n = 0; + while (*s && n < max_g) { + uint32_t cp; + unsigned char c = (unsigned char)*s; + if (c < 0x80) { + cp = c; + s++; + } else if ((c & 0xE0) == 0xC0 && s[1]) { + cp = ((uint32_t)(c & 0x1Fu) << 6) | (uint32_t)(s[1] & 0x3Fu); + s += 2; + } else if ((c & 0xF0) == 0xE0 && s[1] && s[2]) { + cp = ((uint32_t)(c & 0x0Fu) << 12) | ((uint32_t)(s[1] & 0x3Fu) << 6) + | (uint32_t)(s[2] & 0x3Fu); + s += 3; + } else + return -1; + + if (cp == '\n') + continue; + if (cp == ' ' || cp == '\t') + continue; + int g = rdpq_font_get_glyph_index(fnt, cp); + if (g < 0) + return -1; + glyphs_out[n++] = g; + } + return n; +} + +static void sort_copy_reading(const rdpq_paragraph_t *p, rdpq_paragraph_char_t *tmp) +{ + memcpy(tmp, p->chars, sizeof(rdpq_paragraph_char_t) * (size_t)p->nchars); + qsort(tmp, (size_t)p->nchars, sizeof(rdpq_paragraph_char_t), cmp_paragraph_reading_order); +} + +/** First @a n glyphs in reading order must match @a full_sorted (regression: typewriter + WRAP_WORD). */ +static void assert_typewriter_prefix_matches_full(TestContext *ctx, + const rdpq_paragraph_char_t *full_sorted, const rdpq_paragraph_t *partial, int n) +{ + ASSERT_EQUAL_SIGNED(partial->nchars, n, "partial nchars"); + rdpq_paragraph_char_t *ps = alloca(sizeof(rdpq_paragraph_char_t) * (size_t)n); + sort_copy_reading(partial, ps); + for (int i = 0; i < n; i++) { + ASSERT_EQUAL_SIGNED((int)ps[i].glyph, (int)full_sorted[i].glyph, "glyph"); + ASSERT_EQUAL_SIGNED((int)roundf((float)ps[i].x), (int)roundf((float)full_sorted[i].x), "x"); + ASSERT_EQUAL_SIGNED((int)roundf((float)ps[i].y), (int)roundf((float)full_sorted[i].y), "y"); + ASSERT_EQUAL_UNSIGNED(ps[i].font_id, full_sorted[i].font_id, "font"); + ASSERT_EQUAL_UNSIGNED(ps[i].style_id, full_sorted[i].style_id, "style"); + } +} + +static void typewriter_max_chars_progression_case(TestContext *ctx, const char *text, + rdpq_textparms_t parms_base, int min_total, int min_lines) +{ + int nbytes = (int)strlen(text); + rdpq_textparms_t parms_full = parms_base; + parms_full.max_chars = 0; + int nb_full = nbytes; + rdpq_paragraph_t *full = rdpq_paragraph_build(&parms_full, 1, text, &nb_full); + if (!full) { + ASSERT(full, "full build"); + return; + } + int ncref = full->nchars, nlref = full->nlines; + if (ncref < min_total || nlref < min_lines) { + rdpq_paragraph_free(full); + ASSERT(ncref >= min_total && nlref >= min_lines, "reference layout"); + return; + } + + int total = full->nchars; + rdpq_paragraph_char_t *full_sorted = alloca(sizeof(rdpq_paragraph_char_t) * (size_t)total); + sort_copy_reading(full, full_sorted); + + for (int n = 1; n <= total; n++) { + rdpq_textparms_t parms = parms_base; + parms.max_chars = n; + int nb = nbytes; + rdpq_paragraph_t *p = rdpq_paragraph_build(&parms, 1, text, &nb); + if (!p) { + rdpq_paragraph_free(full); + ASSERT(p, "partial build"); + return; + } + assert_typewriter_prefix_matches_full(ctx, full_sorted, p, n); + rdpq_paragraph_free(p); + if (ctx->result == TEST_FAILED) { + rdpq_paragraph_free(full); + return; + } + } + rdpq_paragraph_free(full); +} + +static void assert_layout_plain_glyphs(TestContext *ctx, const rdpq_paragraph_t *p, + const rdpq_font_t *fnt, uint8_t expect_font, uint8_t expect_style, const char *plain_utf8) +{ + int exp[256]; + int ne = utf8_plain_glyph_expect(fnt, plain_utf8, exp, 256); + ASSERT(ne >= 0, "glyph expect"); + ASSERT_EQUAL_SIGNED(p->nchars, ne, "nchars"); + if (ne <= 0) + return; + rdpq_paragraph_char_t *tmp = alloca(sizeof(rdpq_paragraph_char_t) * (size_t)ne); + sort_copy_reading(p, tmp); + for (int i = 0; i < ne; i++) { + ASSERT_EQUAL_SIGNED((int)tmp[i].glyph, exp[i], "glyph id"); + ASSERT_EQUAL_UNSIGNED(tmp[i].font_id, expect_font, "font id"); + ASSERT_EQUAL_UNSIGNED(tmp[i].style_id, expect_style, "style id"); + } +} + +/* Empty paragraph is SKIP: builder touches chars[-1] when nchars==0 (UB). */ +void test_rdpq_text_metrics_empty(TestContext *ctx) +{ + SKIP("empty UTF-8: rdpq_paragraph_builder_end indexes chars[-1] when nchars==0; " + "cannot layout zero glyphs safely"); +} + +/* Simple ASCII: nbytes, single line, expected glyphs, stable reading-order dump. */ +void test_rdpq_text_metrics_simple_ascii(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { 0 }; + const char *text = "abc"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT_EQUAL_SIGNED(nbytes, (int)strlen(text), "nbytes"); + ASSERT_EQUAL_SIGNED(layout->nlines, 1, "nlines"); + assert_layout_plain_glyphs(ctx, layout, mono, 1, 0, text); + + char d1[512], d2[512]; + layout_dump_glyphs_reading(layout, d1, sizeof d1); + layout_dump_glyphs_reading(layout, d2, sizeof d2); + ASSERT_EQUAL_STR(d1, d2, "dump stable"); +} + +/* Newlines in input: glyph count, multiple visual lines, vertical order of a vs b. */ +void test_rdpq_text_newline_only(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { 0 }; + const char *text = "a\n\nb"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT_EQUAL_SIGNED(nbytes, (int)strlen(text), "nbytes"); + ASSERT_EQUAL_SIGNED(layout->nchars, 2, "two glyphs"); + ASSERT_EQUAL_SIGNED(layout->nlines, 3, "three visual lines"); + assert_layout_plain_glyphs(ctx, layout, mono, 1, 0, "ab"); + + rdpq_paragraph_char_t tmp[4]; + sort_copy_reading(layout, tmp); + ASSERT(tmp[1].y > tmp[0].y, "b baseline below a"); +} + +/* $0N escape: switch font (glyphs from IDs 1 and 2 in "a$02b"). */ +void test_rdpq_text_escape_dollar_font(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + rdpq_font_t *var = rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_VAR); + ASSERT(var != NULL, "var"); + rdpq_text_register_font(2, var); + DEFER(rdpq_text_unregister_font(2); rdpq_font_free(var);); + + rdpq_textparms_t parms = { 0 }; + const char *text = "a$02b"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT_EQUAL_SIGNED(layout->nchars, 2, "nchars"); + rdpq_paragraph_char_t tmp[8]; + sort_copy_reading(layout, tmp); + int ga = rdpq_font_get_glyph_index(mono, 'a'); + int gb = rdpq_font_get_glyph_index(var, 'b'); + ASSERT_EQUAL_SIGNED(tmp[0].glyph, ga, "a glyph"); + ASSERT_EQUAL_UNSIGNED(tmp[0].font_id, 1u, "font1"); + ASSERT_EQUAL_SIGNED(tmp[1].glyph, gb, "b glyph"); + ASSERT_EQUAL_UNSIGNED(tmp[1].font_id, 2u, "font2"); +} + +/* ^0N escape: adjacent glyphs get different style ids. */ +void test_rdpq_text_escape_caret_style(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { 0 }; + const char *text = "a^01b"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT_EQUAL_SIGNED(layout->nchars, 2, "nchars"); + rdpq_paragraph_char_t tmp[8]; + sort_copy_reading(layout, tmp); + ASSERT_EQUAL_UNSIGNED(tmp[0].style_id, 0u, "a style"); + ASSERT_EQUAL_UNSIGNED(tmp[1].style_id, 1u, "b style"); +} + +/* $$ and ^^ emit literal $ and ^ in the layout. */ +void test_rdpq_text_escape_literal_dollar_caret(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { 0 }; + const char *text = "a$$b^^c"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + assert_layout_plain_glyphs(ctx, layout, mono, 1, 0, "a$b^c"); +} + +/* indent: first line starts farther right on X than the second line. */ +void test_rdpq_text_indent_first_line(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { .indent = 17 }; + const char *text = "a\nb"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT_EQUAL_SIGNED(layout->nchars, 2, "nchars"); + rdpq_paragraph_char_t tmp[4]; + sort_copy_reading(layout, tmp); + ASSERT(tmp[0].y < tmp[1].y, "lines"); + ASSERT(tmp[0].x > tmp[1].x, "first line indented farther right"); +} + +/* WRAP_CHAR: break at fixed width; multiple lines; same glyphs as full ASCII text. */ +void test_rdpq_text_wrap_char_basic(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { .width = 40, .wrap = WRAP_CHAR }; + const char *text = "abcdefghijklmnopqrst"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT(layout->nlines >= 2, "wrapped to multiple lines"); + assert_layout_plain_glyphs(ctx, layout, mono, 1, 0, text); +} + +/* WRAP_WORD: break at spaces; words concatenated in plain-glyph expectations (no spaces). */ +void test_rdpq_text_wrap_word_basic(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { .width = 56, .wrap = WRAP_WORD }; + const char *text = "abc def ghi"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT(layout->nlines >= 2, "word wrap"); + assert_layout_plain_glyphs(ctx, layout, mono, 1, 0, "abcdefghi"); +} + +/* Long token without spaces: WRAP_WORD truncates or ellipsizes (fewer glyphs than input). */ +void test_rdpq_text_wrap_word_long_token(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { .width = 32, .wrap = WRAP_WORD }; + const char *text = "abcdefghijklmno"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT(layout->nchars < (int)strlen(text), "truncated long token"); + ASSERT(layout->nchars >= 1, "some glyph"); +} + +/* WRAP_NONE: no wrap/reflow; text is truncated when it exceeds width. */ +void test_rdpq_text_wrap_none_truncate(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { .width = 24, .wrap = WRAP_NONE }; + const char *text = "abcdefghijklmnopqrst"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT(layout->nchars < (int)strlen(text), "truncate"); +} + +/* WRAP_ELLIPSES: ellipsis glyphs appear when text does not fit the width. */ +void test_rdpq_text_wrap_ellipses(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { .width = 48, .wrap = WRAP_ELLIPSES }; + const char *text = "abcdefghijklmnopqrstuvwxyz"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + int ell_g = rdpq_font_get_glyph_index(mono, '.'); + ASSERT(ell_g >= 0, "ellipsis glyph"); + int n_ell = 0; + for (int i = 0; i < layout->nchars; i++) { + if (layout->chars[i].glyph == ell_g) + n_ell++; + } + ASSERT(n_ell >= 1, "ellipsis glyphs present"); +} + +/* char_spacing increases horizontal gap between two letters vs default zero. */ +void test_rdpq_text_char_spacing(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms0 = { 0 }; + rdpq_textparms_t parms1 = { .char_spacing = 4 }; + const char *text = "ab"; + int nb0 = (int)strlen(text); + int nb1 = (int)strlen(text); + rdpq_paragraph_t *l0 = rdpq_paragraph_build(&parms0, 1, text, &nb0); + rdpq_paragraph_t *l1 = rdpq_paragraph_build(&parms1, 1, text, &nb1); + ASSERT(l0 && l1, "build"); + DEFER(rdpq_paragraph_free(l0); + rdpq_paragraph_free(l1);); + + rdpq_paragraph_char_t t0[4], t1[4]; + sort_copy_reading(l0, t0); + sort_copy_reading(l1, t1); + ASSERT(t1[1].x > t0[1].x, "char_spacing widens second glyph"); +} + +/* line_spacing increases delta-Y between two lines separated by newline. */ +void test_rdpq_text_line_spacing(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms0 = { 0 }; + rdpq_textparms_t parms1 = { .line_spacing = 6 }; + const char *text = "a\nb"; + int nb0 = (int)strlen(text); + int nb1 = (int)strlen(text); + rdpq_paragraph_t *l0 = rdpq_paragraph_build(&parms0, 1, text, &nb0); + rdpq_paragraph_t *l1 = rdpq_paragraph_build(&parms1, 1, text, &nb1); + ASSERT(l0 && l1, "build"); + DEFER(rdpq_paragraph_free(l0); + rdpq_paragraph_free(l1);); + + rdpq_paragraph_char_t t0[4], t1[4]; + sort_copy_reading(l0, t0); + sort_copy_reading(l1, t1); + ASSERT(t1[1].y > t0[1].y, "line_spacing increases delta-Y"); +} + +/* Default tab stops: X advance after \\t is clearly larger than for adjacent chars without tab. */ +void test_rdpq_text_tab_default_32(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { 0 }; + const char *text = "a\tb"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT_EQUAL_SIGNED(layout->nchars, 2, "nchars"); + rdpq_paragraph_char_t tmp[4]; + sort_copy_reading(layout, tmp); + ASSERT(tmp[1].x > tmp[0].x + 4, "tab advances x"); +} + +/* Custom tabstops: second glyph X matches the chosen stop column. */ +void test_rdpq_text_tab_custom_stops(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + int16_t stops[] = { 50, 100, 0 }; + rdpq_textparms_t parms = { .tabstops = stops }; + const char *text = "a\tb"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + rdpq_paragraph_char_t tmp[4]; + sort_copy_reading(layout, tmp); + ASSERT(tmp[1].x > tmp[0].x + 20, "custom tabstop"); +} + +/* max_chars (typewriter): stops at next space after emitting up to the glyph budget. */ +void test_rdpq_text_max_chars_stop_at_space(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { .max_chars = 2 }; + const char *text = "ab cd ef"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT_EQUAL_SIGNED(layout->nchars, 2, "two glyphs"); + ASSERT_EQUAL_SIGNED(nbytes, 2, "2 chars displayed"); +} + +void test_rdpq_text_nbytes_wrap_word_plaintext(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { .width = 40, .height = 40, .wrap = WRAP_WORD }; + const char *text = "abcdef abcdef 123456 123456"; + + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + rdpq_paragraph_free(layout); + ASSERT_EQUAL_SIGNED(nbytes, (int)strlen(text), "first word doesn't fit -> ellipsis: full line consumed"); + + nbytes = (int)strlen(text); + parms.width = 45; + layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + rdpq_paragraph_free(layout); + ASSERT_EQUAL_SIGNED(nbytes, 21, "consume spaces after wrapped words"); + + nbytes = (int)strlen(text); + parms.width = 85; + layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + ASSERT_EQUAL_SIGNED(nbytes, (int)strlen(text), "full string fits in available lines"); +} + +void test_rdpq_text_nbytes_wrap_word_with_escapes(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_font_style(mono, 1, &(rdpq_fontstyle_t){ + .color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF), + }); + rdpq_font_style(mono, 2, &(rdpq_fontstyle_t){ + .color = RGBA32(0xFF, 0x00, 0x00, 0xFF), + }); + + rdpq_textparms_t parms = { .width = 40, .height = 40, .wrap = WRAP_WORD }; + const char *text = "^01abcdef ^02abcdef ^01123456 ^02123456"; + + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + rdpq_paragraph_free(layout); + ASSERT_EQUAL_SIGNED(nbytes, (int)strlen(text), "first word doesn't fit -> ellipsis: full line consumed"); + + nbytes = (int)strlen(text); + parms.width = 45; + layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + rdpq_paragraph_free(layout); + ASSERT_EQUAL_SIGNED(nbytes, 30, "consume escapes and spaces consistently"); + + nbytes = (int)strlen(text); + parms.width = 85; + layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + ASSERT_EQUAL_SIGNED(nbytes, (int)strlen(text), "full string fits in available lines"); +} + +void test_rdpq_text_nbytes_wrap_char_with_escapes(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_font_style(mono, 1, &(rdpq_fontstyle_t){ + .color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF), + }); + rdpq_font_style(mono, 2, &(rdpq_fontstyle_t){ + .color = RGBA32(0xFF, 0x00, 0x00, 0xFF), + }); + + rdpq_textparms_t parms = { .width = 25, .height = 40, .wrap = WRAP_CHAR }; + const char *text = "^01abcdef ^02abcdef ^01123456 ^02123456"; + + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + rdpq_paragraph_free(layout); + ASSERT_EQUAL_SIGNED(nbytes, 16, "3 lines * 3 chars/line, escapes counted"); + + nbytes = (int)strlen(text); + parms.width = 30; + layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + rdpq_paragraph_free(layout); + ASSERT_EQUAL_SIGNED(nbytes, 18, "3 lines * 4 chars/line"); + + nbytes = (int)strlen(text); + parms.width = 35; + layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + ASSERT_EQUAL_SIGNED(nbytes, 24, "3 lines * 5 chars/line"); +} + +void test_rdpq_text_nbytes_ellipsis(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_font_style(mono, 1, &(rdpq_fontstyle_t){ + .color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF), + }); + rdpq_font_style(mono, 2, &(rdpq_fontstyle_t){ + .color = RGBA32(0xFF, 0x00, 0x00, 0xFF), + }); + + rdpq_textparms_t parms = { .width = 65, .height = 30, .wrap = WRAP_WORD }; + const char *text = "^01abcdef\n^02abcdefabcdef\n^01123456 ^02123456"; + + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + rdpq_paragraph_free(layout); + ASSERT_EQUAL_SIGNED(nbytes, 26, "ellipsis consumes to end of overflowing line"); + + nbytes = (int)strlen(text); + parms.height = 40; + layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + ASSERT_EQUAL_SIGNED(nbytes, 36, "next line starts after consumed ellipsis line"); +} + +/* WRAP_WORD: when stopping on a full page, consume trailing newline delimiter too. */ +void test_rdpq_text_nbytes_wrap_word_consumes_newline(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { .width = 40, .height = 20, .wrap = WRAP_WORD }; + const char *text = "abcdef\nghijkl"; + int nbytes = (int)strlen(text); + + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT_EQUAL_SIGNED(nbytes, 7, "first page consumes first word plus newline"); +} + +/* WRAP_WORD + consecutive escapes: nbytes must stop before ^xx^yy when page is full. */ +void test_rdpq_text_nbytes_wrap_word_consecutive_escapes(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_font_style(mono, 1, &(rdpq_fontstyle_t){ + .color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF), + }); + rdpq_font_style(mono, 2, &(rdpq_fontstyle_t){ + .color = RGBA32(0xFF, 0x00, 0x00, 0xFF), + }); + + rdpq_textparms_t parms = { .width = 20, .height = 20, .wrap = WRAP_WORD }; + const char *text = "a ^01^02bbbbbbbb c"; + const int escape_off = (int)(strstr(text, "^01^02") - text); + + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + rdpq_paragraph_free(layout); + ASSERT_EQUAL_SIGNED(nbytes, escape_off, "full page must not consume consecutive escapes"); + ASSERT(text[nbytes] == '^', "next page starts at first escape"); + + rdpq_textparms_t parms2 = parms; + parms2.height = 120; + int nbytes2 = (int)strlen(text) - nbytes; + layout = rdpq_paragraph_build(&parms2, 1, text + nbytes, &nbytes2); + ASSERT(layout != NULL, "build page 2"); + rdpq_paragraph_free(layout); + ASSERT_EQUAL_SIGNED(nbytes2, (int)strlen(text) - nbytes, "page 2 consumes all remaining bytes"); +} + +/* max_chars on a word without spaces: final nchars clamped to max_chars. */ +void test_rdpq_text_max_chars_clamp_end(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { .max_chars = 3 }; + const char *text = "aaaa"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT_EQUAL_SIGNED(layout->nchars, 3, "clamped to max_chars"); +} + +/* max_chars + WRAP_WORD across lines: typewriter does not break word wrap (#739). */ +void test_rdpq_text_max_chars_with_word_wrap(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { .width = 56, .wrap = WRAP_WORD, .max_chars = 6 }; + const char *text = "abc def ghi"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT_EQUAL_SIGNED(layout->nchars, 6, "typewriter stops after six letters (abc+def)"); + /* First six glyphs may sit on one line even when full "abc def ghi" wraps with no max_chars. */ + assert_layout_plain_glyphs(ctx, layout, mono, 1, 0, "abcdef"); +} + +/* For each max_chars, first N glyphs match unlimited layout (WORD wrap regression #739). */ +void test_rdpq_text_typewriter(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + /* Full layout may truncate the long m-run (few total glyphs); min_lines still from short+long word. */ + typewriter_max_chars_progression_case(ctx, "aa mmmmmmmmmmmm", + (rdpq_textparms_t){ .width = 20, .wrap = WRAP_WORD }, 2, 2); + if (ctx->result == TEST_FAILED) + return; + + typewriter_max_chars_progression_case(ctx, "abc def ghi", + (rdpq_textparms_t){ .width = 56, .wrap = WRAP_WORD }, 9, 2); +} + +/* WRAP_ELLIPSES at very small width: ellipsis present (font/line fix dbcbb3634). */ +void test_rdpq_text_wrap_ellipses_narrow_width(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { .width = 10, .wrap = WRAP_ELLIPSES }; + const char *text = "abcdefghijklmnopqrstuvwxyz"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + int ell_g = rdpq_font_get_glyph_index(mono, '.'); + ASSERT(ell_g >= 0, "ellipsis glyph"); + int n_ell = 0; + for (int i = 0; i < layout->nchars; i++) { + if (layout->chars[i].glyph == ell_g) + n_ell++; + } + ASSERT(n_ell >= 1, "ellipsis at tight width"); + ASSERT(layout->nlines >= 1, "nlines"); +} + +/* Short word then long token: only xx on first line; wrap does not pull from previous line (#0c4e38885). */ +void test_rdpq_text_wrap_word_short_then_long_token(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { .width = 20, .wrap = WRAP_WORD }; + char text[40]; + strcpy(text, "xx "); + for (int i = 0; i < 14; i++) + strcat(text, "m"); + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT(layout->nlines >= 2, "short word then long token wraps to next line"); + ASSERT(layout->nchars >= 3, "xx plus continuation"); + + rdpq_paragraph_char_t tmp[32]; + sort_copy_reading(layout, tmp); + int gx = rdpq_font_get_glyph_index(mono, 'x'); + ASSERT_EQUAL_SIGNED((int)tmp[0].glyph, gx, "first line x"); + ASSERT_EQUAL_SIGNED((int)tmp[1].glyph, gx, "first line x"); + float y0 = tmp[0].y; + ASSERT(fabsf(tmp[1].y - y0) < 1.f, "both x on same row"); + int n_first_line = 0; + for (int i = 0; i < layout->nchars && n_first_line < 32; i++) { + if (fabsf(tmp[i].y - y0) < 1.f) + n_first_line++; + } + ASSERT_EQUAL_SIGNED(n_first_line, 2, "only xx on first line; long m-token wraps below"); +} + +/* Max height: vertical layout is shortened (WRAP_CHAR and newline in input). */ +void test_rdpq_text_height_truncates_vertical(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { .height = 18, .wrap = WRAP_CHAR, .width = 24 }; + const char *text = "abcdefghij\nklmnop"; + int full_len = (int)strlen(text); + int nbytes = full_len; + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT(nbytes <= full_len, "nbytes sane"); + ASSERT(nbytes < full_len || layout->nchars < 12, "vertical limit shortens layout"); +} + +/* valign VALIGN_CENTER vs TOP: y0/bbox/advance shift with fixed paragraph height. */ +void test_rdpq_text_valign_center_height(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms_top = { .height = 80, .valign = VALIGN_TOP }; + rdpq_textparms_t parms_ctr = { .height = 80, .valign = VALIGN_CENTER }; + const char *text = "ab"; + int nb0 = (int)strlen(text); + int nb1 = (int)strlen(text); + rdpq_paragraph_t *l0 = rdpq_paragraph_build(&parms_top, 1, text, &nb0); + rdpq_paragraph_t *l1 = rdpq_paragraph_build(&parms_ctr, 1, text, &nb1); + ASSERT(l0 && l1, "build"); + DEFER(rdpq_paragraph_free(l0); + rdpq_paragraph_free(l1);); + + ASSERT(l1->y0 != l0->y0 || l1->bbox.y0 != l0->bbox.y0, "valign shifts layout"); + ASSERT(l1->advance_y != l0->advance_y || l1->y0 != l0->y0, "advance/y0 valign"); +} + +/* ALIGN_CENTER vs LEFT: line shifts right within fixed width. */ +void test_rdpq_text_align_center_width(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms_l = { .width = 200, .align = ALIGN_LEFT }; + rdpq_textparms_t parms_c = { .width = 200, .align = ALIGN_CENTER }; + const char *text = "ab"; + int nb0 = (int)strlen(text); + int nb1 = (int)strlen(text); + rdpq_paragraph_t *l0 = rdpq_paragraph_build(&parms_l, 1, text, &nb0); + rdpq_paragraph_t *l1 = rdpq_paragraph_build(&parms_c, 1, text, &nb1); + ASSERT(l0 && l1, "build"); + DEFER(rdpq_paragraph_free(l0); + rdpq_paragraph_free(l1);); + + rdpq_paragraph_char_t t0[4], t1[4]; + sort_copy_reading(l0, t0); + sort_copy_reading(l1, t1); + ASSERT(t1[0].x > t0[0].x, "center moves line right"); +} + +/* ALIGN_RIGHT vs LEFT: line shifts farther right within fixed width. */ +void test_rdpq_text_align_right_width(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms_l = { .width = 200, .align = ALIGN_LEFT }; + rdpq_textparms_t parms_r = { .width = 200, .align = ALIGN_RIGHT }; + const char *text = "ab"; + int nb0 = (int)strlen(text); + int nb1 = (int)strlen(text); + rdpq_paragraph_t *l0 = rdpq_paragraph_build(&parms_l, 1, text, &nb0); + rdpq_paragraph_t *l1 = rdpq_paragraph_build(&parms_r, 1, text, &nb1); + ASSERT(l0 && l1, "build"); + DEFER(rdpq_paragraph_free(l0); + rdpq_paragraph_free(l1);); + + rdpq_paragraph_char_t t0[4], t1[4]; + sort_copy_reading(l0, t0); + sort_copy_reading(l1, t1); + ASSERT(t1[0].x > t0[0].x, "right moves line farther right"); +} + +/* Multi-byte UTF-8 (e.g. pound): one glyph if present in builtin mono font. */ +void test_rdpq_text_utf8_multibyte_in_range(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + const char *text = "\xc2\xa3"; + if (rdpq_font_get_glyph_index(mono, 0xA3) < 0) { + SKIP("pound sign not in builtin mono"); + return; + } + rdpq_textparms_t parms = { 0 }; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT_EQUAL_SIGNED(layout->nchars, 1, "one glyph"); +} + +/* Invalid UTF-8 sequence: build still succeeds (graceful skip of bad bytes). */ +void test_rdpq_text_utf8_invalid_sequence(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + const char *text = "\xff\xfe"; + rdpq_textparms_t parms = { 0 }; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); +} + +/* preserve_overlap: same reading-order dump as without; chars[] may differ after RDP sort. */ +void test_rdpq_text_preserve_overlap_order(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + rdpq_font_t *var = rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_VAR); + ASSERT(var != NULL, "var"); + rdpq_text_register_font(2, var); + DEFER(rdpq_text_unregister_font(2); rdpq_font_free(var);); + + /* Styles 0,2,1 so sort_key order (0,1,2) differs from insert order (0,2,1). */ + const char *text = "a^02b^01c"; + int nb0 = (int)strlen(text); + int nb1 = (int)strlen(text); + rdpq_textparms_t po = { .preserve_overlap = true }; + rdpq_textparms_t np = { .preserve_overlap = false }; + rdpq_paragraph_t *l0 = rdpq_paragraph_build(&po, 1, text, &nb0); + rdpq_paragraph_t *l1 = rdpq_paragraph_build(&np, 1, text, &nb1); + ASSERT(l0 && l1, "build"); + DEFER(rdpq_paragraph_free(l0); + rdpq_paragraph_free(l1);); + + char d0[256], d1[256]; + layout_dump_glyphs_reading(l0, d0, sizeof d0); + layout_dump_glyphs_reading(l1, d1, sizeof d1); + ASSERT_EQUAL_STR(d0, d1, "reading-order dump same"); + if (l0->nchars > 1) + ASSERT(memcmp(l0->chars, l1->chars, sizeof(rdpq_paragraph_char_t) * (size_t)l0->nchars) != 0, + "chars[] order differs when RDP sort runs"); +} + +/* disable_aa_fix: layout clears RDPQ_PARAGRAPH_FLAG_ANTIALIAS_FIX. */ +void test_rdpq_text_disable_aa_fix_flag(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { .disable_aa_fix = true }; + const char *text = "a"; + int nbytes = 1; + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT((layout->flags & RDPQ_PARAGRAPH_FLAG_ANTIALIAS_FIX) == 0, "AA fix flag off"); +} + +/* Incremental builder (span/newline) matches rdpq_paragraph_build reading-order dump. */ +void test_rdpq_text_builder_api_matches_build(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + rdpq_textparms_t parms = { 0 }; + const char *text = "a\nb"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *from_build = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(from_build != NULL, "build"); + DEFER(rdpq_paragraph_free(from_build);); + + rdpq_paragraph_builder_begin(&parms, 1, NULL); + rdpq_paragraph_builder_span("a", 1); + rdpq_paragraph_builder_newline(); + rdpq_paragraph_builder_span("b", 1); + rdpq_paragraph_t *from_manual = rdpq_paragraph_builder_end(); + ASSERT(from_manual != NULL, "manual"); + DEFER(rdpq_paragraph_free(from_manual);); + + char d0[512], d1[512]; + layout_dump_glyphs_reading(from_build, d0, sizeof d0); + layout_dump_glyphs_reading(from_manual, d1, sizeof d1); + ASSERT_EQUAL_STR(d0, d1, "builder vs build"); +} + +/* Builtin proportional font: AV pair shows kerning (second glyph farther right). */ +void test_rdpq_text_kerning_var_font(TestContext *ctx) +{ + RDPQ_INIT(); + rdpq_font_t *var = rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_VAR); + ASSERT(var != NULL, "var"); + rdpq_text_register_font(1, var); + DEFER(rdpq_text_unregister_font(1); + rdpq_font_free(var);); + + rdpq_textparms_t parms = { 0 }; + const char *text = "AV"; + int nbytes = (int)strlen(text); + rdpq_paragraph_t *layout = rdpq_paragraph_build(&parms, 1, text, &nbytes); + ASSERT(layout != NULL, "build"); + DEFER(rdpq_paragraph_free(layout);); + + ASSERT_EQUAL_SIGNED(layout->nchars, 2, "nchars"); + rdpq_paragraph_char_t tmp[4]; + sort_copy_reading(layout, tmp); + ASSERT(tmp[1].x > tmp[0].x, "second glyph right of first"); +} + +/* Font registry: register at id 7, get, unregister, then get returns NULL. */ +void test_rdpq_text_register_get_unregister(TestContext *ctx) +{ + RDPQ_INIT(); + rdpq_font_t *mono = rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_MONO); + ASSERT(mono != NULL, "mono"); + rdpq_text_register_font(7, mono); + ASSERT(rdpq_text_get_font(7) == mono, "get registered"); + rdpq_text_unregister_font(7); + ASSERT(rdpq_text_get_font(7) == NULL, "gone after unregister"); + rdpq_font_free(mono); +} + +/* Short formatted string: layout matches expanded literal text. */ +void test_rdpq_text_printf_small_buf(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + char fmt[32]; + snprintf(fmt, sizeof fmt, "n=%d", 42); + rdpq_textparms_t parms = { 0 }; + int nb = (int)strlen(fmt); + rdpq_paragraph_t *l = rdpq_paragraph_build(&parms, 1, fmt, &nb); + ASSERT(l != NULL, "layout from formatted string"); + DEFER(rdpq_paragraph_free(l);); + + ASSERT_EQUAL_SIGNED(l->nchars, 4, "n=42 -> four non-space glyphs"); +} + +/* rdpq_text_printf with >512-byte format (heap vasnprintf) and many short lines. */ +void test_rdpq_text_printf_large_buf(TestContext *ctx) +{ + RDPQ_TEXT_FONT_CTX(); + + const int W = 64; + surface_t fb = surface_alloc(FMT_RGBA32, W, W); + DEFER(surface_free(&fb)); + memset(fb.buffer, 0, fb.stride * fb.height); + + rdpq_attach_clear(&fb, NULL); + + /* >512 bytes for vasnprintf heap path; bounded line length and line count (x/y < 2048). */ + char big[800]; + int p = 0; + for (int line = 0; line < 36 && p + 17 < (int)sizeof big; line++) { + for (int i = 0; i < 15; i++) + big[p++] = 'a'; + big[p++] = '\n'; + } + big[p] = '\0'; + ASSERT((int)strlen(big) > 512, "large format string"); + + /* main() will check for memory leaks */ + rdpq_text_printf(NULL, 1, 4.f, 20.f, "%s", big); + rdpq_detach_wait(); +} \ No newline at end of file diff --git a/tests/test_rdpq_tri.c b/tests/test_rdpq_tri.c index 9fb3eb698..b54824966 100644 --- a/tests/test_rdpq_tri.c +++ b/tests/test_rdpq_tri.c @@ -68,7 +68,7 @@ void test_rdpq_triangle(TestContext *ctx) { rdpq_debug_log_msg("CPU"); rdpq_triangle_cpu(&trifmt, v1, v2, v3); rdpq_debug_log_msg("RSP"); - rdpq_triangle_rsp(&trifmt, v1, v2, v3); + rdpq_triangle_rsp(&trifmt, v1, v2, v3, NULL); rspq_wait(); const int RDP_TRI_SIZE = 22; @@ -158,6 +158,88 @@ void test_rdpq_triangle(TestContext *ctx) { } } +void test_rdpq_triangle_block_order(TestContext *ctx) { + RDPQ_INIT(); + + const int FBWIDTH = 64; + const int FBHEIGHT = 32; + surface_t direct = surface_alloc(FMT_RGBA16, FBWIDTH, FBHEIGHT); + surface_t blocked = surface_alloc(FMT_RGBA16, FBWIDTH, FBHEIGHT); + DEFER(surface_free(&direct)); + DEFER(surface_free(&blocked)); + surface_clear(&direct, 0); + surface_clear(&blocked, 0); + + uint16_t red_pixels[8 * 8] __attribute__((aligned(8))); + uint16_t green_pixels[8 * 8] __attribute__((aligned(8))); + const uint16_t red = color_to_packed16(RGBA32(255, 0, 0, 255)); + const uint16_t green = color_to_packed16(RGBA32(0, 255, 0, 255)); + for (int i = 0; i < 8 * 8; ++i) { + red_pixels[i] = red; + green_pixels[i] = green; + } + data_cache_hit_writeback(red_pixels, sizeof(red_pixels)); + data_cache_hit_writeback(green_pixels, sizeof(green_pixels)); + const surface_t red_texture = surface_make_linear(red_pixels, FMT_RGBA16, 8, 8); + const surface_t green_texture = surface_make_linear(green_pixels, FMT_RGBA16, 8, 8); + + const float left_a[5] = {4, 4, 0, 0, 1}; + const float left_b[5] = {28, 4, 7, 0, 1}; + const float left_c[5] = {4, 28, 0, 7, 1}; + const float right_a[5] = {36, 4, 0, 0, 1}; + const float right_b[5] = {60, 4, 7, 0, 1}; + const float right_c[5] = {36, 28, 0, 7, 1}; + + void emit_pair(bool cpu_assembled) { + rdpq_set_mode_standard(); + rdpq_mode_filter(FILTER_POINT); + rdpq_mode_combiner(RDPQ_COMBINER_TEX); + rdpq_tex_upload(TILE0, &red_texture, NULL); + (cpu_assembled ? rdpq_triangle_cpu : rdpq_triangle)( + &TRIFMT_TEX, left_a, left_b, left_c); + rdpq_tex_upload(TILE0, &green_texture, NULL); + (cpu_assembled ? rdpq_triangle_cpu : rdpq_triangle)( + &TRIFMT_TEX, right_a, right_b, right_c); + } + + rdpq_set_color_image(&direct); + rdpq_set_scissor(0, 0, FBWIDTH, FBHEIGHT); + emit_pair(false); + rspq_wait(); + + rspq_block_begin(); + emit_pair(false); + rspq_block_t *block = rspq_block_end(); + DEFER(rspq_block_free(block)); + + rdpq_set_color_image(&blocked); + rdpq_set_scissor(0, 0, FBWIDTH, FBHEIGHT); + rspq_block_run(block); + rspq_wait(); + + uint16_t *direct_pixels = direct.buffer; + ASSERT_EQUAL_HEX(direct_pixels[8 * FBWIDTH + 8], red, + "direct left triangle has wrong texture"); + ASSERT_EQUAL_HEX(direct_pixels[8 * FBWIDTH + 40], green, + "direct right triangle has wrong texture"); + ASSERT_EQUAL_MEM(blocked.buffer, direct.buffer, + direct.stride * direct.height, + "block-recorded triangle stream changed texture/draw ordering"); + + surface_clear(&blocked, 0); + rspq_block_begin(); + emit_pair(true); + rspq_block_t *cpu_block = rspq_block_end(); + DEFER(rspq_block_free(cpu_block)); + rdpq_set_color_image(&blocked); + rdpq_set_scissor(0, 0, FBWIDTH, FBHEIGHT); + rspq_block_run(cpu_block); + rspq_wait(); + ASSERT_EQUAL_MEM(blocked.buffer, direct.buffer, + direct.stride * direct.height, + "CPU-assembled block triangle stream changed texture/draw ordering"); +} + void test_rdpq_triangle_w1(TestContext *ctx) { RDPQ_INIT(); debug_rdp_stream_init(); diff --git a/tests/test_rspq.c b/tests/test_rspq.c index 4fdc6cffa..153d09ac2 100644 --- a/tests/test_rspq.c +++ b/tests/test_rspq.c @@ -1,10 +1,12 @@ #include +#include #include #include #include #include #include +#include "../src/rspq/rspq_internal.h" #include "test_rspq_constants.h" #define ASSERT_GP_BACKWARD 0xF001 // Also defined in rsp_test.S @@ -35,7 +37,7 @@ static uint32_t test2_ovl_id; void test_ovl_init() { - void *test_ovl_state = rspq_overlay_get_state(&rsp_test); + void *test_ovl_state = UncachedAddr(rspq_overlay_get_state(&rsp_test)); memset(test_ovl_state, 0, sizeof(uint32_t) * 2); rspq_init(); @@ -157,14 +159,14 @@ const unsigned long rspq_timeout = 100; ASSERT_RSPQ_EPILOG_SP_STATUS((s)); \ }) -void test_rspq_queue_single(TestContext *ctx) +void test_rspq_cmd_single(TestContext *ctx) { TEST_RSPQ_PROLOG(); TEST_RSPQ_EPILOG(0, rspq_timeout); } -void test_rspq_queue_multiple(TestContext *ctx) +void test_rspq_cmd_multiple(TestContext *ctx) { TEST_RSPQ_PROLOG(); @@ -173,7 +175,7 @@ void test_rspq_queue_multiple(TestContext *ctx) TEST_RSPQ_EPILOG(0, rspq_timeout); } -void test_rspq_queue_rapid(TestContext *ctx) +void test_rspq_cmd_rapid(TestContext *ctx) { TEST_RSPQ_PROLOG(); @@ -206,6 +208,93 @@ void test_rspq_wrap(TestContext *ctx) TEST_RSPQ_EPILOG(0, rspq_timeout); } +void test_rspq_buffer_handoff_atomic(TestContext *ctx) +{ + TEST_RSPQ_PROLOG(); + + volatile uint32_t *first_buffer = rspq_cur_pointer; + uint32_t command_count = 0; + + // Fill the initial low-priority buffer until rspq_next_buffer switches to + // its replacement. Each no-op occupies exactly one word, so the handoff + // command starts immediately after the commands counted here. + do { + rspq_noop(); + command_count++; + } while ((uintptr_t)rspq_cur_pointer >= (uintptr_t)first_buffer && + (uintptr_t)rspq_cur_pointer < + (uintptr_t)(first_buffer + RSPQ_DRAM_LOWPRI_BUFFER_SIZE)); + + volatile uint32_t *handoff = first_buffer + command_count; + ASSERT_EQUAL_HEX(handoff[0] >> 24, RSPQ_CMD_WRITE_WORD, + "buffer handoff must stage its target before signaling completion"); + ASSERT_EQUAL_HEX(handoff[0] & 0x00FFFFFF, + offsetof(rsp_queue_t, rspq_pointer_stack), + "buffer handoff stages its target in the wrong pointer slot"); + ASSERT_EQUAL_HEX(handoff[1], + PhysicalAddr((void *)rspq_cur_pointer), + "buffer handoff has the wrong jump target"); + ASSERT_EQUAL_HEX(handoff[2] >> 24, RSPQ_CMD_SWAP_BUFFERS, + "buffer completion and jump must use one atomic RSPQ command"); + ASSERT_EQUAL_HEX(handoff[2] & 0x00FFFFFF, 0, + "buffer handoff loads its target from the wrong pointer slot"); + ASSERT_EQUAL_HEX(handoff[3], 0, + "buffer handoff saves its discarded return to the wrong pointer slot"); + ASSERT_EQUAL_HEX(handoff[4], SP_WSTATUS_SET_SIG_BUFDONE_LOW, + "buffer handoff has the wrong completion signal"); + ASSERT_EQUAL_HEX(handoff[5], 0, + "buffer handoff must not leave a separate jump command"); + + ASSERT(rspq_cur_sentinel == rspq_cur_pointer + + RSPQ_DRAM_LOWPRI_BUFFER_SIZE - + (RSPQ_MAX_SHORT_COMMAND_SIZE + 5), + "lowpri sentinel must reserve the full atomic handoff"); + + // Exercise repeated highpri requests immediately after lowpri wraps. The + // layout assertions above make the old implementation fail deterministically; + // this loop also runs the handoff under the preemption pattern that exposed + // the original race. + for (int i = 0; i < 32; i++) { + volatile uint32_t *buffer = rspq_cur_pointer; + uint32_t buffer_command_count = 0; + do { + rspq_noop(); + buffer_command_count++; + } while (rspq_cur_pointer == buffer + buffer_command_count); + + rspq_highpri_begin(); + rspq_noop(); + rspq_highpri_end(); + } + rspq_highpri_sync(); + + // Highpri cannot itself be preempted, so it should retain the compact + // status-plus-jump handoff and its original usable buffer capacity. + rspq_highpri_begin(); + volatile uint32_t *first_highpri_buffer = rspq_cur_pointer; + uint32_t highpri_command_count = 0; + do { + rspq_noop(); + highpri_command_count++; + } while (rspq_cur_pointer == + first_highpri_buffer + highpri_command_count); + + volatile uint32_t *highpri_handoff = + first_highpri_buffer + highpri_command_count; + ASSERT_EQUAL_HEX(highpri_handoff[0] >> 24, RSPQ_CMD_WRITE_STATUS, + "highpri handoff should retain its compact completion signal"); + ASSERT_EQUAL_HEX(highpri_handoff[1] >> 24, RSPQ_CMD_JUMP, + "highpri handoff should retain its compact jump"); + ASSERT(rspq_cur_sentinel == rspq_cur_pointer + + RSPQ_DRAM_HIGHPRI_BUFFER_SIZE - + (RSPQ_MAX_SHORT_COMMAND_SIZE + 2), + "highpri sentinel should reserve only its two-word handoff"); + rspq_highpri_end(); + rspq_highpri_sync(); + + TEST_RSPQ_EPILOG(0, rspq_timeout); +} + void test_rspq_high_load(TestContext *ctx) { TEST_RSPQ_PROLOG(); @@ -516,6 +605,113 @@ void test_rspq_block(TestContext *ctx) TEST_RSPQ_EPILOG(0, rspq_timeout); } +static int rspq_block_reuse_atexit_count; + +static void rspq_block_reuse_atexit_cb(void *ctx) +{ + rspq_block_reuse_atexit_count++; + (void)ctx; +} + +void test_rspq_block_begin_reuse(TestContext *ctx) +{ + TEST_RSPQ_PROLOG(); + test_ovl_init(); + DEFER(test_ovl_close()); + + // begin_reuse(NULL) should behave like rspq_block_begin (same effect on RSP side). + rspq_block_begin_reuse(NULL); + rspq_test_8(1); + rspq_test_8(2); + rspq_block_t *b_reuse_null = rspq_block_end(); + DEFER(rspq_block_free(b_reuse_null)); + + rspq_block_begin(); + rspq_test_8(1); + rspq_test_8(2); + rspq_block_t *b_traditional = rspq_block_end(); + DEFER(rspq_block_free(b_traditional)); + + uint64_t sum_reuse[2] __attribute__((aligned(16))) = {0}; + uint64_t sum_trad[2] __attribute__((aligned(16))) = {0}; + data_cache_hit_writeback_invalidate(sum_reuse, 16); + data_cache_hit_writeback_invalidate(sum_trad, 16); + + rspq_test_reset(); + rspq_block_run(b_reuse_null); + rspq_test_output(sum_reuse); + rspq_wait(); + + rspq_test_reset(); + rspq_block_run(b_traditional); + rspq_test_output(sum_trad); + rspq_wait(); + ASSERT_EQUAL_UNSIGNED(sum_reuse[0], sum_trad[0], "begin_reuse(NULL) block differs from rspq_block_begin"); + + // Same pointer after re-recording; second recording still runs. + rspq_block_begin(); + rspq_test_8(7); + rspq_block_t *p = rspq_block_end(); + rspq_block_begin_reuse(p); + rspq_test_8(8); + rspq_block_t *p2 = rspq_block_end(); + ASSERT(p == p2, "begin_reuse must keep the same block pointer"); + + data_cache_hit_invalidate(sum_reuse, 16); + rspq_test_reset(); + rspq_block_run(p2); + rspq_test_output(sum_reuse); + rspq_wait(); + data_cache_hit_invalidate(sum_reuse, 16); + ASSERT_EQUAL_UNSIGNED(sum_reuse[0], 8u, "reused block wrong sum"); + + DEFER(rspq_block_free(p2)); + + // Large block (extra RSP chain chunks), then reuse and run again. + rspq_block_begin(); + for (uint32_t i = 0; i < 4096; i++) { + rspq_test_8(1); + if (i % 256 == 0) + rspq_test_wait(0x10); + } + rspq_block_t *big = rspq_block_end(); + rspq_block_begin_reuse(big); + for (uint32_t i = 0; i < 4096; i++) { + rspq_test_8(1); + if (i % 256 == 0) + rspq_test_wait(0x10); + } + rspq_block_t *big2 = rspq_block_end(); + ASSERT(big == big2, "large block pointer must be stable"); + + data_cache_hit_invalidate(sum_trad, 16); + rspq_test_reset(); + rspq_block_run(big2); + rspq_test_output(sum_trad); + rspq_wait(); + data_cache_hit_invalidate(sum_trad, 16); + ASSERT_EQUAL_UNSIGNED(sum_trad[0], 4096u, "large reused block wrong sum"); + + DEFER(rspq_block_free(big2)); + + // atexit runs on begin_reuse (tear down old content) and on free. + rspq_block_reuse_atexit_count = 0; + rspq_block_begin(); + rspq_block_atexit(rspq_block_reuse_atexit_cb, NULL); + rspq_test_8(1); + rspq_block_t *ac = rspq_block_end(); + ASSERT_EQUAL_UNSIGNED((unsigned)rspq_block_reuse_atexit_count, 0u, "atexit before reuse"); + rspq_block_begin_reuse(ac); + ASSERT_EQUAL_UNSIGNED((unsigned)rspq_block_reuse_atexit_count, 1u, "atexit not run on begin_reuse"); + rspq_block_atexit(rspq_block_reuse_atexit_cb, NULL); + rspq_block_end(); + ASSERT_EQUAL_UNSIGNED((unsigned)rspq_block_reuse_atexit_count, 1u, "atexit must not run on end"); + rspq_block_free(ac); + ASSERT_EQUAL_UNSIGNED((unsigned)rspq_block_reuse_atexit_count, 2u, "atexit not run on free"); + + TEST_RSPQ_EPILOG(0, rspq_timeout); +} + void test_rspq_wait_sync_in_block(TestContext *ctx) { TEST_RSPQ_PROLOG(); @@ -525,11 +721,13 @@ void test_rspq_wait_sync_in_block(TestContext *ctx) rspq_syncpoint_t syncpoint = rspq_syncpoint_new(); rspq_block_begin(); - DEFER(rspq_block_end()); - rspq_syncpoint_wait(syncpoint); + rspq_block_t *block = rspq_block_end(); + DEFER(rspq_block_free(block)); + + rspq_wait(); - // Test will cause an RSP crash (timeout) if it fails. + TEST_RSPQ_EPILOG(0, rspq_timeout); } // Test the basic working of highpri queue. @@ -815,3 +1013,263 @@ void test_rspq_rdp_dynamic_switch(TestContext *ctx) } } + +void test_rspq_deferred_call(TestContext *ctx) +{ + TEST_RSPQ_PROLOG(); + test_ovl_init(); + + int num_call_expected = 0; + int num_call_found = 0; + + uint64_t *actual_sum = malloc_uncached(2 * sizeof(uint64_t)); + DEFER(free_uncached(actual_sum)); + + int value = 0; + + void cb1(void* expectedp) { + ++num_call_found; + int exp = (int)expectedp; + volatile uint64_t cur_counter = actual_sum[0]; + ASSERT(cur_counter >= exp, "invalid sequence for deferred call (expected %d, got %d)", exp, (int)cur_counter); + } + + rspq_test_reset(); + + SRAND(123); + for (int i=0;i<1000;i++) { + switch (RANDN(8)) { + case 0: case 1: case 2: { + rspq_test_4(1); value+=1; + } break; + case 3: { + rspq_test_output(actual_sum); + rspq_syncpoint_new_cb(cb1, (void*)value); + num_call_expected++; + } break; + case 4: case 5: { + int count = RANDN(RSPQ_DRAM_LOWPRI_BUFFER_SIZE / 16); + for (int j=0;jresult == TEST_FAILED) + return; + } + + rspq_wait(); + if (ctx->result == TEST_FAILED) + return; + + ASSERT_EQUAL_UNSIGNED(num_call_found, num_call_expected, "invalid number of deferred calls"); +} + +void test_rspq_queue_basic(TestContext *ctx) +{ + TEST_RSPQ_PROLOG(); + + test_ovl_init(); + DEFER(test_ovl_close()); + + rspq_queue_t *q = rspq_queue_create(); + DEFER(rspq_queue_destroy(q)); + + const uint32_t count = 8; + uint64_t actual_sum[4] __attribute__((aligned(16))) = {0}; + data_cache_hit_writeback_invalidate(actual_sum, sizeof(actual_sum)); + + rspq_test_reset(); + rspq_queue_switch(q); + for (uint32_t i = 0; i < count; i++) + rspq_test_4(1); + + rspq_test_output(actual_sum + 0); + rspq_queue_run(q); + + for (uint32_t i = 0; i < count; i++) + rspq_test_4(1); + rspq_test_output(actual_sum + 2); + rspq_queue_run(q); + + rspq_queue_switch(NULL); + rspq_wait(); + + ASSERT_EQUAL_UNSIGNED(actual_sum[0], count, "queue sum is not correct"); + ASSERT_EQUAL_UNSIGNED(actual_sum[2], count*2, "queue sum is not correct"); + TEST_RSPQ_EPILOG(0, rspq_timeout); +} + +void test_rspq_queue_clear(TestContext *ctx) +{ + TEST_RSPQ_PROLOG(); + + test_ovl_init(); + DEFER(test_ovl_close()); + + rspq_queue_t *q = rspq_queue_create(); + DEFER(rspq_queue_destroy(q)); + + const uint32_t count_a = 32*8; + const uint32_t count_b = 5*8; + + rspq_queue_switch(q); + for (uint32_t i = 0; i < count_a; i++) + rspq_test_4(1); + + rspq_queue_clear(q); + + for (uint32_t i = 0; i < count_b; i++) + rspq_test_4(1); + rspq_queue_switch(NULL); + + rspq_test_reset(); + + uint64_t actual_sum[2] __attribute__((aligned(16))) = {0}; + data_cache_hit_writeback_invalidate(actual_sum, 16); + + rspq_queue_run(q); + rspq_test_output(actual_sum); + rspq_wait(); + + ASSERT_EQUAL_UNSIGNED(*actual_sum, count_b, "queue clear did not discard old commands"); + TEST_RSPQ_EPILOG(0, rspq_timeout); +} + +void test_rspq_queue_growth(TestContext *ctx) +{ + TEST_RSPQ_PROLOG(); + + test_ovl_init(); + DEFER(test_ovl_close()); + + rspq_queue_t *q = rspq_queue_create(); + DEFER(rspq_queue_destroy(q)); + + const uint32_t count = RSPQ_BLOCK_MIN_SIZE * 8; + + rspq_test_reset(); + rspq_queue_switch(q); + for (uint32_t i = 0; i < count; i++) + rspq_test_8(1); + rspq_queue_switch(NULL); + + uint64_t actual_sum[2] __attribute__((aligned(16))) = {0}; + data_cache_hit_writeback_invalidate(actual_sum, 16); + + rspq_queue_run(q); + rspq_test_output(actual_sum); + rspq_wait(); + + ASSERT_EQUAL_UNSIGNED(*actual_sum, count, "queue growth sum is not correct"); + TEST_RSPQ_EPILOG(0, rspq_timeout); +} + +void test_rspq_queue_order(TestContext *ctx) +{ + TEST_RSPQ_PROLOG(); + + test_ovl_init(); + DEFER(test_ovl_close()); + + rspq_queue_t *q1 = rspq_queue_create(); + rspq_queue_t *q2 = rspq_queue_create(); + rspq_queue_t *q3 = rspq_queue_create(); + DEFER(rspq_queue_destroy(q3)); + DEFER(rspq_queue_destroy(q2)); + DEFER(rspq_queue_destroy(q1)); + + const uint32_t count1 = 3; + const uint32_t count2 = 5; + const uint32_t count3 = 7; + + rspq_queue_switch(q1); + for (uint32_t i = 0; i < count1; i++) + rspq_test_4(1); + + rspq_queue_switch(q2); + for (uint32_t i = 0; i < count2; i++) + rspq_test_4(1); + + rspq_queue_switch(q3); + for (uint32_t i = 0; i < count3; i++) + rspq_test_4(1); + + uint64_t actual_sum[6] __attribute__((aligned(16))) = {0}; + data_cache_hit_writeback_invalidate(actual_sum, sizeof(actual_sum)); + + rspq_queue_switch(NULL); + rspq_test_reset(); + + rspq_queue_run(q2); + rspq_test_output(actual_sum + 0); + + rspq_queue_run(q1); + rspq_test_output(actual_sum + 2); + + rspq_queue_run(q3); + rspq_test_output(actual_sum + 4); + + rspq_wait(); + + ASSERT_EQUAL_UNSIGNED(actual_sum[0], count2, "queue order step 1 is not correct"); + ASSERT_EQUAL_UNSIGNED(actual_sum[2], count2 + count1, "queue order step 2 is not correct"); + ASSERT_EQUAL_UNSIGNED(actual_sum[4], count2 + count1 + count3, "queue order step 3 is not correct"); + + TEST_RSPQ_EPILOG(0, rspq_timeout); +} + +void test_rspq_queue_block_nesting(TestContext *ctx) +{ + TEST_RSPQ_PROLOG(); + + test_ovl_init(); + DEFER(test_ovl_close()); + + const uint32_t b1_count = 4; + const uint32_t b2_extra = 2; + const uint32_t q_before = 3; + const uint32_t q_after = 5; + + rspq_block_begin(); + for (uint32_t i = 0; i < b1_count; i++) + rspq_test_4(1); + rspq_block_t *b1 = rspq_block_end(); + DEFER(rspq_block_free(b1)); + + rspq_block_begin(); + rspq_block_run(b1); + for (uint32_t i = 0; i < b2_extra; i++) + rspq_test_4(1); + rspq_block_t *b2 = rspq_block_end(); + DEFER(rspq_block_free(b2)); + + rspq_queue_t *q = rspq_queue_create(); + DEFER(rspq_queue_destroy(q)); + + uint64_t actual_sum[6] __attribute__((aligned(16))) = {0}; + data_cache_hit_writeback_invalidate(actual_sum, sizeof(actual_sum)); + + rspq_queue_switch(q); + for (uint32_t i = 0; i < q_before; i++) + rspq_test_4(1); + rspq_test_output(actual_sum + 0); + rspq_block_run(b2); + rspq_test_output(actual_sum + 2); + for (uint32_t i = 0; i < q_after; i++) + rspq_test_4(1); + rspq_test_output(actual_sum + 4); + rspq_queue_switch(NULL); + + rspq_test_reset(); + rspq_queue_run(q); + rspq_wait(); + + ASSERT_EQUAL_UNSIGNED(actual_sum[0], q_before, "queue pre-block sum is not correct"); + ASSERT_EQUAL_UNSIGNED(actual_sum[2], q_before + b1_count + b2_extra, "queue block sum is not correct"); + ASSERT_EQUAL_UNSIGNED(actual_sum[4], q_before + b1_count + b2_extra + q_after, "queue post-block sum is not correct"); + + TEST_RSPQ_EPILOG(0, rspq_timeout); +} diff --git a/tests/test_samplebuffer.c b/tests/test_samplebuffer.c new file mode 100644 index 000000000..1d2b503c0 --- /dev/null +++ b/tests/test_samplebuffer.c @@ -0,0 +1,316 @@ +/** + * @file test_samplebuffer.c + * @brief Standalone testrom for the samplebuffer circular logic + * + * A synthetic producer appends fixed-size chunks whose content is a known + * function of the absolute waveform position; every window handed out by + * #samplebuffer_get is verified against it. Covers declared append + * granularity, seeks, undos, and 8-bit odd-wrap parity. + */ +#include +#include +#include +#include +#include + +#define SB_CHUNK 960 // append granularity of the fake codec + +static int sb_undo_units; // units the producer drops from a read +static bool sb_undo_always; // drop on every read, not just the seeking ones +static int sb_nwindows; +static int sb_failed; + +static int16_t sb_expected(int pos) { + return (int16_t)(pos * 7919 + 12345); +} + +static void sb_prod_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + (void)ctx; + int n = (wlen + SB_CHUNK - 1) / SB_CHUNK; + int16_t *out = samplebuffer_append(sbuf, n * SB_CHUNK); + for (int i = 0; i < n * SB_CHUNK; i++) + out[i] = sb_expected(wpos + i); + // Emulate the Opus intra-frame skip: part of the decoded frame is dropped. + if (sb_undo_units && (seeking || sb_undo_always)) + samplebuffer_undo(sbuf, sb_undo_units); +} + +static waveform_t sb_wave = { + .name = "fake", .bits = 16, .channels = 1, .frequency = 48000, + .len = 1 << 30, .read = sb_prod_read, .append_units = SB_CHUNK, +}; + +static waveform_t sb_rsp_wave = { + .name = "fake-rsp", .bits = 16, .channels = 1, .frequency = 48000, + .len = 1 << 30, .append_units = 1024, .rsp_written = true, +}; + +// ULC produces an aligned full block and undoes the invalid tail. Verify that +// reusing its stereo ring for mono keeps the next RSP destination aligned. +static bool sb_rsp_undo_alignment(void) +{ + void *mem = malloc_uncached(16384); + assertf(mem != NULL, "malloc_uncached failed"); + + samplebuffer_t sb = {0}; + samplebuffer_init(&sb, mem, 16384, 0); + samplebuffer_set_bps(&sb, 32); + samplebuffer_set_waveform(&sb, &sb_rsp_wave, NULL); + + // 42 stereo frames occupy 168 bytes (aligned), but carrying the numeric + // head 42 into a mono configuration would address byte 84. + samplebuffer_append(&sb, 1024); + samplebuffer_undo(&sb, 1024 - 42); + samplebuffer_flush(&sb); + samplebuffer_set_bps(&sb, 16); + samplebuffer_set_waveform(&sb, &sb_rsp_wave, NULL); + void *mono = samplebuffer_append(&sb, 1024); + bool ok = ((uintptr_t)mono & 7) == 0; + + char line[96]; + snprintf(line, sizeof(line), " %-22s %s\n", + "rsp undo/reconfigure", ok ? "PASS" : "FAIL"); + printf("%s", line); + debugf("%s", line); + + memset(&sb, 0, sizeof(sb)); + free_uncached(mem); + return ok; +} + +// Same trim, but the waveform loops on itself: the buffer restarts through a +// flush, with no reconfiguration in between. Whatever the trim left behind, the +// next block must still land on an 8-byte boundary. +static bool sb_rsp_trim_alignment(void) +{ + void *mem = malloc_uncached(16384); + assertf(mem != NULL, "malloc_uncached failed"); + bool ok = true; + + for (int bps = 16; bps <= 32; bps *= 2) { + // A waveform ends at an arbitrary sample, so the kept part of the last + // block covers every phase of the 8-byte boundary. + for (int keep = 1; keep <= 8 && ok; keep++) { + samplebuffer_t sb = {0}; + samplebuffer_init(&sb, mem, 16384, 0); + samplebuffer_set_bps(&sb, bps); + samplebuffer_set_waveform(&sb, &sb_rsp_wave, NULL); + + samplebuffer_append(&sb, 1024); + samplebuffer_undo(&sb, 1024 - keep); + samplebuffer_flush(&sb); + void *next = samplebuffer_append(&sb, 1024); + if ((uintptr_t)next & 7) { + printf(" FAIL bps=%d keep=%d: unaligned %p\n", bps, keep, next); + debugf(" FAIL bps=%d keep=%d: unaligned %p\n", bps, keep, next); + ok = false; + } + memset(&sb, 0, sizeof(sb)); + } + } + + char line[96]; + snprintf(line, sizeof(line), " %-22s %s\n", + "rsp undo/loop", ok ? "PASS" : "FAIL"); + printf("%s", line); + debugf("%s", line); + + free_uncached(mem); + return ok; +} + +// 8-bit PCM: unit_bytes is 1, so an odd-length append leaves widx odd and the +// next discard can make wpos odd. The ring slot and the waveform position must +// keep the same byte parity for dma_read; both becoming odd is fine. +static int8_t sb8_expected(int pos) { + return (int8_t)(pos * 17 + 3); +} + +static void sb8_prod_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + (void)ctx; (void)seeking; + // Odd chunks so the write cursor spends most of its life on an odd index. + while (wlen > 0) { + int n = wlen > 17 ? 17 : wlen; + int8_t *out = samplebuffer_append(sbuf, n); + for (int i = 0; i < n; i++) + out[i] = sb8_expected(wpos + i); + wlen -= n; + wpos += n; + } +} + +static waveform_t sb8_wave = { + .name = "fake8", .bits = 8, .channels = 1, .frequency = 22050, + .len = 1 << 30, .read = sb8_prod_read, +}; + +static bool sb_check_window(const int16_t *p, int pos, int len) +{ + sb_nwindows++; + for (int i = 0; i < len; i++) { + if (p[i] != sb_expected(pos + i)) { + printf(" FAIL mismatch at pos=%d (+%d): got %04x, expected %04x\n", + pos, i, (uint16_t)p[i], (uint16_t)sb_expected(pos + i)); + debugf(" FAIL mismatch at pos=%d (+%d): got %04x, expected %04x\n", + pos, i, (uint16_t)p[i], (uint16_t)sb_expected(pos + i)); + return false; + } + } + return true; +} + +static bool sb8_stress(const char *tag, int nbytes, int niter) +{ + sb_nwindows = 0; + + void *mem = malloc_uncached(nbytes); + assertf(mem != NULL, "malloc_uncached(%d) failed", nbytes); + + samplebuffer_t sb = {0}; + samplebuffer_init(&sb, mem, nbytes, 0); + samplebuffer_set_bps(&sb, 8); + samplebuffer_set_waveform(&sb, &sb8_wave, sb8_prod_read); + + uint32_t rng = 424242; + int pos = 0; + bool ok = true; + for (int i = 0; i < niter && ok; i++) { + rng = rng * 1103515245 + 12345; + int want = 1 + (int)((rng >> 9) % 100); + int len = want; + int8_t *p = samplebuffer_get(&sb, pos, &len); + assertf(len > 0, "samplebuffer_get returned nothing"); + sb_nwindows++; + for (int j = 0; j < len; j++) { + if (p[j] != sb8_expected(pos + j)) { + printf(" FAIL 8bit mismatch at pos=%d (+%d): got %02x, expected %02x\n", + pos, j, (uint8_t)p[j], (uint8_t)sb8_expected(pos + j)); + ok = false; + break; + } + } + // Consume an odd count often enough that the next discard leaves an + // odd wpos: that is the state the old assert rejected. + pos += 1 + (int)((rng >> 17) % len); + } + + char line[96]; + snprintf(line, sizeof(line), " %-22s %s size=%5d windows=%5d\n", + tag, ok ? "PASS" : "FAIL", sb.size, sb_nwindows); + printf("%s", line); + debugf("%s", line); + + memset(&sb, 0, sizeof(sb)); + free_uncached(mem); + return ok; +} + +// Drive a samplebuffer of `nbytes` for `niter` windows, seeking every +// `seek_every` iterations, and verify all the data handed out. +static bool sb_stress(const char *tag, int nbytes, + int niter, int seek_every, int undo, bool ualways) +{ + sb_undo_units = undo; + sb_undo_always = ualways; + sb_nwindows = 0; + + void *mem = malloc_uncached(nbytes); + assertf(mem != NULL, "malloc_uncached(%d) failed", nbytes); + + samplebuffer_t sb = {0}; + samplebuffer_init(&sb, mem, nbytes, 0); + samplebuffer_set_bps(&sb, 16); + samplebuffer_set_waveform(&sb, &sb_wave, sb_prod_read); + + uint32_t rng = 12345; + int pos = 0; + bool ok = true; + for (int i = 0; i < niter && ok; i++) { + rng = rng * 1103515245 + 12345; + if (seek_every && i && i % seek_every == 0) { + // Jump around: backwards (flush) or far forward (flush). + pos = (int)((rng >> 8) % 100000); + continue; + } + int want = 1 + (int)((rng >> 9) % 100); + int len = want; + int16_t *p = samplebuffer_get(&sb, pos, &len); + assertf(len > 0, "samplebuffer_get returned nothing"); + ok = sb_check_window(p, pos, len); + // Consume part of the window, like the mixer does. + pos += 1 + (int)((rng >> 17) % len); + } + + char line[96]; + snprintf(line, sizeof(line), " %-22s %s size=%5d windows=%5d\n", + tag, ok ? "PASS" : "FAIL", sb.size, sb_nwindows); + printf("%s", line); + debugf("%s", line); + + // Do not call samplebuffer_close: it would free `mem` a second time. + memset(&sb, 0, sizeof(sb)); + free_uncached(mem); + return ok; +} + +int main(void) +{ + debug_init_emulog(); + debug_init_usblog(); + emux_ioctl_fast(); + console_init(); + + // Reconfiguring a samplebuffer may call #rspq_highpri_sync; keep the queue + // engine alive. + rspq_init(); + + printf("samplebuffer stress tests\n\n"); + debugf("samplebuffer stress tests\n\n"); + + // Usable size covers several chunks plus the mirrored tail (sized to + // append_units). `odd` is not a multiple of the chunk, so every lap of the + // ring spills a different amount into the tail. Stress with seeks and + // undos across the wrap. + int big = (SB_CHUNK * 4 + SB_CHUNK) * 2; + int odd = big + 2 * 133; + int small = (SB_CHUNK * 3 + SB_CHUNK) * 2; + + int total = 0; + #define RUN(...) do { total++; if (!sb_stress(__VA_ARGS__)) sb_failed++; } while (0) + RUN("plain", big, 3000, 0, 0, false); + RUN("seek", big, 3000, 97, 0, false); + RUN("seek+undo", big, 3000, 97, 333, false); + RUN("odd", odd, 3000, 0, 0, false); + RUN("odd+undo", odd, 3000, 97, 333, false); + RUN("drift", small, 6000, 0, 333, true); + RUN("drift+seek", small, 6000, 397, 333, true); + RUN("drift-odd", odd, 6000, 0, 333, true); + #undef RUN + + total++; + if (!sb_rsp_undo_alignment()) + sb_failed++; + + total++; + if (!sb_rsp_trim_alignment()) + sb_failed++; + + // 8-bit PCM with odd appends: wraps the ring with an odd wpos. + { + int nbytes = (SAMPLEBUFFER_MARGIN_UNITS * 4 + SAMPLEBUFFER_MARGIN_UNITS) * 1; + total++; + if (!sb8_stress("pcm8/odd-wrap", nbytes, 8000)) + sb_failed++; + } + + if (sb_failed) { + printf("\n%d/%d TESTS FAILED\n", sb_failed, total); + debugf("\n%d/%d TESTS FAILED\n", sb_failed, total); + abort(); + } + printf("\nALL TESTS PASSED (%d)\n", total); + debugf("\nALL TESTS PASSED (%d)\n", total); + return 0; +} diff --git a/tests/test_scratch.c b/tests/test_scratch.c new file mode 100644 index 000000000..418ed1f31 --- /dev/null +++ b/tests/test_scratch.c @@ -0,0 +1,272 @@ +#include +#include +#include +#include +#include "../include/scratch.h" + +static void scratch_fill_pattern(uint8_t *buf, size_t size, uint8_t seed) { + for (size_t i = 0; i < size; i++) { + buf[i] = (uint8_t)(seed + (uint8_t)i); + } +} + +static void scratch_assert_pattern(TestContext *ctx, const uint8_t *buf, size_t size, uint8_t seed, const char *what) { + for (size_t i = 0; i < size; i++) { + uint8_t expected = (uint8_t)(seed + (uint8_t)i); + ASSERT_EQUAL_HEX(buf[i], expected, "%s: mismatch at offset %u", what, (unsigned)i); + } +} + +static void scratch_assert_invariants(TestContext *ctx, const char *where) { + scratch_stats_t st; + scratch_get_stats(&st); + + ASSERT((scratch_empty() && st.live_blocks == 0) || (!scratch_empty() && st.live_blocks != 0), + "%s: scratch_empty() and live_blocks disagree", where); + ASSERT(st.peak_bytes >= st.live_bytes, "%s: peak_bytes < live_bytes", where); + + scratch_check(); +} + +static void scratch_reset_for_test(TestContext *ctx) { + scratch_stats_t st; + scratch_get_stats(&st); + ASSERT(scratch_empty(), "scratch not empty at test start (live_blocks=%u)", (unsigned)st.live_blocks); + if (!scratch_empty()) return; + ASSERT_EQUAL_UNSIGNED(st.live_bytes, 0, "live_bytes should reset to zero"); + ASSERT_EQUAL_UNSIGNED(st.live_blocks, 0, "live_blocks should reset to zero"); + ASSERT_EQUAL_UNSIGNED(st.reserved_bytes, 0, "reserved_bytes should reset to zero"); +} + +void test_scratch_basics(TestContext *ctx) { + scratch_reset_for_test(ctx); + + scratch_stats_t before, after; + scratch_get_stats(&before); + scratch_free(NULL); + scratch_get_stats(&after); + ASSERT_EQUAL_UNSIGNED(after.live_bytes, before.live_bytes, "scratch_free(NULL) changed live_bytes"); + ASSERT_EQUAL_UNSIGNED(after.live_blocks, before.live_blocks, "scratch_free(NULL) changed live_blocks"); + ASSERT_EQUAL_UNSIGNED(after.reserved_bytes, before.reserved_bytes, "scratch_free(NULL) changed reserved_bytes"); + + const size_t sizes[] = { 1, 15, 16, 17, 1024 }; + for (size_t i = 0; i < sizeof(sizes)/sizeof(sizes[0]); i++) { + uint8_t *p = scratch_malloc(sizes[i]); + ASSERT(p != NULL, "scratch_malloc(%u) failed", (unsigned)sizes[i]); + ASSERT_EQUAL_UNSIGNED(((uintptr_t)p & 15u), 0, "pointer is not 16-byte aligned"); + scratch_fill_pattern(p, sizes[i], (uint8_t)(0x10 + i)); + scratch_assert_pattern(ctx, p, sizes[i], (uint8_t)(0x10 + i), "basic fill/read"); + scratch_free(p); + scratch_assert_invariants(ctx, "after aligned malloc/free"); + } + + uint8_t *p0 = scratch_malloc(0); + ASSERT(p0 != NULL, "scratch_malloc(0) returned NULL"); + scratch_get_stats(&after); + ASSERT_EQUAL_UNSIGNED(after.live_blocks, 1, "scratch_malloc(0) should allocate one block"); + ASSERT_EQUAL_UNSIGNED(after.live_bytes, 1, "scratch_malloc(0) should account one byte"); + scratch_free(p0); + + uint8_t *p = scratch_malloc(128); + ASSERT(p != NULL, "scratch_malloc(128) failed"); + scratch_fill_pattern(p, 128, 0x42); + scratch_assert_pattern(ctx, p, 128, 0x42, "roundtrip pattern"); + scratch_free(p); + + scratch_get_stats(&after); + ASSERT(scratch_empty(), "scratch should be empty after roundtrip"); + ASSERT_EQUAL_UNSIGNED(after.live_bytes, 0, "live_bytes should be zero"); + ASSERT_EQUAL_UNSIGNED(after.live_blocks, 0, "live_blocks should be zero"); + ASSERT_EQUAL_UNSIGNED(after.reserved_bytes, 0, "reserved_bytes should be zero"); +} + +void test_scratch_calloc(TestContext *ctx) { + scratch_reset_for_test(ctx); + + const size_t count = 64; + const size_t elem = 4; + uint8_t *p = scratch_calloc(count, elem); + ASSERT(p != NULL, "scratch_calloc(%u,%u) failed", (unsigned)count, (unsigned)elem); + for (size_t i = 0; i < count * elem; i++) { + ASSERT_EQUAL_HEX(p[i], 0, "scratch_calloc not zeroed at offset %u", (unsigned)i); + } + + scratch_stats_t st; + scratch_get_stats(&st); + ASSERT_EQUAL_UNSIGNED(st.live_bytes, count * elem, "calloc live_bytes mismatch"); + ASSERT_EQUAL_UNSIGNED(st.live_blocks, 1, "calloc live_blocks mismatch"); + scratch_free(p); + + scratch_stats_t before, after; + scratch_get_stats(&before); + void *ov = scratch_calloc(SIZE_MAX, 2); + ASSERT(ov == NULL, "scratch_calloc overflow should return NULL"); + scratch_get_stats(&after); + ASSERT_EQUAL_UNSIGNED(after.live_bytes, before.live_bytes, "overflow changed live_bytes"); + ASSERT_EQUAL_UNSIGNED(after.live_blocks, before.live_blocks, "overflow changed live_blocks"); + ASSERT_EQUAL_UNSIGNED(after.reserved_bytes, before.reserved_bytes, "overflow changed reserved_bytes"); +} + +void test_scratch_stats_and_peak(TestContext *ctx) { + scratch_reset_for_test(ctx); + + scratch_stats_t st_start; + scratch_get_stats(&st_start); + size_t expected_peak = st_start.peak_bytes > 300 ? st_start.peak_bytes : 300; + + void *a = scratch_malloc(100); + void *b = scratch_malloc(200); + ASSERT(a && b, "scratch_malloc failed in stats test"); + + scratch_stats_t st; + scratch_get_stats(&st); + ASSERT_EQUAL_UNSIGNED(st.live_bytes, 300, "live_bytes after two allocations"); + ASSERT_EQUAL_UNSIGNED(st.live_blocks, 2, "live_blocks after two allocations"); + ASSERT_EQUAL_UNSIGNED(st.peak_bytes, expected_peak, "peak_bytes after two allocations"); + ASSERT(st.reserved_bytes >= st.live_bytes, "reserved_bytes should include live_bytes"); + + scratch_free(b); + scratch_get_stats(&st); + ASSERT_EQUAL_UNSIGNED(st.live_bytes, 100, "live_bytes after free(b)"); + ASSERT_EQUAL_UNSIGNED(st.live_blocks, 1, "live_blocks after free(b)"); + ASSERT_EQUAL_UNSIGNED(st.peak_bytes, expected_peak, "peak_bytes should not decrease after free(b)"); + + scratch_free(a); + scratch_get_stats(&st); + ASSERT_EQUAL_UNSIGNED(st.live_bytes, 0, "live_bytes after free(a)"); + ASSERT_EQUAL_UNSIGNED(st.live_blocks, 0, "live_blocks after free(a)"); + ASSERT_EQUAL_UNSIGNED(st.peak_bytes, expected_peak, "peak_bytes should keep max value"); + ASSERT_EQUAL_UNSIGNED(st.reserved_bytes, 0, "reserved_bytes should collapse to zero"); +} + +void test_scratch_collapse_ordering(TestContext *ctx) { + scratch_reset_for_test(ctx); + + void *a = scratch_malloc(128); + void *b = scratch_malloc(256); + void *c = scratch_malloc(384); + ASSERT(a && b && c, "scratch_malloc failed in collapse test"); + + scratch_stats_t st; + scratch_get_stats(&st); + size_t reserved_after_alloc = st.reserved_bytes; + ASSERT_EQUAL_UNSIGNED(st.live_blocks, 3, "expected 3 live blocks"); + ASSERT_EQUAL_UNSIGNED(st.live_bytes, 128 + 256 + 384, "live_bytes after A/B/C alloc"); + + scratch_free(b); + scratch_get_stats(&st); + ASSERT_EQUAL_UNSIGNED(st.live_blocks, 2, "live_blocks after free(B)"); + ASSERT_EQUAL_UNSIGNED(st.live_bytes, 128 + 384, "live_bytes after free(B)"); + + scratch_free(c); + scratch_get_stats(&st); + ASSERT_EQUAL_UNSIGNED(st.live_blocks, 1, "live_blocks after free(C)"); + ASSERT_EQUAL_UNSIGNED(st.live_bytes, 128, "live_bytes after free(C)"); + ASSERT(st.reserved_bytes < reserved_after_alloc, + "reserved_bytes should decrease after trimming free head chain"); + ASSERT(st.reserved_bytes > 0, "reserved_bytes should stay >0 while A is live"); + + scratch_free(a); + scratch_get_stats(&st); + ASSERT(scratch_empty(), "scratch should be empty after free(A)"); + ASSERT_EQUAL_UNSIGNED(st.reserved_bytes, 0, "reserved_bytes should return to zero"); + scratch_assert_invariants(ctx, "after collapse ordering"); +} + +void test_scratch_realloc(TestContext *ctx) { + scratch_reset_for_test(ctx); + + uint8_t *p = scratch_realloc(NULL, 64); + ASSERT(p != NULL, "scratch_realloc(NULL,64) failed"); + scratch_fill_pattern(p, 64, 0x31); + p = scratch_realloc(p, 0); + ASSERT(p == NULL, "scratch_realloc(ptr,0) should return NULL"); + ASSERT(scratch_empty(), "realloc(ptr,0) should free allocation"); + + uint8_t *upper = scratch_malloc(64); + uint8_t *lower = scratch_malloc(64); + ASSERT(upper && lower, "malloc failed in realloc grow test"); + scratch_fill_pattern(lower, 64, 0x55); + scratch_free(upper); + uint8_t *grown = scratch_realloc(lower, 96); + ASSERT(grown != NULL, "realloc grow failed"); + scratch_assert_pattern(ctx, grown, 64, 0x55, "realloc grow preserves prefix"); + + uint8_t *shrunk = scratch_realloc(grown, 32); + ASSERT(shrunk != NULL, "realloc shrink failed"); + scratch_assert_pattern(ctx, shrunk, 32, 0x55, "realloc shrink preserves prefix"); + scratch_free(shrunk); + ASSERT(scratch_empty(), "scratch should be empty after grow/shrink sequence"); + scratch_stats_t mid; + scratch_get_stats(&mid); + ASSERT_EQUAL_UNSIGNED(mid.reserved_bytes, 0, "scratch should have no reserved bytes after grow/shrink sequence"); + scratch_check(); + + uint8_t *x = scratch_malloc(128); + uint8_t *y = scratch_malloc(128); + ASSERT(x && y, "malloc failed in realloc relocate test"); + scratch_fill_pattern(y, 128, 0x72); + uint8_t *new_y = scratch_realloc(y, 512); + ASSERT(new_y != NULL, "realloc relocate grow failed"); + scratch_assert_pattern(ctx, new_y, 128, 0x72, "realloc relocate preserves data"); + scratch_check(); + scratch_free(new_y); + scratch_check(); + scratch_free(x); + + uint8_t *z = scratch_malloc(256); + ASSERT(z != NULL, "malloc failed before realloc failure test"); + scratch_fill_pattern(z, 256, 0x93); + void *z2 = scratch_realloc(z, (size_t)(32u * 1024u * 1024u)); + ASSERT(z2 == NULL, "realloc to huge size should fail"); + scratch_assert_pattern(ctx, z, 256, 0x93, "realloc failure keeps original data"); + scratch_free(z); + + scratch_stats_t st; + scratch_get_stats(&st); + ASSERT_EQUAL_UNSIGNED(st.live_blocks, 0, "realloc test should end with no live blocks"); + ASSERT_EQUAL_UNSIGNED(st.reserved_bytes, 0, "realloc test should fully collapse"); +} + +void test_scratch_exhaustion_recovery(TestContext *ctx) { + scratch_reset_for_test(ctx); + + uint8_t *big = scratch_malloc(70000); + ASSERT(big != NULL, "large allocation (>64KiB) failed"); + big[0] = 0xAA; + big[69999] = 0x55; + ASSERT_EQUAL_HEX(big[0], 0xAA, "large allocation first byte mismatch"); + ASSERT_EQUAL_HEX(big[69999], 0x55, "large allocation last byte mismatch"); + scratch_free(big); + ASSERT(scratch_empty(), "scratch should be empty after freeing big allocation"); + + enum { MAX_PTRS = 512 }; + void *ptrs[MAX_PTRS]; + memset(ptrs, 0, sizeof(ptrs)); + + int count = 0; + bool hit_null = false; + for (int i = 0; i < MAX_PTRS; i++) { + ptrs[i] = scratch_malloc(64 * 1024); + if (!ptrs[i]) { + hit_null = true; + break; + } + count++; + } + ASSERT(hit_null, "exhaustion loop did not hit NULL within %d allocations", MAX_PTRS); + ASSERT(count > 0, "exhaustion loop should allocate at least one block"); + + for (int i = count - 1; i >= 0; i--) { + scratch_free(ptrs[i]); + } + + scratch_stats_t st; + scratch_get_stats(&st); + ASSERT_EQUAL_UNSIGNED(st.live_blocks, 0, "live_blocks should be zero after recovery"); + ASSERT_EQUAL_UNSIGNED(st.reserved_bytes, 0, "reserved_bytes should be zero after recovery"); + + void *p = scratch_malloc(256); + ASSERT(p != NULL, "allocation after recovery failed"); + scratch_free(p); +} diff --git a/tests/test_sf64.c b/tests/test_sf64.c new file mode 100644 index 000000000..92e4a6f62 --- /dev/null +++ b/tests/test_sf64.c @@ -0,0 +1,3008 @@ +/** + * @file test_sf64.c + * @author Giovanni Bajo + * @brief Standalone testrom for the SF64 bank loader and synth + * + * Loads the deterministic sf64test bank, checks preset/region/sample layout, + * plays embedded waveforms through the real mixer, and exercises the + * synthesizer through step 5 (pitch, matching, envelopes, polyphony, layers, + * note identity, voice stealing, and the SF2 gain model). + */ +#include +#include +#include +#include +#include + +#include "../src/audio/sf64_internal.h" + +#define SAMPLE_RATE 22050 +#define CH 0 +#define NCH 8 +#define AMP 16000 +#define SQ 16 +#define A_LEN 4096 +#define B_INTRO 1000 +#define B_LOOP 1000 +#define B_REL 1000 +#define B_LEN (B_INTRO + B_LOOP + B_REL) +#define C_LEN 4096 +#define D_LEN 4096 +#define VOL 0.5f + +// Must match tests/assets/gen_sf64_bank.py (plus the trailing pad sample TSF adds). +static int16_t pcm_a(int i) { + if (i < 0 || i >= A_LEN) return 0; + return (int16_t)(-AMP + (int32_t)(2 * AMP) * i / A_LEN); +} +static int16_t pcm_b(int i) { + if (i < 0 || i >= B_LEN) return 0; + if (i < B_INTRO) + return (int16_t)(-AMP + (int32_t)(2 * AMP) * i / B_INTRO); + if (i < B_INTRO + B_LOOP) + return ((i - B_INTRO) % SQ < SQ / 2) ? AMP : (int16_t)-AMP; + int j = i - (B_INTRO + B_LOOP); + return (int16_t)(AMP - (int32_t)(2 * AMP) * j / B_REL); +} +static int16_t pcm_c(int i) { + if (i < 0 || i >= C_LEN) return 0; + return (i % SQ < SQ / 2) ? (AMP / 2) : (int16_t)-(AMP / 2); +} +static int16_t pcm_d(int i) { + if (i < 0 || i >= D_LEN) return 0; + return (int16_t)(AMP - (int32_t)(2 * AMP) * (i % 64) / 64); +} + +static int16_t *out; +static int out_cap; + +static void mix(int nsamples) +{ + assert(nsamples * 2 <= out_cap); + int16_t *p = out; + int left = nsamples; + while (left > 0) { + int n = left < 1024 ? left : 1024; + mixer_poll(p, n); + p += n * 2; + left -= n; + } +} + +static void silence(void) +{ + mixer_ch_stop(CH); + mix(2048); +} + +/** Stop mixer channels and clear synth voice state (re-binds the range). */ +static void synth_silence(sf64_synth_t *synth, int nch) +{ + for (int i = 0; i < nch; i++) + mixer_ch_stop(i); + mix(2048); + sf64_synth_set_channels(synth, 0, nch, MIXER_PRIORITY_MUSIC); +} + +// Settle the volume ramp at step 0 so the playhead stays on @p start. +static void play_at(waveform_t *wave, int start) +{ + silence(); + mixer_ch_set_limits(CH, 16, SAMPLE_RATE, 0); + mixer_ch_play(CH, wave); + mixer_ch_set_vol(CH, VOL, VOL); + mixer_ch_set_pos(CH, start); + mixer_ch_set_freq(CH, 0); + mix(2048); + mixer_ch_set_freq(CH, SAMPLE_RATE); +} + +// Hermite at 1:1 returns the second tap (source pos + 1). Volume is 0.5. +static bool check_pcm(const char *tag, int start, int nsamples, + int16_t (*sample)(int), int loop_end, int loop_len) +{ + int nbad = 0, first = -1, maxdiff = 0, peak = 0; + for (int i = 0; i < nsamples; i++) { + int p = start + 1 + i; + if (loop_len && p >= loop_end) + p = loop_end - loop_len + (p - loop_end) % loop_len; + int want = sample(p) / 2; + int got = out[i * 2]; + int d = got - want; if (d < 0) d = -d; + if (d > 64) { + nbad++; + if (first < 0) first = i; + if (d > maxdiff) maxdiff = d; + } + int a = got < 0 ? -got : got; + if (a > peak) peak = a; + } + if (nbad) { + printf("FAILED %s: %d/%d off (max %d) from %d\n", + tag, nbad, nsamples, maxdiff, first); + for (int i = first; i < first + 6 && i < nsamples; i++) { + int p = start + 1 + i; + if (loop_len && p >= loop_end) + p = loop_end - loop_len + (p - loop_end) % loop_len; + printf(" [%d] play=%d got %d want %d\n", + i, p, out[i * 2], sample(p) / 2); + } + return false; + } + if (peak < 2048) { + printf("FAILED %s: silent (peak %d)\n", tag, peak); + return false; + } + return true; +} + +static bool test_bank_layout(sf64_bank_t *bank) +{ + if (bank->num_presets != 6 || bank->num_regions != 11 || bank->num_samples != 4) { + printf("FAILED layout: presets=%d regions=%d samples=%d\n", + bank->num_presets, bank->num_regions, bank->num_samples); + return false; + } + if (sf64_preset_count(bank) != 6) { + printf("FAILED preset_count\n"); + return false; + } + if (sf64_find_preset(bank, 0, 0) != 0 || + sf64_find_preset(bank, 0, 1) != 1 || + sf64_find_preset(bank, 0, 2) != 2 || + sf64_find_preset(bank, 0, 3) != 3 || + sf64_find_preset(bank, 0, 4) != 4 || + sf64_find_preset(bank, 128, 0) != 5 || + sf64_find_preset(bank, 0, 5) != -1) { + printf("FAILED find_preset\n"); + return false; + } + int mb, prog; + sf64_preset_id(bank, 1, &mb, &prog); + if (mb != 0 || prog != 1 || strcmp(sf64_preset_name(bank, 0), "KeySplit") != 0 || + strcmp(sf64_preset_name(bank, 1), "VelSplit") != 0 || + strcmp(sf64_preset_name(bank, 3), "Layer2") != 0 || + strcmp(sf64_preset_name(bank, 4), "Layer3") != 0 || + strcmp(sf64_preset_name(bank, 5), "DrumKit") != 0) { + printf("FAILED preset introspect: bank=%d prog=%d name0=%s name1=%s\n", + mb, prog, sf64_preset_name(bank, 0), sf64_preset_name(bank, 1)); + return false; + } + + sf64_preset_t *p0 = &bank->presets[0]; + sf64_preset_t *p1 = &bank->presets[1]; + sf64_preset_t *p2 = &bank->presets[2]; + sf64_preset_t *p3 = &bank->presets[3]; + sf64_preset_t *p4 = &bank->presets[4]; + sf64_preset_t *p5 = &bank->presets[5]; + if (p0->bank != 0 || p0->program != 0 || p0->num_regions != 2 || + p1->bank != 0 || p1->program != 1 || p1->num_regions != 2 || + p2->bank != 0 || p2->program != 2 || p2->num_regions != 1 || + p3->bank != 0 || p3->program != 3 || p3->num_regions != 2 || + p4->bank != 0 || p4->program != 4 || p4->num_regions != 3 || + p5->bank != 128 || p5->program != 0 || p5->num_regions != 1) { + printf("FAILED preset headers\n"); + return false; + } + + // Format counts must fit the on-disk uint16 fields. + if (bank->num_presets > 65535 || bank->num_regions > 65535 || + bank->num_samples > 65535) { + printf("FAILED layout: counts exceed uint16\n"); + return false; + } + + // Preset 0: A key 0-71, C key 72-127 + sf64_region_t *r = &bank->regions[p0->first_region]; + if (r[0].key_min != 0 || r[0].key_max != 71 || r[0].root_key != 60 || + r[0].loop_mode != SF64_LOOP_NONE || r[0].sample_index != 0) { + printf("FAILED preset0 region A\n"); + return false; + } + if (r[1].key_min != 72 || r[1].key_max != 127 || r[1].root_key != 72 || + r[1].loop_mode != SF64_LOOP_NONE || r[1].sample_index != 1) { + printf("FAILED preset0 region C\n"); + return false; + } + + // Preset 1: A vel 0-79, D vel 80-127 (D has 200 cB → gain 0.1). + r = &bank->regions[p1->first_region]; + if (r[0].velocity_min != 0 || r[0].velocity_max != 79 || + r[0].sample_index != 0 || + r[1].velocity_min != 80 || r[1].velocity_max != 127 || + r[1].sample_index != 2 || r[1].gain < 0.09f || r[1].gain > 0.11f) { + printf("FAILED preset1 velocity split (gainD=%f)\n", r[1].gain); + return false; + } + + // Dedup: sample A is shared by KeySplit / VelSplit / Layer2. + if (bank->regions[p0->first_region].sample_index != + bank->regions[p1->first_region].sample_index || + bank->regions[p0->first_region].sample_index != + bank->regions[p3->first_region].sample_index) { + printf("FAILED layout: sample A not deduplicated across presets\n"); + return false; + } + + // Preset 2: sustain loop B, loop_end < len + r = &bank->regions[p2->first_region]; + sf64_sample_t *sb = &bank->samples[r->sample_index]; + if (r->loop_mode != SF64_LOOP_SUSTAIN || r->root_key != 60 || + sb->loop_start != B_INTRO || sb->loop_end != B_INTRO + B_LOOP || + sb->loop_end >= sb->sample_end || sb->sample_rate != SAMPLE_RATE) { + printf("FAILED preset2 sample B: mode=%d ls=%lu le=%lu end=%lu rate=%lu\n", + r->loop_mode, (unsigned long)sb->loop_start, (unsigned long)sb->loop_end, + (unsigned long)sb->sample_end, (unsigned long)sb->sample_rate); + return false; + } + + // Preset 3 Layer2 / 4 Layer3: overlapping full-range regions. + r = &bank->regions[p3->first_region]; + if (r[0].sample_index == r[1].sample_index || + r[0].key_min != 0 || r[0].key_max != 127 || + r[1].key_min != 0 || r[1].key_max != 127) { + printf("FAILED preset3 Layer2 regions\n"); + return false; + } + r = &bank->regions[p4->first_region]; + if (r[0].sample_index == r[1].sample_index || + r[1].sample_index == r[2].sample_index || + r[0].sample_index == r[2].sample_index) { + printf("FAILED preset4 Layer3 regions\n"); + return false; + } + + for (int i = 0; i < bank->num_samples; i++) { + sf64_sample_t *s = &bank->samples[i]; + wav64_t *w = bank->waves[i]; + if (!w || w->wave.channels != 1 || w->wave.bits != 16 || + w->wave.frequency != SAMPLE_RATE || + (int)w->wave.len != (int)s->sample_end) { + printf("FAILED sample %d waveform (len=%d meta=%lu)\n", + i, w ? (int)w->wave.len : -1, (unsigned long)s->sample_end); + return false; + } + if (s->loop_end && s->loop_end >= s->sample_end) { + printf("FAILED sample %d: loop_end=%lu >= end=%lu\n", + i, (unsigned long)s->loop_end, (unsigned long)s->sample_end); + return false; + } + } + return true; +} + +static bool test_play_oneshot(sf64_bank_t *bank, int si, int16_t (*sample)(int), const char *tag) +{ + waveform_t *wave = &bank->waves[si]->wave; + play_at(wave, 0); + int pos = (int)mixer_ch_get_pos(CH); + mix(1024); + return check_pcm(tag, pos, 1024, sample, wave->len, 0); +} + +static bool test_play_loop(sf64_bank_t *bank) +{ + // Sample B is last in the bank (index 3). + int si = bank->regions[bank->presets[2].first_region].sample_index; + waveform_t *wave = &bank->waves[si]->wave; + int loop_end = wave->loop_end ? wave->loop_end : wave->len; + int loop_len = wave->loop_len; + + if (loop_end != B_INTRO + B_LOOP || loop_len != B_LOOP) { + printf("FAILED loop wave: end=%d len=%d (wave len=%d)\n", + loop_end, loop_len, (int)wave->len); + return false; + } + + // Start inside the loop and verify wrapping. + play_at(wave, B_INTRO + 8); + int pos = (int)mixer_ch_get_pos(CH); + mix(2048); + if (!check_pcm("sample B loop", pos, 2048, pcm_b, loop_end, loop_len)) + return false; + + // Note-off: disable loop, drain through the release tail, then idle. + silence(); + mixer_ch_play(CH, wave); + mixer_ch_set_vol(CH, VOL, VOL); + mixer_ch_set_pos(CH, B_INTRO + B_LOOP / 2); + mixer_ch_set_freq(CH, 0); + mix(2048); + mixer_ch_set_freq(CH, SAMPLE_RATE); + mixer_ch_set_loop(CH, false); + pos = (int)mixer_ch_get_pos(CH); + mix(B_REL + 512); + if (!check_pcm("sample B release", pos, B_REL - 8, pcm_b, wave->len, 0)) + return false; + mix(2048); + if (mixer_ch_playing(CH)) { + printf("FAILED sample B release: still playing at %d (len=%d)\n", + (int)mixer_ch_get_pos(CH), (int)wave->len); + return false; + } + return true; +} + +static int peak_range(int start, int n) +{ + int peak = 0; + for (int i = start * 2; i < (start + n) * 2; i++) { + int a = out[i] < 0 ? -out[i] : out[i]; + if (a > peak) peak = a; + } + return peak; +} + +// An envelope of several seconds asks the mixer for a ramp shallower than the +// one fx15 unit per block of four samples that is the least the ucode can +// walk. It has to be bounded by the volume each round ends on: bounded by the +// end of the whole ramp instead, it would run through every round many times +// too fast and be pulled back at the next one, an audible flutter. +static bool test_slow_ramp(sf64_bank_t *bank) +{ + // The loop of sample B is a square of constant amplitude, so the peak of + // a window of output is the volume the ramp has reached over it. + int si = bank->regions[bank->presets[2].first_region].sample_index; + waveform_t *wave = &bank->waves[si]->wave; + play_at(wave, B_INTRO + 8); + + const int SECS = 20, WIN = 64, BLK = 2048, NBLK = 4; + mixer_ch_set_vol_ramp(CH, 0, 0, SECS * SAMPLE_RATE); + + int prev = -1; + for (int b = 0; b < NBLK; b++) { + mix(BLK); + for (int i = 0; i < BLK / WIN; i++) { + int peak = peak_range(i * WIN, WIN); + // The envelope only sinks: any rise is the ramp snapping back. + if (prev >= 0 && peak > prev + 4) { + printf("FAILED slow ramp: peak %d -> %d at sample %d\n", + prev, peak, b * BLK + i * WIN); + silence(); + return false; + } + prev = peak; + } + } + + // And it sank by as much as it should have, give or take a round. + float t = (float)(NBLK * BLK - WIN) / (float)(SECS * SAMPLE_RATE); + int want = (int)(AMP * VOL * (1.0f - t)); + if (prev < want - 32 || prev > want + 8) { + printf("FAILED slow ramp: peak %d after %d samples, want %d\n", + prev, NBLK * BLK, want); + silence(); + return false; + } + silence(); + return true; +} + +// Expected playback frequency for a region / key (SF2 pitch formula). +static float expect_freq(const sf64_region_t *r, const sf64_sample_t *s, int key) +{ + float cents = (key - r->root_key) * (float)r->pitch_keytrack + + r->coarse_tune * 100.0f + r->fine_tune; + return s->sample_rate * powf(2.0f, cents / 1200.0f); +} + +// Advance of the playhead over @p nsamples at the current mixer frequency. +static bool check_freq(sf64_synth_t *synth, const char *tag, float want) +{ + (void)synth; + double p0 = mixer_ch_get_pos(CH); + mix(512); + double got = (mixer_ch_get_pos(CH) - p0) * SAMPLE_RATE / 512.0; + float err = got - want; + if (err < 0) err = -err; + if (err > want * 0.02f + 1.0f) { + printf("FAILED %s: freq got %.1f want %.1f\n", tag, got, want); + return false; + } + return true; +} + +static bool test_synth_pitch(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 1, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + + sf64_region_t *ra = &bank->regions[bank->presets[0].first_region]; + sf64_sample_t *sa = &bank->samples[ra->sample_index]; + + synth_silence(synth, 1); + if (!sf64_synth_note_on(synth, 0, 60, 100)) { + printf("FAILED pitch: note_on root\n"); + sf64_synth_close(synth); + return false; + } + if (mixer_ch_playing_waveform(CH) != &bank->waves[ra->sample_index]->wave) { + printf("FAILED pitch: wrong waveform at root\n"); + sf64_synth_close(synth); + return false; + } + if (!check_freq(synth, "pitch root", expect_freq(ra, sa, 60))) { + sf64_synth_close(synth); + return false; + } + + // −12 on region A (root 60); +12 on region C (root 72, key 84). + sf64_region_t *rc = ra + 1; + sf64_sample_t *sc = &bank->samples[rc->sample_index]; + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 48, 100); + if (!check_freq(synth, "pitch -12", expect_freq(ra, sa, 48))) { + sf64_synth_close(synth); + return false; + } + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 84, 100); + if (mixer_ch_playing_waveform(CH) != &bank->waves[rc->sample_index]->wave) { + printf("FAILED pitch +12: wrong waveform\n"); + sf64_synth_close(synth); + return false; + } + if (!check_freq(synth, "pitch +12", expect_freq(rc, sc, 84))) { + sf64_synth_close(synth); + return false; + } + // Expect ~2× the sample rate. + if (fabsf(expect_freq(rc, sc, 84) - 2.0f * sc->sample_rate) > 1.0f) { + printf("FAILED pitch +12: expect_freq not 2x rate\n"); + sf64_synth_close(synth); + return false; + } + + // Coarse / fine / keytrack by patching the live region, then restore. + int8_t save_coarse = ra->coarse_tune; + int16_t save_fine = ra->fine_tune; + int16_t save_kt = ra->pitch_keytrack; + ra->coarse_tune = 12; + ra->fine_tune = 0; + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 100); + if (!check_freq(synth, "pitch coarse+12", expect_freq(ra, sa, 60))) { + ra->coarse_tune = save_coarse; + sf64_synth_close(synth); + return false; + } + ra->coarse_tune = 0; + ra->fine_tune = 100; // +1 semitone + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 100); + if (!check_freq(synth, "pitch fine+100", expect_freq(ra, sa, 60))) { + ra->fine_tune = save_fine; + sf64_synth_close(synth); + return false; + } + ra->fine_tune = 0; + ra->pitch_keytrack = 50; // half tracking + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 66, 100); + if (!check_freq(synth, "pitch keytrack50", expect_freq(ra, sa, 66))) { + ra->pitch_keytrack = save_kt; + sf64_synth_close(synth); + return false; + } + ra->coarse_tune = save_coarse; + ra->fine_tune = save_fine; + ra->pitch_keytrack = save_kt; + + sf64_synth_close(synth); + return true; +} + +static bool test_synth_key_split(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 1, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + + sf64_region_t *r = &bank->regions[bank->presets[0].first_region]; + synth_silence(synth, 1); + if (!sf64_synth_note_on(synth, 0, 60, 100) || + mixer_ch_playing_waveform(CH) != &bank->waves[r[0].sample_index]->wave) { + printf("FAILED key_split: low key\n"); + sf64_synth_close(synth); + return false; + } + synth_silence(synth, 1); + if (!sf64_synth_note_on(synth, 0, 72, 100) || + mixer_ch_playing_waveform(CH) != &bank->waves[r[1].sample_index]->wave) { + printf("FAILED key_split: high key\n"); + sf64_synth_close(synth); + return false; + } + // Boundary inclusivity: 71 → A, 72 → C. + synth_silence(synth, 1); + if (!sf64_synth_note_on(synth, 0, 71, 100) || + mixer_ch_playing_waveform(CH) != &bank->waves[r[0].sample_index]->wave) { + printf("FAILED key_split: key 71 boundary\n"); + sf64_synth_close(synth); + return false; + } + + // Key outside every region: shrink both ranges, leave a hole at 60. + uint8_t a_min = r[0].key_min, a_max = r[0].key_max; + uint8_t c_min = r[1].key_min, c_max = r[1].key_max; + r[0].key_min = 0; r[0].key_max = 40; + r[1].key_min = 80; r[1].key_max = 127; + synth_silence(synth, 1); + if (sf64_synth_note_on(synth, 0, 60, 100)) { + printf("FAILED key_split: key outside ranges still played\n"); + r[0].key_min = a_min; r[0].key_max = a_max; + r[1].key_min = c_min; r[1].key_max = c_max; + sf64_synth_close(synth); + return false; + } + if (mixer_ch_playing(CH)) { + printf("FAILED key_split: channel active after unmatched note_on\n"); + r[0].key_min = a_min; r[0].key_max = a_max; + r[1].key_min = c_min; r[1].key_max = c_max; + sf64_synth_close(synth); + return false; + } + r[0].key_min = a_min; r[0].key_max = a_max; + r[1].key_min = c_min; r[1].key_max = c_max; + + sf64_synth_close(synth); + return true; +} + +static bool test_synth_vel_split(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 1, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 1)); + + sf64_region_t *r = &bank->regions[bank->presets[1].first_region]; + synth_silence(synth, 1); + if (!sf64_synth_note_on(synth, 0, 60, 79) || + mixer_ch_playing_waveform(CH) != &bank->waves[r[0].sample_index]->wave) { + printf("FAILED vel_split: vel 79\n"); + sf64_synth_close(synth); + return false; + } + synth_silence(synth, 1); + if (!sf64_synth_note_on(synth, 0, 60, 80) || + mixer_ch_playing_waveform(CH) != &bank->waves[r[1].sample_index]->wave) { + printf("FAILED vel_split: vel 80\n"); + sf64_synth_close(synth); + return false; + } + // Upper bound inclusive. + synth_silence(synth, 1); + if (!sf64_synth_note_on(synth, 0, 60, 127) || + mixer_ch_playing_waveform(CH) != &bank->waves[r[1].sample_index]->wave) { + printf("FAILED vel_split: vel 127\n"); + sf64_synth_close(synth); + return false; + } + sf64_synth_close(synth); + return true; +} + +static int16_t samples_to_timecents(int n) +{ + if (n <= 0) return -12000; + return (int16_t)lroundf(1200.0f * log2f((float)n / (float)SAMPLE_RATE)); +} + +static int env_samples(int16_t tc) +{ + if (tc <= -12000) return 0; + int n = (int)lroundf(powf(2.0f, tc / 1200.0f) * (float)SAMPLE_RATE); + return n > 0 ? n : 0; +} + +/** Match synth keynum scaling: base + scale*(60-key), then timecents→samples. */ +static int env_scaled_samples_tc(int16_t base_tc, int16_t keynum_scale, int key) +{ + int tc = base_tc; + if (keynum_scale) + tc += (int)keynum_scale * (60 - key); + if (tc < -12000) tc = -12000; + if (tc > 8000) tc = 8000; + return env_samples((int16_t)tc); +} + +static int peak_n(int n) +{ + int peak = 0; + for (int i = 0; i < n * 2; i++) { + int a = out[i] < 0 ? -out[i] : out[i]; + if (a > peak) peak = a; + } + return peak; +} + +static int count_playing(int nch) +{ + int n = 0; + for (int i = 0; i < nch; i++) + if (mixer_ch_playing(i)) n++; + return n; +} + +static bool wave_playing(int nch, waveform_t *w) +{ + for (int i = 0; i < nch; i++) + if (mixer_ch_playing(i) && mixer_ch_playing_waveform(i) == w) + return true; + return false; +} + +////////////////////////////////////////////////////////////////////////////// +// Step 4 — polyphony and voice stealing +////////////////////////////////////////////////////////////////////////////// + +/** Sequential alloc, free, reuse of the lowest free channel, range. */ +static bool test_synth_allocator(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 3, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + + synth_silence(synth, 3); + if (!sf64_synth_note_on(synth, 0, 48, 100) || + !sf64_synth_note_on(synth, 0, 60, 100) || + !sf64_synth_note_on(synth, 0, 72, 100) || + !mixer_ch_playing(0) || !mixer_ch_playing(1) || !mixer_ch_playing(2)) { + printf("FAILED alloc: sequential fill of ch0-2\n"); + sf64_synth_close(synth); + return false; + } + + // Free the middle channel; the next note must take the lowest free one. + sf64_synth_note_off(synth, 0, 60); + mix(64); + if (mixer_ch_playing(1)) { + printf("FAILED alloc: note-off did not free ch1\n"); + sf64_synth_close(synth); + return false; + } + if (!sf64_synth_note_on(synth, 0, 64, 100) || !mixer_ch_playing(1) || + !mixer_ch_playing(0) || !mixer_ch_playing(2)) { + printf("FAILED alloc: reuse did not pick the lowest free channel\n"); + sf64_synth_close(synth); + return false; + } + + // first_channel=1, num=2 → only ch1/ch2, never ch0 or ch3. + synth_silence(synth, NCH); + sf64_synth_set_channels(synth, 1, 2, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_synth_note_on(synth, 0, 48, 100); + sf64_synth_note_on(synth, 0, 60, 100); + if (mixer_ch_playing(0) || !mixer_ch_playing(1) || !mixer_ch_playing(2) || + mixer_ch_playing(3)) { + printf("FAILED alloc: out-of-range channel used\n"); + sf64_synth_close(synth); + return false; + } + // Saturated range: steals inside [1,2], still never touches ch0/ch3. + if (!sf64_synth_note_on(synth, 0, 72, 100) || mixer_ch_playing(0) || + mixer_ch_playing(3) || + (int)mixer_ch_playing(1) + (int)mixer_ch_playing(2) != 2) { + printf("FAILED alloc: steal escaped the reserved range\n"); + sf64_synth_close(synth); + return false; + } + + sf64_synth_close(synth); + return true; +} + +/** Chord, note-off of one key, release frees only at deadline, reuse after. */ +static bool test_synth_polyphony(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 3, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + + synth_silence(synth, 3); + if (!sf64_synth_note_on(synth, 0, 48, 100) || + !sf64_synth_note_on(synth, 0, 60, 100) || + !sf64_synth_note_on(synth, 0, 72, 100)) { + printf("FAILED poly: could not start three notes\n"); + sf64_synth_close(synth); + return false; + } + if (!mixer_ch_playing(0) || !mixer_ch_playing(1) || !mixer_ch_playing(2)) { + printf("FAILED poly: expected channels 0-2 playing\n"); + sf64_synth_close(synth); + return false; + } + + sf64_synth_note_off(synth, 0, 60); + mix(64); + if (!mixer_ch_playing(0) || mixer_ch_playing(1) || !mixer_ch_playing(2)) { + printf("FAILED poly: note-off 60 should free only ch1\n"); + sf64_synth_close(synth); + return false; + } + + // Release with non-zero envelope: channel freed only at the deadline. + sf64_region_t *r = &bank->regions[bank->presets[0].first_region]; + int16_t save_rel = r->amp_env.release_timecents; + r->amp_env.release_timecents = samples_to_timecents(400); + int rel = env_samples(r->amp_env.release_timecents); + synth_silence(synth, 3); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_synth_note_on(synth, 0, 48, 100); + sf64_synth_note_off(synth, 0, 48); + if (!mixer_ch_playing(0)) { + printf("FAILED poly: release stopped immediately\n"); + r->amp_env.release_timecents = save_rel; + sf64_synth_close(synth); + return false; + } + int mid = sf64_synth_process(synth, rel / 2); + if (!mixer_ch_playing(0) || mid != rel - rel / 2) { + printf("FAILED poly: release mid remaining %d\n", mid); + r->amp_env.release_timecents = save_rel; + sf64_synth_close(synth); + return false; + } + sf64_synth_process(synth, mid); + mix(64); + if (mixer_ch_playing(0)) { + printf("FAILED poly: channel not freed after release deadline\n"); + r->amp_env.release_timecents = save_rel; + sf64_synth_close(synth); + return false; + } + if (!sf64_synth_note_on(synth, 0, 60, 100) || !mixer_ch_playing(0)) { + printf("FAILED poly: could not reuse freed channel\n"); + r->amp_env.release_timecents = save_rel; + sf64_synth_close(synth); + return false; + } + r->amp_env.release_timecents = save_rel; + + // note-off wrong key ignored; velocity 0 == note-off; repeated off ok. + synth_silence(synth, 3); + sf64_synth_set_channels(synth, 0, 1, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_synth_note_on(synth, 0, 60, 100); + sf64_synth_note_off(synth, 0, 48); + if (!mixer_ch_playing(0)) { + printf("FAILED poly: wrong-key note-off stopped the note\n"); + sf64_synth_close(synth); + return false; + } + sf64_synth_note_on(synth, 0, 60, 0); + mix(64); + if (mixer_ch_playing(0)) { + printf("FAILED poly: velocity 0 did not note-off\n"); + sf64_synth_close(synth); + return false; + } + sf64_synth_note_off(synth, 0, 60); + sf64_synth_note_off(synth, 0, 60); + + sf64_synth_close(synth); + return true; +} + +/** Full pool: new note steals a voice; polyphony stays at the limit. */ +static bool test_synth_saturation(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 3, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + + synth_silence(synth, 3); + sf64_synth_note_on(synth, 0, 48, 100); + sf64_synth_note_on(synth, 0, 60, 100); + sf64_synth_note_on(synth, 0, 72, 100); + mix(128); + + uint32_t id = sf64_synth_note_on(synth, 0, 64, 100); + if (!id) { + printf("FAILED sat: note rejected with full pool (stealing expected)\n"); + sf64_synth_close(synth); + return false; + } + if (count_playing(3) != 3) { + printf("FAILED sat: after steal playing=%d, want 3\n", count_playing(3)); + sf64_synth_close(synth); + return false; + } + + sf64_synth_close(synth); + return true; +} + +/** Voice stealing: phase order, age tie-break, layers, exclusive, stress. */ +static bool test_synth_voice_steal(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_region_t *r0 = &bank->regions[bank->presets[0].first_region]; + int16_t save_rel0 = r0[0].amp_env.release_timecents; + int16_t save_rel1 = r0[1].amp_env.release_timecents; + int16_t save_atk1 = r0[1].amp_env.attack_timecents; + waveform_t *wc = &bank->waves[r0[1].sample_index]->wave; + + // Release is cheaper than sustain. + sf64_synth_set_channels(synth, 0, 2, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + r0[0].amp_env.release_timecents = samples_to_timecents(2000); + r0[1].amp_env.release_timecents = samples_to_timecents(2000); + synth_silence(synth, 2); + sf64_synth_note_on(synth, 0, 48, 100); // A + sf64_synth_note_on(synth, 0, 72, 100); // C + sf64_synth_note_off(synth, 0, 48); // A → RELEASE + if (!sf64_synth_note_on(synth, 0, 60, 100) || count_playing(2) != 2 || + !wave_playing(2, wc)) { + printf("FAILED steal: release was not preferred over sustain\n"); + goto fail_steal; + } + + // Sustain is cheaper than attack. + r0[1].amp_env.attack_timecents = samples_to_timecents(2000); + synth_silence(synth, 2); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_synth_note_on(synth, 0, 48, 100); // A sustain + sf64_synth_note_on(synth, 0, 72, 100); // C attack + if (!sf64_synth_note_on(synth, 0, 60, 100) || count_playing(2) != 2 || + !wave_playing(2, wc)) { + printf("FAILED steal: sustain was not preferred over attack\n"); + goto fail_steal; + } + r0[1].amp_env.attack_timecents = save_atk1; + + // Same phase: oldest is stolen. + r0[0].amp_env.release_timecents = save_rel0; + r0[1].amp_env.release_timecents = save_rel1; + synth_silence(synth, 2); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_synth_note_on(synth, 0, 48, 100); + mix(512); + sf64_synth_note_on(synth, 0, 60, 100); + if (!sf64_synth_note_on(synth, 0, 64, 100)) { + printf("FAILED steal: oldest tie-break note rejected\n"); + goto fail_steal; + } + sf64_synth_note_off(synth, 0, 60); + mix(64); + if (count_playing(2) != 1) { + printf("FAILED steal: expected oldest (48) stolen, playing=%d\n", + count_playing(2)); + goto fail_steal; + } + + // Layer3 needs 3 slots: steals a full pool of mono notes. + sf64_synth_set_channels(synth, 0, 3, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + synth_silence(synth, 3); + sf64_synth_note_on(synth, 0, 48, 100); + sf64_synth_note_on(synth, 0, 60, 100); + sf64_synth_note_on(synth, 0, 65, 100); + assert(sf64_synth_set_program(synth, 0, 0, 4)); // Layer3 + uint32_t lid = sf64_synth_note_on(synth, 0, 72, 100); + if (!lid || count_playing(3) != 3) { + printf("FAILED steal: Layer3 did not claim 3 slots (id=%lu playing=%d)\n", + (unsigned long)lid, count_playing(3)); + goto fail_steal; + } + + // Stealing a layered identity removes all of its layers. + sf64_synth_set_channels(synth, 0, 2, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 3)); // Layer2 + synth_silence(synth, 2); + sf64_synth_note_on(synth, 0, 60, 100); // fills both channels + mix(64); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + // Mono note steals one layer channel; expansion must clear the other too. + if (!sf64_synth_note_on(synth, 0, 64, 100) || count_playing(2) != 1) { + printf("FAILED steal: layered victim not expanded (playing=%d)\n", + count_playing(2)); + goto fail_steal; + } + + // Exclusive victim is preferred (advertised at MIN). + sf64_synth_set_channels(synth, 0, 2, MIXER_PRIORITY_MUSIC); + uint8_t g0 = r0[0].exclusive_group, g1 = r0[1].exclusive_group; + r0[0].exclusive_group = 1; + r0[1].exclusive_group = 1; + assert(sf64_synth_set_program(synth, 0, 0, 0)); + synth_silence(synth, 2); + sf64_synth_note_on(synth, 0, 48, 100); // A group 1 + sf64_synth_note_on(synth, 0, 60, 100); // A group 1 + if (!sf64_synth_note_on(synth, 0, 72, 100) || count_playing(2) != 1 || + !wave_playing(2, wc)) { + printf("FAILED steal: exclusive victims not reclaimed (playing=%d)\n", + count_playing(2)); + r0[0].exclusive_group = g0; + r0[1].exclusive_group = g1; + goto fail_steal; + } + r0[0].exclusive_group = g0; + r0[1].exclusive_group = g1; + + // Stress: rapid note-on/off must not stick or over-allocate. + sf64_synth_set_channels(synth, 0, 4, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + synth_silence(synth, 4); + for (int i = 0; i < 2000; i++) { + int key = 48 + (i % 24); + sf64_synth_note_on(synth, 0, key, 100); + if ((i & 3) == 3) + sf64_synth_note_off(synth, 0, 48 + ((i - 3) % 24)); + if ((i & 15) == 15) { + sf64_synth_process(synth, 64); + mix(64); + } + } + if (count_playing(4) > 4) { + printf("FAILED steal: stress over-allocated (%d)\n", count_playing(4)); + goto fail_steal; + } + synth_silence(synth, 4); + if (count_playing(4) != 0) { + printf("FAILED steal: stress left stuck voices\n"); + goto fail_steal; + } + + r0[0].amp_env.release_timecents = save_rel0; + r0[1].amp_env.release_timecents = save_rel1; + r0[1].amp_env.attack_timecents = save_atk1; + sf64_synth_close(synth); + return true; + +fail_steal: + r0[0].amp_env.release_timecents = save_rel0; + r0[1].amp_env.release_timecents = save_rel1; + r0[1].amp_env.attack_timecents = save_atk1; + sf64_synth_close(synth); + return false; +} + +////////////////////////////////////////////////////////////////////////////// +// Step 5 — layers and note identity +////////////////////////////////////////////////////////////////////////////// + +/** Matching regions start together; non-matching ones are ignored. */ +static bool test_synth_layers(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 4, MIXER_PRIORITY_MUSIC); + + // Layer2: two full-range regions → two voices, distinct samples. + assert(sf64_synth_set_program(synth, 0, 0, 3)); + sf64_region_t *r2 = &bank->regions[bank->presets[3].first_region]; + waveform_t *wa = &bank->waves[r2[0].sample_index]->wave; + waveform_t *wc = &bank->waves[r2[1].sample_index]->wave; + synth_silence(synth, 4); + uint32_t id = sf64_synth_note_on(synth, 0, 60, 100); + if (id == 0 || count_playing(4) != 2 || + !wave_playing(4, wa) || !wave_playing(4, wc)) { + printf("FAILED layers: Layer2 did not start two distinct voices\n"); + sf64_synth_close(synth); + return false; + } + + // Layer3: three voices. + assert(sf64_synth_set_program(synth, 0, 0, 4)); + sf64_region_t *r3 = &bank->regions[bank->presets[4].first_region]; + synth_silence(synth, 4); + id = sf64_synth_note_on(synth, 0, 60, 100); + if (id == 0 || count_playing(4) != 3 || + !wave_playing(4, &bank->waves[r3[0].sample_index]->wave) || + !wave_playing(4, &bank->waves[r3[1].sample_index]->wave) || + !wave_playing(4, &bank->waves[r3[2].sample_index]->wave)) { + printf("FAILED layers: Layer3 did not start three voices\n"); + sf64_synth_close(synth); + return false; + } + + // KeySplit: key 60 matches only region A (C starts at 72). + assert(sf64_synth_set_program(synth, 0, 0, 0)); + synth_silence(synth, 4); + id = sf64_synth_note_on(synth, 0, 60, 100); + sf64_region_t *rk = &bank->regions[bank->presets[0].first_region]; + if (id == 0 || count_playing(4) != 1 || + !wave_playing(4, &bank->waves[rk[0].sample_index]->wave) || + wave_playing(4, &bank->waves[rk[1].sample_index]->wave)) { + printf("FAILED layers: incompatible region was not ignored\n"); + sf64_synth_close(synth); + return false; + } + + sf64_synth_close(synth); + return true; +} + +/** + * Layered note that needs more channels than the pool has is rejected with + * no partial state. With room to steal, a layered note takes free + stolen. + */ +static bool test_synth_layer_atomic(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 3, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + + synth_silence(synth, 3); + // Occupy two channels; one free. Layer2 needs two → steals one. + sf64_synth_note_on(synth, 0, 48, 100); + sf64_synth_note_on(synth, 0, 60, 100); + mix(64); + assert(sf64_synth_set_program(synth, 0, 0, 3)); + if (!sf64_synth_note_on(synth, 0, 72, 100) || count_playing(3) != 3) { + printf("FAILED atomic: layered note should steal to start (playing=%d)\n", + count_playing(3)); + sf64_synth_close(synth); + return false; + } + + // Pool smaller than the layer count → reject, leave the mono note alone. + sf64_synth_set_channels(synth, 0, 1, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 48, 100); + mix(64); + waveform_t *w0 = mixer_ch_playing_waveform(0); + double p0 = mixer_ch_get_pos(0); + assert(sf64_synth_set_program(synth, 0, 0, 3)); + if (sf64_synth_note_on(synth, 0, 72, 100) != 0) { + printf("FAILED atomic: layered note started with pool size 1\n"); + sf64_synth_close(synth); + return false; + } + if (mixer_ch_playing_waveform(0) != w0 || mixer_ch_get_pos(0) != p0) { + printf("FAILED atomic: rejected layer disturbed the existing voice\n"); + sf64_synth_close(synth); + return false; + } + + sf64_synth_close(synth); + return true; +} + +/** + * Stacked note-ons on the same key get distinct identities; note_off peels + * the oldest identity (and all of its layers) first. + */ +static bool test_synth_note_identity(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 4, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 3)); // Layer2 + sf64_region_t *r = &bank->regions[bank->presets[3].first_region]; + waveform_t *wa = &bank->waves[r[0].sample_index]->wave; + waveform_t *wc = &bank->waves[r[1].sample_index]->wave; + + synth_silence(synth, 4); + uint32_t id1 = sf64_synth_note_on(synth, 0, 60, 100); + uint32_t id2 = sf64_synth_note_on(synth, 0, 60, 100); + if (id1 == 0 || id2 == 0 || id1 == id2 || count_playing(4) != 4) { + printf("FAILED identity: stacked Layer2 note-ons (id1=%lu id2=%lu playing=%d)\n", + (unsigned long)id1, (unsigned long)id2, count_playing(4)); + sf64_synth_close(synth); + return false; + } + if (!wave_playing(4, wa) || !wave_playing(4, wc)) { + printf("FAILED identity: layered samples missing after stack\n"); + sf64_synth_close(synth); + return false; + } + + // First note_off releases only the oldest identity (2 voices). + sf64_synth_note_off(synth, 0, 60); + mix(64); + if (count_playing(4) != 2) { + printf("FAILED identity: first note-off left %d voices, want 2\n", + count_playing(4)); + sf64_synth_close(synth); + return false; + } + if (!wave_playing(4, wa) || !wave_playing(4, wc)) { + printf("FAILED identity: second identity lost a layer\n"); + sf64_synth_close(synth); + return false; + } + + sf64_synth_note_off(synth, 0, 60); + mix(64); + if (count_playing(4) != 0) { + printf("FAILED identity: second note-off left %d voices\n", + count_playing(4)); + sf64_synth_close(synth); + return false; + } + sf64_synth_note_off(synth, 0, 60); // no-op + + sf64_synth_close(synth); + return true; +} + +/** Independent attack/release deadlines across voices. */ +static bool test_synth_deadlines(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 2, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_region_t *r0 = &bank->regions[bank->presets[0].first_region]; + sf64_region_t *r1 = r0 + 1; + int16_t save_atk0 = r0->amp_env.attack_timecents; + int16_t save_atk1 = r1->amp_env.attack_timecents; + int16_t save_rel0 = r0->amp_env.release_timecents; + int16_t save_rel1 = r1->amp_env.release_timecents; + + // Two attacks with different deadlines → process returns the sooner one. + r0->amp_env.attack_timecents = samples_to_timecents(800); + r1->amp_env.attack_timecents = samples_to_timecents(200); + int atk0 = env_samples(r0->amp_env.attack_timecents); + int atk1 = env_samples(r1->amp_env.attack_timecents); + synth_silence(synth, 2); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_synth_note_on(synth, 0, 48, 100); // region A + sf64_synth_note_on(synth, 0, 72, 100); // region C + int dl = sf64_synth_process(synth, 0); + if (dl != atk1) { + printf("FAILED deadlines: soonest attack %d want %d\n", dl, atk1); + goto fail_dl; + } + // Several process(0) calls before the deadline leave it unchanged. + if (sf64_synth_process(synth, 0) != atk1 || + sf64_synth_process(synth, 0) != atk1) { + printf("FAILED deadlines: process(0) changed the pending attack\n"); + goto fail_dl; + } + dl = sf64_synth_process(synth, atk1 / 2); + if (dl != atk1 - atk1 / 2) { + printf("FAILED deadlines: mid-attack remaining %d want %d\n", + dl, atk1 - atk1 / 2); + goto fail_dl; + } + dl = sf64_synth_process(synth, dl); + if (dl != atk0 - atk1) { + printf("FAILED deadlines: after first attack remaining %d want %d\n", + dl, atk0 - atk1); + goto fail_dl; + } + sf64_synth_process(synth, dl); + if (sf64_synth_process(synth, 0) >= 0) { + printf("FAILED deadlines: expected no deadline in dual sustain\n"); + goto fail_dl; + } + + // Two equal releases; a late process past both deadlines frees both. + r0->amp_env.attack_timecents = -12000; + r1->amp_env.attack_timecents = -12000; + r0->amp_env.release_timecents = samples_to_timecents(300); + r1->amp_env.release_timecents = samples_to_timecents(300); + int rel = env_samples(r0->amp_env.release_timecents); + synth_silence(synth, 2); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_synth_note_on(synth, 0, 48, 100); + sf64_synth_note_on(synth, 0, 72, 100); + sf64_synth_note_off(synth, 0, 48); + sf64_synth_note_off(synth, 0, 72); + dl = sf64_synth_process(synth, 0); + if (dl != rel) { + printf("FAILED deadlines: equal release remaining %d want %d\n", dl, rel); + goto fail_dl; + } + sf64_synth_process(synth, rel + 50); + mix(64); + if (mixer_ch_playing(0) || mixer_ch_playing(1)) { + printf("FAILED deadlines: late process did not free both channels\n"); + goto fail_dl; + } + + r0->amp_env.attack_timecents = save_atk0; + r1->amp_env.attack_timecents = save_atk1; + r0->amp_env.release_timecents = save_rel0; + r1->amp_env.release_timecents = save_rel1; + sf64_synth_close(synth); + return true; + +fail_dl: + r0->amp_env.attack_timecents = save_atk0; + r1->amp_env.attack_timecents = save_atk1; + r0->amp_env.release_timecents = save_rel0; + r1->amp_env.release_timecents = save_rel1; + sf64_synth_close(synth); + return false; +} + +static bool test_synth_preset(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 2, MIXER_PRIORITY_MUSIC); + + if (sf64_synth_set_program(synth, 0, 0, 99)) { + printf("FAILED preset: missing program accepted\n"); + sf64_synth_close(synth); + return false; + } + if (!sf64_synth_set_program(synth, 0, 0, 0)) { + printf("FAILED preset: program 0 rejected\n"); + sf64_synth_close(synth); + return false; + } + + // Change preset while a note is sounding: the sounding wave stays, + // a note-on on a different key uses the new preset on another channel. + synth_silence(synth, 2); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_synth_note_on(synth, 0, 48, 100); + waveform_t *before = mixer_ch_playing_waveform(CH); + assert(sf64_synth_set_program(synth, 0, 0, 1)); + if (mixer_ch_playing_waveform(CH) != before) { + printf("FAILED preset: change interrupted the sounding note\n"); + sf64_synth_close(synth); + return false; + } + // High velocity on VelSplit → sample D on ch1 (key 60 ≠ 48, so no retrigger). + sf64_synth_note_on(synth, 0, 60, 100); + sf64_region_t *rd = &bank->regions[bank->presets[1].first_region + 1]; + if (mixer_ch_playing_waveform(1) != &bank->waves[rd->sample_index]->wave) { + printf("FAILED preset: next note did not use new preset\n"); + sf64_synth_close(synth); + return false; + } + if (mixer_ch_playing_waveform(CH) != before) { + printf("FAILED preset: retriggered the previous key\n"); + sf64_synth_close(synth); + return false; + } + + sf64_synth_close(synth); + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// Attack / release / sustain-loop (real mixer, virtual clock) +////////////////////////////////////////////////////////////////////////////// + +static bool test_synth_attack(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 1, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_region_t *r = &bank->regions[bank->presets[0].first_region]; + int16_t save_atk = r->amp_env.attack_timecents; + + // Zero attack → immediate sustain, no deadline. + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 100); + if (sf64_synth_process(synth, 0) >= 0) { + printf("FAILED attack0: expected no deadline\n"); + sf64_synth_close(synth); + return false; + } + mix(128); + if (peak_n(128) < 1024) { + printf("FAILED attack0: silent after note_on\n"); + sf64_synth_close(synth); + return false; + } + + // Non-zero attack on sample C (constant amplitude square): early quieter + // than post-attack, then sustain with matched L/R. + sf64_region_t *rc = r + 1; + int16_t save_atk_c = rc->amp_env.attack_timecents; + rc->amp_env.attack_timecents = samples_to_timecents(512); + int atk = env_samples(rc->amp_env.attack_timecents); + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 72, 100); + int dl = sf64_synth_process(synth, 0); + if (dl != atk) { + printf("FAILED attack: remaining %d want %d\n", dl, atk); + rc->amp_env.attack_timecents = save_atk_c; + sf64_synth_close(synth); + return false; + } + int mid = sf64_synth_process(synth, atk / 4); + if (mid != atk - atk / 4) { + printf("FAILED attack: remaining after %d is %d want %d\n", + atk / 4, mid, atk - atk / 4); + rc->amp_env.attack_timecents = save_atk_c; + sf64_synth_close(synth); + return false; + } + mix(32); + int early = peak_n(32); + mix((atk - 32 + 1) & ~1); + sf64_synth_process(synth, mid); // → sustain + if (sf64_synth_process(synth, 0) >= 0) { + printf("FAILED attack: not in sustain after deadline\n"); + rc->amp_env.attack_timecents = save_atk_c; + sf64_synth_close(synth); + return false; + } + mix(128); + int late = peak_n(128); + if (late < 1024 || early * 2 > late) { + printf("FAILED attack: early peak %d late %d\n", early, late); + rc->amp_env.attack_timecents = save_atk_c; + sf64_synth_close(synth); + return false; + } + int l = 0, rr = 0; + for (int i = 0; i < 128; i++) { + int a = out[i * 2] < 0 ? -out[i * 2] : out[i * 2]; + int b = out[i * 2 + 1] < 0 ? -out[i * 2 + 1] : out[i * 2 + 1]; + if (a > l) l = a; + if (b > rr) rr = b; + } + int d = l - rr; if (d < 0) d = -d; + if (d > 64) { + printf("FAILED attack: L/R mismatch %d/%d\n", l, rr); + rc->amp_env.attack_timecents = save_atk_c; + sf64_synth_close(synth); + return false; + } + + r->amp_env.attack_timecents = save_atk; + rc->amp_env.attack_timecents = save_atk_c; + sf64_synth_close(synth); + return true; +} + +static bool test_synth_release(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 1, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_region_t *r = &bank->regions[bank->presets[0].first_region]; + int16_t save_atk = r->amp_env.attack_timecents; + int16_t save_rel = r->amp_env.release_timecents; + + // Zero release in sustain → immediate stop. + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 100); + sf64_synth_note_off(synth, 0, 60); + mix(64); + if (mixer_ch_playing(CH)) { + printf("FAILED release0: still playing\n"); + sf64_synth_close(synth); + return false; + } + + // Note-off during attack: release ramp replaces attack. + r->amp_env.attack_timecents = samples_to_timecents(800); + r->amp_env.release_timecents = samples_to_timecents(400); + int atk = env_samples(r->amp_env.attack_timecents); + int rel = env_samples(r->amp_env.release_timecents); + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 100); + mix(64); + sf64_synth_process(synth, 64); // still in attack + sf64_synth_note_off(synth, 0, 60); + int dl = sf64_synth_process(synth, 0); + if (dl != rel) { + printf("FAILED release-in-attack: remaining %d want %d\n", dl, rel); + r->amp_env.attack_timecents = save_atk; + r->amp_env.release_timecents = save_rel; + sf64_synth_close(synth); + return false; + } + if (!mixer_ch_playing(CH)) { + printf("FAILED release-in-attack: stopped too early\n"); + r->amp_env.attack_timecents = save_atk; + r->amp_env.release_timecents = save_rel; + sf64_synth_close(synth); + return false; + } + mix(rel); + // Late / overdue process still stops. + sf64_synth_process(synth, rel + 100); + mix(64); + if (mixer_ch_playing(CH)) { + printf("FAILED release-in-attack: late process did not stop\n"); + r->amp_env.attack_timecents = save_atk; + r->amp_env.release_timecents = save_rel; + sf64_synth_close(synth); + return false; + } + + // Note-off in sustain with non-zero release. + r->amp_env.attack_timecents = -12000; + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 100); + sf64_synth_note_off(synth, 0, 60); + dl = sf64_synth_process(synth, 0); + if (dl != rel) { + printf("FAILED release-sustain: remaining %d want %d\n", dl, rel); + r->amp_env.attack_timecents = save_atk; + r->amp_env.release_timecents = save_rel; + sf64_synth_close(synth); + return false; + } + mix(rel / 2); + dl = sf64_synth_process(synth, rel / 2); + if (!mixer_ch_playing(CH) || dl != rel - rel / 2) { + printf("FAILED release-sustain: early stop or remaining %d\n", dl); + r->amp_env.attack_timecents = save_atk; + r->amp_env.release_timecents = save_rel; + sf64_synth_close(synth); + return false; + } + sf64_synth_process(synth, dl); + mix(64); + if (mixer_ch_playing(CH)) { + printf("FAILED release-sustain: not stopped at deadline\n"); + r->amp_env.attack_timecents = save_atk; + r->amp_env.release_timecents = save_rel; + sf64_synth_close(synth); + return false; + } + + (void)atk; + r->amp_env.attack_timecents = save_atk; + r->amp_env.release_timecents = save_rel; + sf64_synth_close(synth); + return true; +} + +static bool test_synth_loop_env(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 1, MIXER_PRIORITY_MUSIC); + + // Sustain loop (preset 2 / sample B): note-off disables loop, pos leaves the loop. + assert(sf64_synth_set_program(synth, 0, 0, 2)); + sf64_region_t *r = &bank->regions[bank->presets[2].first_region]; + int16_t save_rel = r->amp_env.release_timecents; + uint8_t save_mode = r->loop_mode; + r->amp_env.release_timecents = samples_to_timecents(600); + int rel = env_samples(r->amp_env.release_timecents); + + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 100); + // Land inside the loop. + mixer_ch_set_freq(CH, 0); + mixer_ch_set_pos(CH, B_INTRO + 8); + mix(2048); + mixer_ch_set_freq(CH, SAMPLE_RATE); + sf64_synth_note_off(synth, 0, 60); + mix(256); + double pos = mixer_ch_get_pos(CH); + if (pos < B_INTRO + B_LOOP) { + // May still be before loop_end early in the release; mix more. + mix(B_LOOP); + pos = mixer_ch_get_pos(CH); + } + if (pos < B_INTRO + B_LOOP) { + printf("FAILED sustain-loop: pos %.1f still inside loop after note-off\n", pos); + r->amp_env.release_timecents = save_rel; + r->loop_mode = save_mode; + sf64_synth_close(synth); + return false; + } + sf64_synth_process(synth, rel); + mix(64); + if (mixer_ch_playing(CH)) { + printf("FAILED sustain-loop: not stopped after release\n"); + r->amp_env.release_timecents = save_rel; + r->loop_mode = save_mode; + sf64_synth_close(synth); + return false; + } + + // Continuous loop: note-off must NOT disable the loop (pos stays in range). + r->loop_mode = SF64_LOOP_CONTINUOUS; + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 100); + mixer_ch_set_freq(CH, 0); + mixer_ch_set_pos(CH, B_INTRO + 8); + mix(2048); + mixer_ch_set_freq(CH, SAMPLE_RATE); + sf64_synth_note_off(synth, 0, 60); + mix(B_LOOP + 256); + pos = mixer_ch_get_pos(CH); + if (pos < B_INTRO || pos >= B_INTRO + B_LOOP) { + printf("FAILED continuous-loop: pos %.1f left the loop after note-off\n", pos); + r->amp_env.release_timecents = save_rel; + r->loop_mode = save_mode; + sf64_synth_close(synth); + return false; + } + sf64_synth_process(synth, rel); + mix(64); + if (mixer_ch_playing(CH)) { + printf("FAILED continuous-loop: not stopped after release\n"); + r->amp_env.release_timecents = save_rel; + r->loop_mode = save_mode; + sf64_synth_close(synth); + return false; + } + + // No-loop region: note-off with release still works (no set_loop). + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_region_t *ra = &bank->regions[bank->presets[0].first_region]; + int16_t save_rel_a = ra->amp_env.release_timecents; + ra->amp_env.release_timecents = samples_to_timecents(300); + rel = env_samples(ra->amp_env.release_timecents); + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 100); + sf64_synth_note_off(synth, 0, 60); + if (!mixer_ch_playing(CH)) { + printf("FAILED oneshot-release: not playing during release\n"); + ra->amp_env.release_timecents = save_rel_a; + r->amp_env.release_timecents = save_rel; + r->loop_mode = save_mode; + sf64_synth_close(synth); + return false; + } + sf64_synth_process(synth, rel); + mix(64); + if (mixer_ch_playing(CH)) { + printf("FAILED oneshot-release: not stopped\n"); + ra->amp_env.release_timecents = save_rel_a; + r->amp_env.release_timecents = save_rel; + r->loop_mode = save_mode; + sf64_synth_close(synth); + return false; + } + + ra->amp_env.release_timecents = save_rel_a; + r->amp_env.release_timecents = save_rel; + r->loop_mode = save_mode; + sf64_synth_close(synth); + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// Step 6 — 16 MIDI channels, controllers +////////////////////////////////////////////////////////////////////////////// + +static void peaks_lr(int n, int *lpeak, int *rpeak) +{ + int l = 0, r = 0; + for (int i = 0; i < n; i++) { + int a = out[i * 2] < 0 ? -out[i * 2] : out[i * 2]; + int b = out[i * 2 + 1] < 0 ? -out[i * 2 + 1] : out[i * 2 + 1]; + if (a > l) l = a; + if (b > r) r = b; + } + *lpeak = l; + *rpeak = r; +} + +static float expect_freq_bend(const sf64_region_t *r, const sf64_sample_t *s, + int key, int pitch_bend, int range_cents) +{ + float bend = (pitch_bend - 8192) * (float)range_cents / 8192.0f; + float cents = (key - r->root_key) * (float)r->pitch_keytrack + + r->coarse_tune * 100.0f + r->fine_tune + bend; + return s->sample_rate * powf(2.0f, cents / 1200.0f); +} + +/** Independent MIDI channels, programs, controllers, pitch bend. */ +static bool test_synth_midi_channels(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 4, MIXER_PRIORITY_MUSIC); + + // Different programs on ch 0 / ch 1. + assert(sf64_synth_set_program(synth, 0, 0, 0)); // KeySplit → A at 48 + assert(sf64_synth_set_program(synth, 1, 0, 1)); // VelSplit → D at vel 100 + synth_silence(synth, 4); + if (!sf64_synth_note_on(synth, 0, 48, 100) || + !sf64_synth_note_on(synth, 1, 60, 100)) { + printf("FAILED midi: could not start notes on two channels\n"); + sf64_synth_close(synth); + return false; + } + sf64_region_t *ra = &bank->regions[bank->presets[0].first_region]; + sf64_region_t *rd = &bank->regions[bank->presets[1].first_region + 1]; + if (mixer_ch_playing_waveform(0) != &bank->waves[ra->sample_index]->wave || + mixer_ch_playing_waveform(1) != &bank->waves[rd->sample_index]->wave) { + printf("FAILED midi: wrong presets on channels\n"); + sf64_synth_close(synth); + return false; + } + + // Program change on ch 0: sounding note keeps sample A; new note uses VelSplit. + assert(sf64_synth_set_program(synth, 0, 0, 1)); + if (mixer_ch_playing_waveform(0) != &bank->waves[ra->sample_index]->wave) { + printf("FAILED midi: program change interrupted sounding note\n"); + sf64_synth_close(synth); + return false; + } + sf64_synth_note_on(synth, 0, 60, 100); + if (mixer_ch_playing_waveform(2) != &bank->waves[rd->sample_index]->wave) { + printf("FAILED midi: new note after program change wrong sample\n"); + sf64_synth_close(synth); + return false; + } + + // note-off on ch 1 must not release ch 0's key 60 (different MIDI channel). + sf64_synth_note_off(synth, 1, 60); + mix(64); + sf64_synth_process(synth, 64); + if (!mixer_ch_playing(0) || !mixer_ch_playing(2) || mixer_ch_playing(1)) { + printf("FAILED midi: note-off crossed MIDI channels\n"); + sf64_synth_close(synth); + return false; + } + + sf64_synth_close(synth); + return true; +} + +static bool test_synth_midi_controllers(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 2, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + assert(sf64_synth_set_program(synth, 1, 0, 0)); + + // Volume follows the SF2 (x/127)² curve, not a linear CC/127: half CC7 is + // a quarter of the peak on constant-amplitude sample C (key 72). + sf64_synth_set_volume(synth, 0, 127); + synth_silence(synth, 2); + sf64_synth_note_on(synth, 0, 72, 127); + mix(256); + int full = peak_n(256); + sf64_synth_set_volume(synth, 0, 64); + mix(256); + int quarter = peak_n(256); + if (full < 1024 || quarter * 3 > full || quarter * 5 < full) { + printf("FAILED midi vol: full %d vol64 %d (want ~4×)\n", full, quarter); + sf64_synth_close(synth); + return false; + } + + // Expression on ch 0 must not change ch 1. + synth_silence(synth, 2); + sf64_synth_set_volume(synth, 0, 127); + sf64_synth_set_expression(synth, 0, 127); + sf64_synth_set_expression(synth, 1, 127); + sf64_synth_note_on(synth, 0, 72, 127); + sf64_synth_note_on(synth, 1, 72, 127); + mix(128); + // Isolate ch0, then drop expression. + sf64_synth_note_off(synth, 1, 72); + mix(64); + sf64_synth_set_expression(synth, 0, 32); + mix(256); + int quiet = peak_n(256); + if (quiet * 3 > full) { + printf("FAILED midi expr: peak %d after expr 32 (full was %d)\n", + quiet, full); + sf64_synth_close(synth); + return false; + } + + // Pan hard left / hard right on sample C. + synth_silence(synth, 2); + sf64_synth_set_expression(synth, 0, 127); + sf64_synth_set_pan(synth, 0, 0); + sf64_synth_note_on(synth, 0, 72, 127); + mix(256); + int lp, rp; + peaks_lr(256, &lp, &rp); + if (lp < 1024 || rp * 4 > lp) { + printf("FAILED midi pan left: L=%d R=%d\n", lp, rp); + sf64_synth_close(synth); + return false; + } + sf64_synth_set_pan(synth, 0, 127); + mix(256); + peaks_lr(256, &lp, &rp); + if (rp < 1024 || lp * 4 > rp) { + printf("FAILED midi pan right: L=%d R=%d\n", lp, rp); + sf64_synth_close(synth); + return false; + } + + // Pitch bend min / center / max (±200 cents default). + sf64_region_t *rc = &bank->regions[bank->presets[0].first_region + 1]; + sf64_sample_t *sc = &bank->samples[rc->sample_index]; + synth_silence(synth, 2); + sf64_synth_set_pan(synth, 0, 64); + sf64_synth_note_on(synth, 0, 72, 100); + sf64_synth_set_pitch_bend(synth, 0, 8192); + if (!check_freq(synth, "bend center", + expect_freq_bend(rc, sc, 72, 8192, 200))) { + sf64_synth_close(synth); + return false; + } + sf64_synth_set_pitch_bend(synth, 0, 0); + if (!check_freq(synth, "bend min", + expect_freq_bend(rc, sc, 72, 0, 200))) { + sf64_synth_close(synth); + return false; + } + sf64_synth_set_pitch_bend(synth, 0, 16383); + if (!check_freq(synth, "bend max", + expect_freq_bend(rc, sc, 72, 16383, 200))) { + sf64_synth_close(synth); + return false; + } + + // Bend on ch 0 must not move ch 1's frequency. + synth_silence(synth, 2); + sf64_synth_set_pitch_bend(synth, 0, 8192); + sf64_synth_set_pitch_bend(synth, 1, 8192); + sf64_synth_note_on(synth, 0, 72, 100); + sf64_synth_note_on(synth, 1, 72, 100); + sf64_synth_set_pitch_bend(synth, 0, 0); + double p0 = mixer_ch_get_pos(1); + mix(512); + double got = (mixer_ch_get_pos(1) - p0) * SAMPLE_RATE / 512.0; + float want = expect_freq(rc, sc, 72); + float err = got - want; + if (err < 0) err = -err; + if (err > want * 0.02f + 1.0f) { + printf("FAILED midi bend isolation: ch1 freq %.1f want %.1f\n", + got, want); + sf64_synth_close(synth); + return false; + } + + sf64_synth_close(synth); + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// Sustain pedal / exclusive class +////////////////////////////////////////////////////////////////////////////// + +static int count_playing_n(int n) +{ + int c = 0; + for (int i = 0; i < n; i++) + if (mixer_ch_playing(i)) + c++; + return c; +} + +static bool test_synth_sustain_pedal(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 4, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_region_t *r = &bank->regions[bank->presets[0].first_region]; + int16_t save_rel = r->amp_env.release_timecents; + r->amp_env.release_timecents = samples_to_timecents(400); + int rel = env_samples(r->amp_env.release_timecents); + + // Note-off without pedal → immediate release. + synth_silence(synth, 4); + sf64_synth_set_sustain(synth, 0, 0); + sf64_synth_note_on(synth, 0, 60, 100); + sf64_synth_note_off(synth, 0, 60); + int dl = sf64_synth_process(synth, 0); + if (dl != rel || !mixer_ch_playing(0)) { + printf("FAILED pedal: note-off without pedal remaining %d\n", dl); + goto fail_pedal; + } + sf64_synth_process(synth, rel); + mix(64); + if (mixer_ch_playing(0)) { + printf("FAILED pedal: still playing after release\n"); + goto fail_pedal; + } + + // Note-off with pedal → keep sounding; pedal up → release. + synth_silence(synth, 4); + sf64_synth_set_sustain(synth, 0, 127); + sf64_synth_note_on(synth, 0, 60, 100); + sf64_synth_note_off(synth, 0, 60); + if (!mixer_ch_playing(0) || sf64_synth_process(synth, 0) >= 0) { + printf("FAILED pedal: note-off with pedal entered release\n"); + goto fail_pedal; + } + sf64_synth_set_sustain(synth, 0, 0); + dl = sf64_synth_process(synth, 0); + if (dl != rel || !mixer_ch_playing(0)) { + printf("FAILED pedal: pedal-up remaining %d want %d\n", dl, rel); + goto fail_pedal; + } + sf64_synth_process(synth, rel); + mix(64); + if (mixer_ch_playing(0)) { + printf("FAILED pedal: not stopped after pedal-up release\n"); + goto fail_pedal; + } + + // Multiple notes held by pedal; pedal up releases all. + synth_silence(synth, 4); + sf64_synth_set_sustain(synth, 0, 127); + sf64_synth_note_on(synth, 0, 48, 100); + sf64_synth_note_on(synth, 0, 60, 100); + sf64_synth_note_off(synth, 0, 48); + sf64_synth_note_off(synth, 0, 60); + if (count_playing_n(4) != 2) { + printf("FAILED pedal: expected 2 held notes, got %d\n", + count_playing_n(4)); + goto fail_pedal; + } + sf64_synth_set_sustain(synth, 0, 0); + sf64_synth_process(synth, rel); + mix(64); + if (count_playing_n(4) != 0) { + printf("FAILED pedal: multi hold not cleared\n"); + goto fail_pedal; + } + + // Stacked same key: peel under pedal, then pedal-up releases remaining. + synth_silence(synth, 4); + sf64_synth_set_sustain(synth, 0, 127); + sf64_synth_note_on(synth, 0, 60, 100); + sf64_synth_note_on(synth, 0, 60, 100); + sf64_synth_note_off(synth, 0, 60); // first identity held by pedal + if (count_playing_n(4) != 2) { + printf("FAILED pedal: stack note-off dropped a voice\n"); + goto fail_pedal; + } + sf64_synth_note_off(synth, 0, 60); // second identity also held + if (count_playing_n(4) != 2) { + printf("FAILED pedal: second stack note-off dropped a voice\n"); + goto fail_pedal; + } + sf64_synth_set_sustain(synth, 0, 0); + sf64_synth_process(synth, rel); + mix(64); + if (count_playing_n(4) != 0) { + printf("FAILED pedal: stacked identities not released\n"); + goto fail_pedal; + } + + // Sustain loop: pedal note-off must NOT leave the loop; pedal-up does. + assert(sf64_synth_set_program(synth, 0, 0, 2)); + sf64_region_t *rb = &bank->regions[bank->presets[2].first_region]; + int16_t save_rel_b = rb->amp_env.release_timecents; + uint8_t save_mode = rb->loop_mode; + rb->amp_env.release_timecents = samples_to_timecents(600); + int rel_b = env_samples(rb->amp_env.release_timecents); + synth_silence(synth, 4); + sf64_synth_set_sustain(synth, 0, 127); + sf64_synth_note_on(synth, 0, 60, 100); + mixer_ch_set_freq(0, 0); + mixer_ch_set_pos(0, B_INTRO + 8); + mix(2048); + mixer_ch_set_freq(0, SAMPLE_RATE); + sf64_synth_note_off(synth, 0, 60); + mix(B_LOOP + 256); + double pos = mixer_ch_get_pos(0); + if (pos < B_INTRO || pos >= B_INTRO + B_LOOP) { + printf("FAILED pedal: sustain loop left early (pos %.1f)\n", pos); + rb->amp_env.release_timecents = save_rel_b; + rb->loop_mode = save_mode; + goto fail_pedal; + } + sf64_synth_set_sustain(synth, 0, 0); + mix(B_LOOP); + pos = mixer_ch_get_pos(0); + if (pos < B_INTRO + B_LOOP) { + printf("FAILED pedal: sustain loop still on after pedal-up (pos %.1f)\n", + pos); + rb->amp_env.release_timecents = save_rel_b; + rb->loop_mode = save_mode; + goto fail_pedal; + } + sf64_synth_process(synth, rel_b); + mix(64); + + // Continuous loop: stays in loop through real release after pedal-up. + rb->loop_mode = SF64_LOOP_CONTINUOUS; + synth_silence(synth, 4); + sf64_synth_set_sustain(synth, 0, 127); + sf64_synth_note_on(synth, 0, 60, 100); + mixer_ch_set_freq(0, 0); + mixer_ch_set_pos(0, B_INTRO + 8); + mix(2048); + mixer_ch_set_freq(0, SAMPLE_RATE); + sf64_synth_note_off(synth, 0, 60); + sf64_synth_set_sustain(synth, 0, 0); + mix(B_LOOP + 256); + pos = mixer_ch_get_pos(0); + if (pos < B_INTRO || pos >= B_INTRO + B_LOOP) { + printf("FAILED pedal: continuous loop left during release (pos %.1f)\n", + pos); + rb->amp_env.release_timecents = save_rel_b; + rb->loop_mode = save_mode; + goto fail_pedal; + } + sf64_synth_process(synth, rel_b); + mix(64); + + rb->amp_env.release_timecents = save_rel_b; + rb->loop_mode = save_mode; + r->amp_env.release_timecents = save_rel; + sf64_synth_close(synth); + return true; + +fail_pedal: + r->amp_env.release_timecents = save_rel; + sf64_synth_close(synth); + return false; +} + +static bool test_synth_exclusive(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 6, MIXER_PRIORITY_MUSIC); + sf64_region_t *r0 = &bank->regions[bank->presets[0].first_region]; + sf64_region_t *r1 = &bank->regions[bank->presets[1].first_region]; + uint8_t g0a = r0[0].exclusive_group, g0c = r0[1].exclusive_group; + uint8_t g1a = r1[0].exclusive_group, g1d = r1[1].exclusive_group; + + // Same group + same preset → choke. + r0[0].exclusive_group = 1; + r0[1].exclusive_group = 1; + assert(sf64_synth_set_program(synth, 0, 0, 0)); + synth_silence(synth, 6); + sf64_synth_note_on(synth, 0, 48, 100); // region A + sf64_synth_note_on(synth, 0, 72, 100); // region C chokes A + mix(64); + waveform_t *wave_c = &bank->waves[r0[1].sample_index]->wave; + if (count_playing_n(6) != 1) { + printf("FAILED excl: bitmap after choke has %d voices\n", + count_playing_n(6)); + goto fail_excl; + } + if (!wave_playing(6, wave_c)) { + printf("FAILED excl: same group did not leave region C\n"); + goto fail_excl; + } + + // Different groups → no choke. + r0[0].exclusive_group = 1; + r0[1].exclusive_group = 2; + synth_silence(synth, 6); + sf64_synth_note_on(synth, 0, 48, 100); + sf64_synth_note_on(synth, 0, 72, 100); + if (!mixer_ch_playing(0) || !mixer_ch_playing(1)) { + printf("FAILED excl: different groups choked\n"); + goto fail_excl; + } + + // Same group, different presets → no choke. + r0[0].exclusive_group = 1; + r1[1].exclusive_group = 1; // VelSplit high-vel → D + assert(sf64_synth_set_program(synth, 0, 0, 0)); + assert(sf64_synth_set_program(synth, 1, 0, 1)); + synth_silence(synth, 6); + sf64_synth_note_on(synth, 0, 48, 100); + sf64_synth_note_on(synth, 1, 60, 100); + if (!mixer_ch_playing(0) || !mixer_ch_playing(1)) { + printf("FAILED excl: different presets choked\n"); + goto fail_excl; + } + + // Multiple victims in the same group. + r0[0].exclusive_group = 1; + r0[1].exclusive_group = 1; + assert(sf64_synth_set_program(synth, 0, 0, 0)); + synth_silence(synth, 6); + sf64_synth_note_on(synth, 0, 48, 100); + sf64_synth_note_on(synth, 0, 60, 100); // also A (same region), both group 1 + // Second note-on of key 60 also matches region A only — both on group 1. + // A third note at 72 chokes both. + sf64_synth_note_on(synth, 0, 72, 100); + mix(64); + if (count_playing_n(6) != 1) { + printf("FAILED excl: multi-victim left %d voices\n", count_playing_n(6)); + goto fail_excl; + } + if (!wave_playing(6, wave_c)) { + printf("FAILED excl: multi-victim survivor is not region C\n"); + goto fail_excl; + } + + // Same note-on layers sharing a group must not choke each other. + sf64_region_t *rl = &bank->regions[bank->presets[3].first_region]; + uint8_t gl0 = rl[0].exclusive_group, gl1 = rl[1].exclusive_group; + rl[0].exclusive_group = 5; + rl[1].exclusive_group = 5; + assert(sf64_synth_set_program(synth, 0, 0, 3)); + synth_silence(synth, 6); + uint32_t id = sf64_synth_note_on(synth, 0, 60, 100); + if (!id || count_playing_n(6) != 2) { + printf("FAILED excl: layered same-group note-on playing=%d id=%lu\n", + count_playing_n(6), (unsigned long)id); + rl[0].exclusive_group = gl0; + rl[1].exclusive_group = gl1; + goto fail_excl; + } + rl[0].exclusive_group = gl0; + rl[1].exclusive_group = gl1; + + // Rejected layered note-on must not choke victims (atomic with exclusive). + // Pool of 1 with a group-1 mono voice: Layer2 needs 2 channels so the plan + // fails, and the exclusive victim must still be sounding. + sf64_synth_set_channels(synth, 0, 1, MIXER_PRIORITY_MUSIC); + r0[0].exclusive_group = 1; + r0[1].exclusive_group = 0; + assert(sf64_synth_set_program(synth, 0, 0, 0)); + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 48, 100); + mix(64); + waveform_t *w0 = mixer_ch_playing_waveform(0); + assert(sf64_synth_set_program(synth, 0, 0, 3)); + rl[0].exclusive_group = 1; + rl[1].exclusive_group = 0; + if (sf64_synth_note_on(synth, 0, 61, 100) != 0) { + printf("FAILED excl: layered note should have been rejected\n"); + rl[0].exclusive_group = gl0; + rl[1].exclusive_group = gl1; + goto fail_excl; + } + if (!mixer_ch_playing(0) || mixer_ch_playing_waveform(0) != w0) { + printf("FAILED excl: rejected note-on choked a victim\n"); + rl[0].exclusive_group = gl0; + rl[1].exclusive_group = gl1; + goto fail_excl; + } + rl[0].exclusive_group = gl0; + rl[1].exclusive_group = gl1; + + r0[0].exclusive_group = g0a; + r0[1].exclusive_group = g0c; + r1[0].exclusive_group = g1a; + r1[1].exclusive_group = g1d; + sf64_synth_close(synth); + return true; + +fail_excl: + r0[0].exclusive_group = g0a; + r0[1].exclusive_group = g0c; + r1[0].exclusive_group = g1a; + r1[1].exclusive_group = g1d; + sf64_synth_close(synth); + return false; +} + +////////////////////////////////////////////////////////////////////////////// +// Envelope delay / hold / key scaling, multi-phase process, reclaim, MIDI +////////////////////////////////////////////////////////////////////////////// + +static bool test_synth_delay_hold(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 1, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_region_t *r = &bank->regions[bank->presets[0].first_region]; + sf64_envelope_t save = r->amp_env; + + // Delay: silent until deadline, then attack remaining. + r->amp_env.delay_timecents = samples_to_timecents(256); + r->amp_env.attack_timecents = samples_to_timecents(128); + r->amp_env.hold_timecents = -12000; + r->amp_env.decay_timecents = -12000; + r->amp_env.sustain_gain = 1.0f; + r->amp_env.keynum_to_hold = 0; + r->amp_env.keynum_to_decay = 0; + int delay = env_samples(r->amp_env.delay_timecents); + int atk = env_samples(r->amp_env.attack_timecents); + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 100); + int dl = sf64_synth_process(synth, 0); + if (dl != delay) { + printf("FAILED delay: remaining %d want %d\n", dl, delay); + goto fail_dh; + } + mix(64); + if (peak_n(64) > 64) { + printf("FAILED delay: audible during delay (peak %d)\n", peak_n(64)); + goto fail_dh; + } + dl = sf64_synth_process(synth, delay); + if (dl != atk) { + printf("FAILED delay→attack remaining %d want %d\n", dl, atk); + goto fail_dh; + } + + // Hold: after attack, peak holds before decay to silence. + r->amp_env.delay_timecents = -12000; + r->amp_env.attack_timecents = samples_to_timecents(64); + r->amp_env.hold_timecents = samples_to_timecents(256); + r->amp_env.decay_timecents = samples_to_timecents(64); + r->amp_env.sustain_gain = 0.0f; + atk = env_samples(r->amp_env.attack_timecents); + int hold = env_samples(r->amp_env.hold_timecents); + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 127); + sf64_synth_process(synth, atk); // → hold + dl = sf64_synth_process(synth, 0); + if (dl != hold) { + printf("FAILED hold: remaining %d want %d\n", dl, hold); + goto fail_dh; + } + mix(64); + int held = peak_n(64); + if (held < 1024) { + printf("FAILED hold: silent at peak (peak %d)\n", held); + goto fail_dh; + } + sf64_synth_process(synth, hold); // → decay + sf64_synth_process(synth, env_samples(r->amp_env.decay_timecents)); + mix(64); + if (peak_n(64) > held / 8) { + printf("FAILED hold: still loud after decay to silence\n"); + goto fail_dh; + } + + // Keynum scaling shortens hold at keys above 60. + r->amp_env.attack_timecents = -12000; + r->amp_env.hold_timecents = samples_to_timecents(400); + r->amp_env.keynum_to_hold = 100; + r->amp_env.decay_timecents = -12000; + r->amp_env.sustain_gain = 1.0f; + int hold60 = env_scaled_samples_tc(r->amp_env.hold_timecents, + r->amp_env.keynum_to_hold, 60); + int hold72 = env_scaled_samples_tc(r->amp_env.hold_timecents, + r->amp_env.keynum_to_hold, 72); + if (hold72 >= hold60) { + printf("FAILED keyscale: hold72=%d not shorter than hold60=%d\n", + hold72, hold60); + goto fail_dh; + } + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 100); + dl = sf64_synth_process(synth, 0); + if (dl != hold60) { + printf("FAILED keyscale: key60 hold %d want %d\n", dl, hold60); + goto fail_dh; + } + // Lower key → longer hold (scale*(60-key) adds timecents). + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 48, 100); + int hold48 = env_scaled_samples_tc(r->amp_env.hold_timecents, + r->amp_env.keynum_to_hold, 48); + dl = sf64_synth_process(synth, 0); + if (dl != hold48 || hold48 <= hold60) { + printf("FAILED keyscale: key48 hold %d want %d (key60=%d)\n", + dl, hold48, hold60); + goto fail_dh; + } + + r->amp_env = save; + sf64_synth_close(synth); + return true; + +fail_dh: + r->amp_env = save; + sf64_synth_close(synth); + return false; +} + +// modEnvToPitch: instant attack → octave-up peak, then linear decay to base. +static bool test_synth_modenv(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 1, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_region_t *r = &bank->regions[bank->presets[0].first_region]; + sf64_sample_t *s = &bank->samples[r->sample_index]; + sf64_envelope_t save_amp = r->amp_env; + sf64_envelope_t save_mod = r->mod_env; + int16_t save_pitch = r->mod_env_to_pitch; + + r->amp_env.delay_timecents = -12000; + r->amp_env.attack_timecents = -12000; + r->amp_env.hold_timecents = -12000; + r->amp_env.decay_timecents = -12000; + r->amp_env.sustain_gain = 1.0f; + r->amp_env.release_timecents = samples_to_timecents(64); + r->amp_env.keynum_to_hold = 0; + r->amp_env.keynum_to_decay = 0; + + r->mod_env_to_pitch = 1200; + r->mod_env.delay_timecents = -12000; + r->mod_env.attack_timecents = -12000; + r->mod_env.hold_timecents = samples_to_timecents(600); + r->mod_env.decay_timecents = samples_to_timecents(1024); + r->mod_env.sustain_gain = 0.0f; + r->mod_env.release_timecents = -12000; + r->mod_env.keynum_to_hold = 0; + r->mod_env.keynum_to_decay = 0; + + float base = expect_freq(r, s, 60); + float peak = base * 2.0f; + int hold = env_samples(r->mod_env.hold_timecents); + int decay = env_samples(r->mod_env.decay_timecents); + + synth_silence(synth, 1); + if (!sf64_synth_note_on(synth, 0, 60, 100)) { + printf("FAILED modenv: note_on\n"); + goto fail_me; + } + int dl = sf64_synth_process(synth, 0); + if (dl != hold) { + printf("FAILED modenv: deadline %d want hold %d\n", dl, hold); + goto fail_me; + } + // Measure during hold so the playhead is not averaging a running Hz ramp. + if (!check_freq(synth, "modenv peak", peak)) + goto fail_me; + // Keep synth time in sync with the samples just mixed by check_freq. + sf64_synth_process(synth, 512); + + int left = hold - 512; + if (left > 0) { + mix(left); + sf64_synth_process(synth, left); + } + dl = sf64_synth_process(synth, 0); + if (dl != decay) { + printf("FAILED modenv: after hold deadline %d want decay %d\n", dl, decay); + goto fail_me; + } + mix(decay); + sf64_synth_process(synth, decay); + if (!check_freq(synth, "modenv after decay", base)) + goto fail_me; + + // Attack 0→peak: first half slower than second (SF2 convex in cents). + r->mod_env.attack_timecents = samples_to_timecents(2048); + r->mod_env.hold_timecents = -12000; + r->mod_env.decay_timecents = -12000; + r->mod_env.sustain_gain = 1.0f; + int attack = env_samples(r->mod_env.attack_timecents); + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 100); + dl = sf64_synth_process(synth, 0); + if (dl != attack) { + printf("FAILED modenv attack: deadline %d want %d\n", dl, attack); + goto fail_me; + } + double p0 = mixer_ch_get_pos(CH); + mix(attack / 2); + double p1 = mixer_ch_get_pos(CH); + mix(attack / 2); + double p2 = mixer_ch_get_pos(CH); + double a0 = p1 - p0, a1 = p2 - p1; + if (a0 >= a1) { + printf("FAILED modenv attack: first-half advance %.2f >= second %.2f\n", + a0, a1); + goto fail_me; + } + + // A new note must pin the base pitch before arming the mod ramp: the ramp + // starts from the channel frequency, which is still the previous note's + // peak target (and right after mixer_ch_play, the waveform's own rate). + r->mod_env.attack_timecents = samples_to_timecents(262144); + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 100); + if (!check_freq(synth, "modenv attack start", base)) + goto fail_me; + + r->amp_env = save_amp; + r->mod_env = save_mod; + r->mod_env_to_pitch = save_pitch; + sf64_synth_close(synth); + return true; + +fail_me: + r->amp_env = save_amp; + r->mod_env = save_mod; + r->mod_env_to_pitch = save_pitch; + sf64_synth_close(synth); + return false; +} + +static bool test_synth_multiphase(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 1, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_region_t *r = &bank->regions[bank->presets[0].first_region]; + sf64_envelope_t save = r->amp_env; + + r->amp_env.delay_timecents = samples_to_timecents(100); + r->amp_env.attack_timecents = samples_to_timecents(100); + r->amp_env.hold_timecents = samples_to_timecents(100); + r->amp_env.decay_timecents = -12000; + r->amp_env.sustain_gain = 1.0f; + r->amp_env.keynum_to_hold = 0; + r->amp_env.keynum_to_decay = 0; + int delay = env_samples(r->amp_env.delay_timecents); + int atk = env_samples(r->amp_env.attack_timecents); + int hold = env_samples(r->amp_env.hold_timecents); + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 100); + // One process spanning delay+attack+hold must land in sustain. + sf64_synth_process(synth, delay + atk + hold); + if (sf64_synth_process(synth, 0) >= 0) { + printf("FAILED multiphase: still pending after full envelope span\n"); + r->amp_env = save; + sf64_synth_close(synth); + return false; + } + mix(64); + if (peak_n(64) < 1024) { + printf("FAILED multiphase: silent in sustain after catch-up\n"); + r->amp_env = save; + sf64_synth_close(synth); + return false; + } + + r->amp_env = save; + sf64_synth_close(synth); + return true; +} + +static bool test_synth_oneshot_reclaim(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 1, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_region_t *r = &bank->regions[bank->presets[0].first_region]; + sf64_envelope_t save = r->amp_env; + r->amp_env.delay_timecents = -12000; + r->amp_env.attack_timecents = -12000; + r->amp_env.hold_timecents = -12000; + r->amp_env.decay_timecents = -12000; + r->amp_env.release_timecents = -12000; + + synth_silence(synth, 1); + if (!sf64_synth_note_on(synth, 0, 60, 100)) { + printf("FAILED oneshot reclaim: first note_on failed\n"); + goto fail_os; + } + // Play past the oneshot length at 1:1 (root key 60). + mix(A_LEN + 512); + if (mixer_ch_playing(0)) { + printf("FAILED oneshot reclaim: mixer still playing after sample end\n"); + goto fail_os; + } + // Mixer channel is free but the voice is not yet reclaimed: note_on steals + // it (same path as an occupied slot with phase != OFF). Same key so the + // oneshot still finishes in A_LEN samples at 1:1. + if (!sf64_synth_note_on(synth, 0, 60, 100) || !mixer_ch_playing(0)) { + printf("FAILED oneshot reclaim: note_on did not reuse finished voice\n"); + goto fail_os; + } + // process() also reclaims finished voices when no note_on intervenes. + mix(A_LEN + 512); + if (mixer_ch_playing(0)) { + printf("FAILED oneshot reclaim: second oneshot still playing\n"); + goto fail_os; + } + sf64_synth_process(synth, 0); + if (sf64_synth_process(synth, 0) >= 0) { + printf("FAILED oneshot reclaim: process left a pending deadline\n"); + goto fail_os; + } + + r->amp_env = save; + sf64_synth_close(synth); + return true; + +fail_os: + r->amp_env = save; + sf64_synth_close(synth); + return false; +} + +static bool test_synth_drum_channel(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 2, MIXER_PRIORITY_MUSIC); + sf64_region_t *rd = &bank->regions[bank->presets[5].first_region]; + sf64_region_t *ra = &bank->regions[bank->presets[0].first_region]; + + // Channel 9 defaults to bank 128 / DrumKit without Bank Select. + synth_silence(synth, 2); + if (!sf64_synth_note_on(synth, 9, 60, 100)) { + printf("FAILED drum: ch9 note_on with default bank\n"); + sf64_synth_close(synth); + return false; + } + if (mixer_ch_playing_waveform(0) != &bank->waves[rd->sample_index]->wave) { + printf("FAILED drum: ch9 did not play DrumKit sample\n"); + sf64_synth_close(synth); + return false; + } + + // Melodic ch0 still defaults to bank 0 / KeySplit. + if (!sf64_synth_note_on(synth, 0, 48, 100)) { + printf("FAILED drum: ch0 note_on failed\n"); + sf64_synth_close(synth); + return false; + } + if (mixer_ch_playing_waveform(1) != &bank->waves[ra->sample_index]->wave) { + printf("FAILED drum: ch0 did not play KeySplit\n"); + sf64_synth_close(synth); + return false; + } + + sf64_synth_close(synth); + return true; +} + +static bool test_synth_midi_reset(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 2, MIXER_PRIORITY_MUSIC); + midi_target_t *mt = sf64_synth_midi_target(synth); + sf64_region_t *r = &bank->regions[bank->presets[0].first_region]; + int16_t save_rel = r->amp_env.release_timecents; + r->amp_env.release_timecents = samples_to_timecents(200); + int rel = env_samples(r->amp_env.release_timecents); + + assert(sf64_synth_set_program(synth, 0, 0, 3)); // Layer2 + sf64_synth_set_volume(synth, 0, 16); + sf64_synth_set_sustain(synth, 0, 127); + sf64_synth_set_pan(synth, 0, 0); + synth_silence(synth, 2); + sf64_synth_note_on(synth, 0, 60, 100); + if (count_playing_n(2) != 2) { + printf("FAILED midi reset: Layer2 did not start two voices\n"); + goto fail_mr; + } + + mt->ops->reset(mt, 0); + mix(64); + if (count_playing_n(2) != 0) { + printf("FAILED midi reset: voices still playing\n"); + goto fail_mr; + } + + // Controllers / program restored: KeySplit, full volume, no sustain. + synth_silence(synth, 2); + if (!sf64_synth_note_on(synth, 0, 48, 127) || count_playing_n(2) != 1) { + printf("FAILED midi reset: expected single KeySplit voice\n"); + goto fail_mr; + } + mix(128); + int full = peak_n(128); + if (full < 1024) { + printf("FAILED midi reset: volume not restored (peak %d)\n", full); + goto fail_mr; + } + sf64_synth_note_off(synth, 0, 48); + int dl = sf64_synth_process(synth, 0); + if (dl != rel) { + printf("FAILED midi reset: sustain still held (remaining %d)\n", dl); + goto fail_mr; + } + + // Drum channel bank restored to 128. + synth_silence(synth, 2); + sf64_synth_set_program(synth, 9, 0, 0); // force melodic + mt->ops->reset(mt, 0); + sf64_region_t *rd = &bank->regions[bank->presets[5].first_region]; + if (!sf64_synth_note_on(synth, 9, 60, 100) || + mixer_ch_playing_waveform(0) != &bank->waves[rd->sample_index]->wave) { + printf("FAILED midi reset: ch9 bank not restored to 128\n"); + goto fail_mr; + } + + r->amp_env.release_timecents = save_rel; + sf64_synth_close(synth); + return true; + +fail_mr: + r->amp_env.release_timecents = save_rel; + sf64_synth_close(synth); + return false; +} + +/** CC120 / CC121 / CC123 Channel Mode Messages. */ +static bool test_synth_channel_mode(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 4, MIXER_PRIORITY_MUSIC); + midi_target_t *mt = sf64_synth_midi_target(synth); + sf64_region_t *r = &bank->regions[bank->presets[0].first_region]; + sf64_region_t *rc = &bank->regions[bank->presets[0].first_region + 1]; + sf64_sample_t *sc = &bank->samples[rc->sample_index]; + sf64_envelope_t save0 = r->amp_env; + sf64_envelope_t save_c = rc->amp_env; + + r->amp_env.attack_timecents = samples_to_timecents(800); + r->amp_env.release_timecents = samples_to_timecents(800); + rc->amp_env.release_timecents = samples_to_timecents(800); + int rel = env_samples(r->amp_env.release_timecents); + + assert(sf64_synth_set_program(synth, 0, 0, 0)); + assert(sf64_synth_set_program(synth, 1, 0, 0)); + + // CC120 during attack: hard cut, no release tail. + synth_silence(synth, 4); + sf64_synth_note_on(synth, 0, 48, 100); + assert(mixer_ch_playing(0)); + mt->ops->control_change(mt, 0, 120, 0, 0); + mix(64); + if (mixer_ch_playing(0)) { + printf("FAILED CC120 attack: voice still playing\n"); + goto fail_cm; + } + + // CC120 during release: also hard cut. + synth_silence(synth, 4); + sf64_synth_note_on(synth, 0, 48, 100); + sf64_synth_note_off(synth, 0, 48); + assert(sf64_synth_process(synth, 0) == rel); + assert(mixer_ch_playing(0)); + mt->ops->control_change(mt, 0, 120, 0, 0); + mix(64); + if (mixer_ch_playing(0)) { + printf("FAILED CC120 release: voice still playing\n"); + goto fail_cm; + } + + // CC123 sustain OFF → release (still playing mid-release). + synth_silence(synth, 4); + sf64_synth_note_on(synth, 0, 48, 100); + mt->ops->control_change(mt, 0, 123, 0, 0); + int dl = sf64_synth_process(synth, 0); + if (dl != rel || !mixer_ch_playing(0)) { + printf("FAILED CC123: expected release remaining %d playing=%d\n", + dl, mixer_ch_playing(0)); + goto fail_cm; + } + + // CC123 sustain ON → held by pedal through attack into sustain. + synth_silence(synth, 4); + sf64_synth_set_sustain(synth, 0, 127); + sf64_synth_note_on(synth, 0, 48, 100); + mt->ops->control_change(mt, 0, 123, 0, 0); + int atk = env_samples(r->amp_env.attack_timecents); + sf64_synth_process(synth, atk); + mix(64); + if (!mixer_ch_playing(0) || sf64_synth_process(synth, 0) >= 0) { + printf("FAILED CC123 pedal: expected held at sustain (playing=%d dl=%d)\n", + mixer_ch_playing(0), sf64_synth_process(synth, 0)); + goto fail_cm; + } + sf64_synth_set_sustain(synth, 0, 0); + if (sf64_synth_process(synth, 0) != rel) { + printf("FAILED CC123 pedal: pedal-up did not release\n"); + goto fail_cm; + } + + // CC121: pitch bend restored; volume preserved; pedal released. + synth_silence(synth, 4); + sf64_synth_set_volume(synth, 0, 32); + sf64_synth_set_pan(synth, 0, 0); + sf64_synth_set_expression(synth, 0, 32); + sf64_synth_set_sustain(synth, 0, 127); + sf64_synth_set_pitch_bend(synth, 0, 0); + sf64_synth_note_on(synth, 0, 72, 100); + sf64_synth_note_off(synth, 0, 72); // held by pedal + assert(mixer_ch_playing(0)); + if (!check_freq(synth, "CC121 pre-bend", + expect_freq_bend(rc, sc, 72, 0, 200))) + goto fail_cm; + mt->ops->control_change(mt, 0, 121, 0, 0); + if (!check_freq(synth, "CC121 bend center", + expect_freq_bend(rc, sc, 72, 8192, 200))) + goto fail_cm; + // Pedal cleared → release; volume still 32 (quiet vs full). + dl = sf64_synth_process(synth, 0); + if (dl != rel) { + printf("FAILED CC121: pedal not cleared (dl=%d)\n", dl); + goto fail_cm; + } + sf64_synth_process(synth, rel); + mix(64); + synth_silence(synth, 4); + sf64_synth_note_on(synth, 0, 72, 127); + mix(256); + int quiet = peak_n(256); + sf64_synth_set_volume(synth, 0, 127); + mix(256); + int loud = peak_n(256); + if (quiet * 2 > loud) { + printf("FAILED CC121: volume was reset (quiet=%d loud=%d)\n", + quiet, loud); + goto fail_cm; + } + + // CC121 on ch0 does not affect ch1 bend. + synth_silence(synth, 4); + sf64_synth_set_pitch_bend(synth, 0, 0); + sf64_synth_set_pitch_bend(synth, 1, 0); + sf64_synth_note_on(synth, 0, 72, 100); + sf64_synth_note_on(synth, 1, 72, 100); + mt->ops->control_change(mt, 0, 121, 0, 0); + double p0 = mixer_ch_get_pos(1); + mix(512); + double got = (mixer_ch_get_pos(1) - p0) * SAMPLE_RATE / 512.0; + float want = expect_freq_bend(rc, sc, 72, 0, 200); + float err = got - want; + if (err < 0) err = -err; + if (err > want * 0.02f + 1.0f) { + printf("FAILED CC121 isolation: ch1 freq %.1f want %.1f\n", got, want); + goto fail_cm; + } + + // Layered note identity: CC120 / CC123 affect both layers. + synth_silence(synth, 4); + assert(sf64_synth_set_program(synth, 0, 0, 3)); // Layer2 + sf64_region_t *rl = &bank->regions[bank->presets[3].first_region]; + sf64_envelope_t save_l0 = rl[0].amp_env; + sf64_envelope_t save_l1 = rl[1].amp_env; + rl[0].amp_env.release_timecents = samples_to_timecents(800); + rl[1].amp_env.release_timecents = samples_to_timecents(800); + sf64_synth_note_on(synth, 0, 60, 100); + if (count_playing_n(4) != 2) { + printf("FAILED channel mode layer: setup playing=%d\n", + count_playing_n(4)); + rl[0].amp_env = save_l0; + rl[1].amp_env = save_l1; + goto fail_cm; + } + mt->ops->control_change(mt, 0, 123, 0, 0); + if (sf64_synth_process(synth, 0) != rel || count_playing_n(4) != 2) { + printf("FAILED CC123 layer: playing=%d\n", count_playing_n(4)); + rl[0].amp_env = save_l0; + rl[1].amp_env = save_l1; + goto fail_cm; + } + mt->ops->control_change(mt, 0, 120, 0, 0); + mix(64); + if (count_playing_n(4) != 0) { + printf("FAILED CC120 layer: playing=%d\n", count_playing_n(4)); + rl[0].amp_env = save_l0; + rl[1].amp_env = save_l1; + goto fail_cm; + } + rl[0].amp_env = save_l0; + rl[1].amp_env = save_l1; + + r->amp_env = save0; + rc->amp_env = save_c; + sf64_synth_close(synth); + return true; + +fail_cm: + r->amp_env = save0; + rc->amp_env = save_c; + sf64_synth_close(synth); + return false; +} + +/** GM1 mode: ch10 ignores Program Change / Bank Select; melodic PC uses bank 0. */ +static bool test_synth_gm_mode(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 2, MIXER_PRIORITY_MUSIC); + midi_target_t *mt = sf64_synth_midi_target(synth); + sf64_region_t *rd = &bank->regions[bank->presets[5].first_region]; + sf64_region_t *ra = &bank->regions[bank->presets[0].first_region]; + + if (sf64_synth_get_mode(synth) != SF64_MODE_NATIVE) { + printf("FAILED gm mode: create is not NATIVE\n"); + sf64_synth_close(synth); + return false; + } + + // Defaults after create/reset: ch0 program 0, ch9 percussion. + synth_silence(synth, 2); + sf64_synth_note_on(synth, 0, 48, 100); + sf64_synth_note_on(synth, 9, 60, 100); + if (mixer_ch_playing_waveform(0) != &bank->waves[ra->sample_index]->wave || + mixer_ch_playing_waveform(1) != &bank->waves[rd->sample_index]->wave) { + printf("FAILED gm mode: default programs wrong\n"); + sf64_synth_close(synth); + return false; + } + + sf64_synth_set_mode(synth, SF64_MODE_GM1); + if (sf64_synth_get_mode(synth) != SF64_MODE_GM1) { + printf("FAILED gm mode: set_mode\n"); + sf64_synth_close(synth); + return false; + } + + // Program Change on ch10 ignored; bank select must not make it melodic. + synth_silence(synth, 2); + mt->ops->control_change(mt, 9, 0, 0, 0); + mt->ops->program_change(mt, 9, 0, 0); + sf64_synth_note_on(synth, 9, 60, 100); + if (mixer_ch_playing_waveform(0) != &bank->waves[rd->sample_index]->wave) { + printf("FAILED gm mode: ch10 became melodic\n"); + sf64_synth_close(synth); + return false; + } + + // Melodic Program Change still works (bank 0 regardless of CC0). + synth_silence(synth, 2); + mt->ops->control_change(mt, 0, 0, 5, 0); + mt->ops->program_change(mt, 0, 1, 0); // VelSplit + sf64_synth_note_on(synth, 0, 60, 100); + sf64_region_t *rvel = &bank->regions[bank->presets[1].first_region + 1]; + if (mixer_ch_playing_waveform(0) != &bank->waves[rvel->sample_index]->wave) { + printf("FAILED gm mode: melodic PC ignored bank incorrectly\n"); + sf64_synth_close(synth); + return false; + } + + // system_reset → GM1 + hard silence + default programs. + synth_silence(synth, 2); + sf64_synth_set_mode(synth, SF64_MODE_NATIVE); + assert(sf64_synth_set_program(synth, 9, 0, 0)); // force melodic + sf64_synth_note_on(synth, 9, 48, 100); + assert(mixer_ch_playing(0)); + mt->ops->system_reset(mt, MIDI_SYSTEM_GM1, 0); + mix(64); + if (sf64_synth_get_mode(synth) != SF64_MODE_GM1 || mixer_ch_playing(0)) { + printf("FAILED gm mode: system_reset mode/silence\n"); + sf64_synth_close(synth); + return false; + } + sf64_synth_note_on(synth, 9, 60, 100); + if (mixer_ch_playing_waveform(0) != &bank->waves[rd->sample_index]->wave) { + printf("FAILED gm mode: system_reset did not restore drums\n"); + sf64_synth_close(synth); + return false; + } + + sf64_synth_close(synth); + return true; +} + +/** RPN 0,0 pitch bend sensitivity; CC121 restores default range. */ +static bool test_synth_rpn(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 1, MIXER_PRIORITY_MUSIC); + midi_target_t *mt = sf64_synth_midi_target(synth); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_region_t *rc = &bank->regions[bank->presets[0].first_region + 1]; + sf64_sample_t *sc = &bank->samples[rc->sample_index]; + + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 72, 100); + mt->ops->control_change(mt, 0, 101, 0, 0); // RPN MSB + mt->ops->control_change(mt, 0, 100, 0, 0); // RPN LSB + mt->ops->control_change(mt, 0, 6, 12, 0); // 12 semitones + sf64_synth_set_pitch_bend(synth, 0, 16383); + if (!check_freq(synth, "rpn +12", + expect_freq_bend(rc, sc, 72, 16383, 1200))) { + sf64_synth_close(synth); + return false; + } + + mt->ops->control_change(mt, 0, 121, 0, 0); + if (!check_freq(synth, "rpn CC121 range", + expect_freq_bend(rc, sc, 72, 8192, 200))) { + sf64_synth_close(synth); + return false; + } + sf64_synth_set_pitch_bend(synth, 0, 16383); + if (!check_freq(synth, "rpn after CC121", + expect_freq_bend(rc, sc, 72, 16383, 200))) { + sf64_synth_close(synth); + return false; + } + + sf64_synth_close(synth); + return true; +} + +static bool test_synth_attenuation(sf64_bank_t *bank) +{ + // Converter bakes 200 cB → linear gain 0.1 (not ~20 from the old 10× TSF factor). + sf64_region_t *rd = &bank->regions[bank->presets[1].first_region + 1]; + if (rd->gain < 0.09f || rd->gain > 0.11f) { + printf("FAILED attenuation: region D gain=%f want ~0.1\n", + rd->gain); + return false; + } + + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 1, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 1)); + float save = rd->gain; + + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 127); + mix(256); + int quiet = peak_n(256); + rd->gain = 1.0f; + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 60, 127); + mix(256); + int loud = peak_n(256); + rd->gain = save; + + // 200 cB = 20 dB → amplitude ≈ 10× (band for mixer / hermite). + if (loud < 1024 || quiet * 5 > loud || quiet * 20 < loud) { + printf("FAILED attenuation: quiet %d loud %d (want ~10×)\n", + quiet, loud); + sf64_synth_close(synth); + return false; + } + + sf64_synth_close(synth); + return true; +} + +/** Step 5: SF2 velocity/volume/expression/sustain gain model (no linear /127). */ +static bool test_synth_gain_model(sf64_bank_t *bank) +{ + sf64_synth_t *synth = sf64_synth_create(bank); + sf64_synth_set_channels(synth, 0, 1, MIXER_PRIORITY_MUSIC); + assert(sf64_synth_set_program(synth, 0, 0, 0)); + sf64_region_t *rc = &bank->regions[bank->presets[0].first_region + 1]; + sf64_envelope_t save = rc->amp_env; + float save_gain = rc->gain; + + // Constant-amplitude sample C (key 72), zero attack, full sustain. + rc->amp_env.delay_timecents = -12000; + rc->amp_env.attack_timecents = -12000; + rc->amp_env.hold_timecents = -12000; + rc->amp_env.decay_timecents = -12000; + rc->amp_env.sustain_gain = 1.0f; + rc->gain = 1.0f; + sf64_synth_set_volume(synth, 0, 127); + sf64_synth_set_expression(synth, 0, 127); + + // Velocity follows (x/127)²: 96 is 1.75× down, 64 a full 4× (a linear + // velocity/127 would only halve it). + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 72, 127); + mix(256); + int v127 = peak_n(256); + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 72, 96); + mix(256); + int v96 = peak_n(256); + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 72, 64); + mix(256); + int v64 = peak_n(256); + if (v127 < 1024 || v127 * 2 < v96 * 3 || v127 * 10 > v96 * 21) { + printf("FAILED gain vel: 127=%d 96=%d (want ~1.75×)\n", v127, v96); + goto fail_gain; + } + if (v64 * 3 > v127 || v64 * 5 < v127) { + printf("FAILED gain vel: 127=%d 64=%d (want ~4×)\n", v127, v64); + goto fail_gain; + } + + // Region attenuation halves amplitude when gain = 0.5. + rc->gain = 0.5f; + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 72, 127); + mix(256); + int half_attn = peak_n(256); + rc->gain = 1.0f; + if (half_attn * 2 < v127 * 3 / 4 || half_attn * 2 > v127 * 5 / 4) { + printf("FAILED gain attn: half %d full %d\n", half_attn, v127); + goto fail_gain; + } + + // Sustain at half peak: after instant decay, peak ≈ half. + rc->amp_env.sustain_gain = 0.5f; + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 72, 127); + mix(256); + int sus = peak_n(256); + rc->amp_env.sustain_gain = 1.0f; + if (sus * 2 < v127 * 3 / 4 || sus * 2 > v127 * 5 / 4) { + printf("FAILED gain sustain: sus %d full %d\n", sus, v127); + goto fail_gain; + } + + // Expression shares the SF2 curve with volume: 64 → a quarter. + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 72, 127); + mix(128); + sf64_synth_set_expression(synth, 0, 64); + mix(256); + int expr = peak_n(256); + sf64_synth_set_expression(synth, 0, 127); + if (expr * 3 > v127 || expr * 5 < v127) { + printf("FAILED gain expr: full %d expr64 %d (want ~4×)\n", v127, expr); + goto fail_gain; + } + + // Combined: velocity 64 × volume 64 → a quarter of a quarter. + sf64_synth_set_volume(synth, 0, 64); + synth_silence(synth, 1); + sf64_synth_note_on(synth, 0, 72, 64); + mix(256); + int combo = peak_n(256); + sf64_synth_set_volume(synth, 0, 127); + if (combo * 12 > v127 || combo * 22 < v127) { + printf("FAILED gain combo: full %d vel64×vol64 %d (want ~16×)\n", + v127, combo); + goto fail_gain; + } + + rc->amp_env = save; + rc->gain = save_gain; + sf64_synth_close(synth); + return true; + +fail_gain: + rc->amp_env = save; + rc->gain = save_gain; + sf64_synth_close(synth); + return false; +} + +int main(void) +{ + debug_init_emulog(); + debug_init_usblog(); + emux_ioctl_fast(); + console_init(); + + printf("SF64 bank / synth tests\n\n"); + + assert(dfs_init(DFS_DEFAULT_LOCATION) == DFS_ESUCCESS); + audio_init(SAMPLE_RATE, 4); + mixer_init(NCH); + for (int i = 0; i < NCH; i++) + mixer_ch_set_limits(i, 16, SAMPLE_RATE * 4, 0); + + out_cap = 8192 * 2; + out = malloc_uncached(out_cap * sizeof(int16_t)); + + sf64_bank_t *bank = sf64_load("rom:/sf64test.sf64"); + assert(bank); + + int total = 0, failed = 0; + + total++; if (!test_bank_layout(bank)) failed++; + total++; if (!test_play_oneshot(bank, 0, pcm_a, "sample A")) failed++; + total++; if (!test_play_oneshot(bank, 1, pcm_c, "sample C")) failed++; + total++; if (!test_play_oneshot(bank, 2, pcm_d, "sample D")) failed++; + total++; if (!test_play_loop(bank)) failed++; + total++; if (!test_slow_ramp(bank)) failed++; + + printf("synth\n"); + fflush(stdout); + // Oneshoot helpers pin the channel to SAMPLE_RATE; pitch tests need headroom. + for (int i = 0; i < NCH; i++) + mixer_ch_set_limits(i, 16, SAMPLE_RATE * 4, 0); + total++; if (!test_synth_pitch(bank)) failed++; + total++; if (!test_synth_key_split(bank)) failed++; + total++; if (!test_synth_vel_split(bank)) failed++; + total++; if (!test_synth_allocator(bank)) failed++; + total++; if (!test_synth_polyphony(bank)) failed++; + total++; if (!test_synth_saturation(bank)) failed++; + total++; if (!test_synth_voice_steal(bank)) failed++; + total++; if (!test_synth_deadlines(bank)) failed++; + total++; if (!test_synth_preset(bank)) failed++; + total++; if (!test_synth_layers(bank)) failed++; + total++; if (!test_synth_layer_atomic(bank)) failed++; + total++; if (!test_synth_note_identity(bank)) failed++; + + printf("envelopes\n"); + fflush(stdout); + total++; if (!test_synth_attack(bank)) failed++; + total++; if (!test_synth_release(bank)) failed++; + total++; if (!test_synth_loop_env(bank)) failed++; + + printf("midi channels\n"); + fflush(stdout); + total++; if (!test_synth_midi_channels(bank)) failed++; + total++; if (!test_synth_midi_controllers(bank)) failed++; + + printf("sustain / exclusive\n"); + fflush(stdout); + total++; if (!test_synth_sustain_pedal(bank)) failed++; + total++; if (!test_synth_exclusive(bank)) failed++; + + printf("tests for fixed bugs\n"); + fflush(stdout); + total++; if (!test_synth_delay_hold(bank)) failed++; + total++; if (!test_synth_modenv(bank)) failed++; + total++; if (!test_synth_multiphase(bank)) failed++; + total++; if (!test_synth_oneshot_reclaim(bank)) failed++; + total++; if (!test_synth_drum_channel(bank)) failed++; + total++; if (!test_synth_midi_reset(bank)) failed++; + total++; if (!test_synth_channel_mode(bank)) failed++; + total++; if (!test_synth_gm_mode(bank)) failed++; + total++; if (!test_synth_rpn(bank)) failed++; + total++; if (!test_synth_attenuation(bank)) failed++; + total++; if (!test_synth_gain_model(bank)) failed++; + + sf64_close(bank); + + printf("\n%d/%d tests failed\n", failed, total); + if (failed == 0) + printf("SUCCESS\n"); + fflush(stdout); + emux_ioctl_exit(); + return failed ? 1 : 0; +} diff --git a/tests/test_sys.c b/tests/test_sys.c new file mode 100644 index 000000000..daf0b333c --- /dev/null +++ b/tests/test_sys.c @@ -0,0 +1,64 @@ +#include + +void test_sys_hwmemset(TestContext *ctx) { + const int BUFFER_SIZE = 4096; + + // Allocate a 4K buffer, aligned to 2K (RDRAM row) + uint8_t *buf = memalign(2048, BUFFER_SIZE); + DEFER(free(buf)); + + // Set different lengths, and check the result + static const int lengths[] = { 1, 2, 3, 7, 8, 9, 16, 17, 31, 32, 33, 63, 64, + 65, 127, 128, 129, 255, 256, 257, 511, 512, 513, 1023, 1024 }; + static const int offsets[] = { 0, 1, 2, 3, 4, 5, 6, 7, 126, 127, 128, 2039, 2040, 2044, 2045, 2046, 2047 }; + + for (int i=0; i= off && i < off+len) ? 0x44 : 0xAA; + + ASSERT_EQUAL_HEX(buf[i], expected, + "sys_hw_memset len=%d off=%d failed at byte %d", len, off, i); + } + } + } +} + +void test_sys_hwmemset_uncached(TestContext *ctx) { + const int BUFFER_SIZE = 4096; + + // Allocate a 4K buffer, aligned to 2K (RDRAM row) + uint8_t *buf = malloc_uncached_aligned(2048, BUFFER_SIZE); + DEFER(free_uncached(buf)); + + // Set different lengths, and check the result + static const int lengths[] = { 3, 7, 8, 9, 16, 64, 65, 127, 128, 511, 512, 1024 }; + static const int offsets[] = { 0, 4, 7, 127, 128, 2046, 2047 }; + + for (int i=0; i= off && i < off+len) ? 0x44 : 0xAA; + + ASSERT_EQUAL_HEX(buf[i], expected, + "sys_hw_memset uncached len=%d off=%d failed at byte %d", len, off, i); + } + } + } +} diff --git a/tests/test_wav64.c b/tests/test_wav64.c new file mode 100644 index 000000000..c5eeeb336 --- /dev/null +++ b/tests/test_wav64.c @@ -0,0 +1,3676 @@ +/** + * @file test_wav64.c + * @brief Standalone testrom for the mixer and wav64 playback + * + * Covers in-mixer VADPCM and PCM resampling, streamed and resident waveforms, + * looping (including mixer_ch_set_loop), and channel allocation. + */ +#include +#include +#include +#include +#include +#include +#include + +#include "../src/audio/mixer_internal.h" +#include "../src/audio/wav64_vadpcm_internal.h" + +////////////////////////////////////////////////////////////////////////////// +// Reference VADPCM decoder +////////////////////////////////////////////////////////////////////////////// + +typedef enum { + kVADPCMErrNone, + kVADPCMErrInvalidData, +} vadpcm_error; + +// Residual @p i of a frame, sign extended. It occupies payload bits +// [i*bits, i*bits + bits) counting bit 0 as the most significant bit of the +// first payload byte, which for bits==4 is the classic nibble packing. +static int vadpcm_get_residual(const uint8_t *frame, int i, int bits) { + int r = 0; + for (int b = 0; b < bits; b++) { + int p = i * bits + b; + r = (r << 1) | ((frame[1 + p/8] >> (7 - (p & 7))) & 1); + } + return (r ^ (1 << (bits-1))) - (1 << (bits-1)); +} + +static int vadpcm_clamp16(int x) { + if (x < -0x8000 || 0x7fff < x) { + return (x >> (sizeof(int) * CHAR_BIT - 1)) ^ 0x7fff; + } + return x; +} + +static vadpcm_error vadpcm_decode(int predictor_count, int order, + const wav64_vadpcm_vector_t *restrict codebook, + wav64_vadpcm_vector_t *restrict state, + size_t frame_count, int16_t *restrict dest, + const void *restrict src, int bits) { + const uint8_t *sptr = src; + for (size_t frame = 0; frame < frame_count; frame++) { + const uint8_t *fin = sptr + VADPCM_FRAME_BYTES(bits) * frame; + int control = fin[0]; + int scaling = control >> 4; + int predictor_index = control & 15; + if (predictor_index >= predictor_count) { + return kVADPCMErrInvalidData; + } + const wav64_vadpcm_vector_t *predictor = + codebook + order * predictor_index; + for (int vector = 0; vector < 2; vector++) { + int32_t accumulator[8]; + for (int i = 0; i < 8; i++) { + accumulator[i] = 0; + } + for (int k = 0; k < order; k++) { + int sample = state->v[8 - order + k]; + for (int i = 0; i < 8; i++) { + accumulator[i] += sample * predictor[k].v[i]; + } + } + int residuals[8]; + for (int i = 0; i < 8; i++) + residuals[i] = vadpcm_get_residual(fin, 8 * vector + i, bits); + const wav64_vadpcm_vector_t *v = &predictor[order - 1]; + for (int k = 0; k < 8; k++) { + int residual = residuals[k] << scaling; + accumulator[k] += residual << 11; + for (int i = 0; i < 7 - k; i++) { + accumulator[k + 1 + i] += residual * v->v[i]; + } + } + for (int i = 0; i < 8; i++) { + int sample = vadpcm_clamp16(accumulator[i] >> 11); + dest[16 * frame + 8 * vector + i] = sample; + state->v[i] = sample; + } + } + } + return 0; +} + +////////////////////////////////////////////////////////////////////////////// +// Test scaffolding +////////////////////////////////////////////////////////////////////////////// + +static uint32_t rand_state = 1; +static void my_srand(uint32_t v) { rand_state = v ? v : 1; } +static uint32_t my_rand(void) { + uint32_t x = rand_state; + x ^= x << 13; x ^= x >> 17; x ^= x << 5; + return rand_state = x; +} + +#define NPREDICTORS 4 +#define ORDER 2 + +#define CH_FLAGS_BPS_SHIFT (3<<0) +#define CH_FLAGS_16BIT (1<<2) +#define CH_FLAGS_STEREO (1<<3) +#define CH_FLAGS_STEREO_SUB (1<<4) +#define CH_FLAGS_VADPCM (1<<5) +#define CH_FLAGS_CLEAR_ACCUM (1<<7) +#define MIXER_FX64_FRAC 12 + +// Mirrors the private command ids/layouts in src/audio/mixer.c. +#define MIXER_CMD_CHANNEL 0x0 +#define MIXER_CMD_SETCHANNEL 0x1 +#define MIXER_CMD_FLUSH 0x2 + +// Register the VADPCM codebook and predictor state of a channel in the ucode +// channel table (MIX_CHANNEL only carries the sample pointer). The residual +// width rides in the two low bits of the codebook pointer, as in mixer.c. +static void emit_setchannel(int ch, void *codebook, void *state, int bits) { + rspq_write(__mixer_overlay_id, MIXER_CMD_SETCHANNEL, (uint32_t)ch << 16, + PhysicalAddr(codebook) | (bits - 2), PhysicalAddr(state), 0); +} + +// @p dlvol / @p drvol are the volume ramp increments applied by the ucode every +// 4 output samples; 0 keeps the volume constant for the whole command. @p tlvol +// / @p trvol are where the ramp stops: the ucode clamps between them and the +// starting volume, so passing the starting volume itself pins the volume. +static void emit_channel_target(int ch, uint32_t flags, int16_t lvol, int16_t rvol, + int16_t dlvol, int16_t drvol, int16_t tlvol, int16_t trvol, uint32_t pos, + uint32_t step, uint32_t len, uint32_t loop_len, void *ptr, int nsamples) +{ + assert((nsamples & 1) == 0 && nsamples >= 0 && nsamples <= 512); + rspq_write_t w = rspq_write_begin(__mixer_overlay_id, MIXER_CMD_CHANNEL, 9); + // a0: ch<<19 | flags<<11 | nsamples/2 (9 bits: nsamples up to 512) + rspq_write_arg(&w, ((uint32_t)ch << 19) | ((flags & 0xFF) << 11) | ((uint32_t)nsamples >> 1)); + rspq_write_arg(&w, ((uint32_t)(uint16_t)lvol << 16) | (uint16_t)rvol); + rspq_write_arg(&w, pos); + rspq_write_arg(&w, step); + rspq_write_arg(&w, len); + rspq_write_arg(&w, loop_len); + rspq_write_arg(&w, PhysicalAddr(ptr)); + rspq_write_arg(&w, ((uint32_t)(uint16_t)dlvol << 16) | (uint16_t)drvol); + rspq_write_arg(&w, ((uint32_t)(uint16_t)tlvol << 16) | (uint16_t)trvol); + rspq_write_end(&w); +} + +static void emit_channel_lr(int ch, uint32_t flags, int16_t lvol, int16_t rvol, + uint32_t pos, uint32_t step, uint32_t len, uint32_t loop_len, void *ptr, + int nsamples) +{ + emit_channel_target(ch, flags, lvol, rvol, 0, 0, lvol, rvol, pos, step, len, + loop_len, ptr, nsamples); +} + +static void emit_channel(int ch, uint32_t flags, int16_t vol, uint32_t pos, + uint32_t step, uint32_t len, uint32_t loop_len, void *ptr, int nsamples) +{ + emit_channel_lr(ch, flags, vol, vol, pos, step, len, loop_len, ptr, nsamples); +} + +static void emit_flush(int nsamples, void *out) { + // MIX_FLUSH publishes the id of the round it ends (the mixer uses it to + // know what the RSP has read); these tests drive the ucode by hand and + // have nowhere to track it, so it goes to a scratch word. + static uint32_t *round_marker = NULL; + if (!round_marker) + round_marker = malloc_uncached(8); + rspq_write(__mixer_overlay_id, MIXER_CMD_FLUSH, (uint32_t)nsamples, + PhysicalAddr(out), 0, PhysicalAddr(round_marker)); +} + +static void gen_codebook(wav64_vadpcm_vector_t *cb) { + for (int v = 0; v < 8; v++) + for (int i = 0; i < 8; i++) + cb[v].v[i] = (int16_t)((my_rand() % 4096) - 2048); +} + +// Every bit pattern is a valid payload whatever the residual width, so the +// frames are just random bytes behind a plausible control byte. +static void gen_frames(uint8_t *frames, int nframes_total, int bits) { + int fb = VADPCM_FRAME_BYTES(bits); + for (int f = 0; f < nframes_total; f++) { + uint8_t *fin = frames + fb * f; + int scaling = my_rand() % 5; + int predictor = my_rand() % NPREDICTORS; + fin[0] = (scaling << 4) | predictor; + for (int i = 1; i < fb; i++) + fin[i] = my_rand(); + } +} + +////////////////////////////////////////////////////////////////////////////// +// Hermite reference (shared by PCM and VADPCM tests) +////////////////////////////////////////////////////////////////////////////// + +// Reference implementation of the RSP mono resampler: 4-tap Catmull-Rom +// evaluated between the second and third tap, in the exact fixed point the +// ucode uses. +static int16_t hermite_ref(int16_t y0, int16_t y1, int16_t y2, int16_t y3, uint16_t x) +{ + uint16_t x2 = (uint16_t)(((uint32_t)x * x) >> 16); + uint16_t x05 = (uint16_t)(((uint32_t)x * 0x8000) >> 16); + uint16_t x205 = (uint16_t)(((uint32_t)x2 * 0x8000) >> 16); + uint16_t x3 = (uint16_t)(((uint32_t)x2 * x) >> 16); + uint16_t x305 = (uint16_t)(((uint32_t)x3 * 0x8000) >> 16); + + // vsub against zero saturates, so -32768 negates to 32767. + #define NEG(v) ((int16_t)((v) == INT16_MIN ? INT16_MAX : -(v))) + int16_t y0n = NEG(y0), y1n = NEG(y1), y2n = NEG(y2), y3n = NEG(y3); + #undef NEG + + int64_t acc = (int64_t)y1 << 16; + acc += (int64_t)y2 * x05; + acc += (int64_t)y0n * x05; + acc += (int64_t)y0 * x2; + acc += (int64_t)y1n * x2; + acc += (int64_t)y1n * x2; + acc += (int64_t)y1n * x205; + acc += (int64_t)y2 * x2; + acc += (int64_t)y2 * x2; + acc += (int64_t)y3n * x205; + acc += (int64_t)y0n * x305; + acc += (int64_t)y1 * x3; + acc += (int64_t)y1 * x305; + acc += (int64_t)y2n * x3; + acc += (int64_t)y2n * x305; + acc += (int64_t)y3 * x305; + + int64_t v = acc >> 16; + if (v > INT16_MAX) v = INT16_MAX; + if (v < INT16_MIN) v = INT16_MIN; + return (int16_t)v; +} + +static bool test_vadpcm_inmixer(int nframes, uint32_t step, int nout, int seed, int bits) { + my_srand(seed); + + // Predictors + Hermite loop taps (zeros: this path does not loop). + uint8_t *codebook_raw = malloc_uncached(VADPCM_CODEBOOK_STRIDE); + memset(codebook_raw, 0, VADPCM_CODEBOOK_STRIDE); + wav64_vadpcm_vector_t *codebook = (wav64_vadpcm_vector_t *)codebook_raw; + gen_codebook(codebook); + + wav64_vadpcm_vector_t *state_rsp = malloc_uncached(sizeof(wav64_vadpcm_vector_t)); + memset(state_rsp, 0, sizeof(*state_rsp)); + wav64_vadpcm_vector_t state_ref; + memset(&state_ref, 0, sizeof(state_ref)); + + int in_bytes = VADPCM_FRAME_BYTES(bits) * nframes; + uint8_t *frames = malloc_uncached(in_bytes + 16); + memset(frames, 0, in_bytes + 16); + gen_frames(frames, nframes, bits); + + int nsamples = nframes * 16; + // Pad decoded PCM with zeros for Hermite taps past the last sample. + int16_t *ref_pcm = malloc((size_t)(nsamples + 4) * sizeof(int16_t)); + memset(ref_pcm, 0, (size_t)(nsamples + 4) * sizeof(int16_t)); + vadpcm_error err = vadpcm_decode(NPREDICTORS, ORDER, codebook, &state_ref, + nframes, ref_pcm, frames, bits); + assertf(err == 0, "reference decode error: %d", err); + + int32_t *out = malloc_uncached(nout * 4); + memset(out, 0, nout * 4); + + int16_t vol = 0x7FFF; + uint32_t flags = CH_FLAGS_VADPCM | CH_FLAGS_16BIT | CH_FLAGS_CLEAR_ACCUM; + uint32_t len = (uint32_t)nsamples << MIXER_FX64_FRAC; + + memset(state_rsp, 0, sizeof(*state_rsp)); + rspq_highpri_begin(); + emit_setchannel(0, codebook, state_rsp, bits); + emit_channel(0, flags, vol, 0, step, len, 0, frames, nout); + emit_flush(nout, out); + rspq_highpri_end(); + rspq_wait(); + + bool ok = true; + int16_t *stereo = (int16_t*)out; + for (int i = 0; i < nout; i++) { + uint32_t pos = step * (uint32_t)i; + int si = pos >> MIXER_FX64_FRAC; + uint16_t x = (uint16_t)(pos << 4); + int16_t y0 = ref_pcm[si], y1 = ref_pcm[si+1], y2 = ref_pcm[si+2], y3 = ref_pcm[si+3]; + int16_t res = hermite_ref(y0, y1, y2, y3, x); + int32_t v = (int32_t)(((int64_t)res * vol * 2) >> 16); + int16_t l = stereo[i*2], r = stereo[i*2+1]; + int d0 = l - v, d1 = r - v; + if (d0 < -2 || d0 > 2 || d1 < -2 || d1 > 2) { + printf("FAILED vadpcm hermite %dbit: nf=%d step=%#lx seed=%d i=%d: L=%d R=%d ref=%ld\n", + bits, nframes, (long)step, seed, i, l, r, (long)v); + printf(" si=%d x=%#x taps=%d %d %d %d\n", si, x, y0, y1, y2, y3); + ok = false; + break; + } + } + + // State ready for floor(final_pos / 16). + uint32_t end_pos = step * (uint32_t)nout; + int end_frame = end_pos >> (MIXER_FX64_FRAC + 4); + if (end_frame > nframes) end_frame = nframes; + wav64_vadpcm_vector_t state_end; + memset(&state_end, 0, sizeof(state_end)); + int16_t *discard = malloc((size_t)nframes * 16 * sizeof(int16_t)); + err = vadpcm_decode(NPREDICTORS, ORDER, codebook, &state_end, + end_frame, discard, frames, bits); + assertf(err == 0, "reference end-state decode error: %d", err); + free(discard); + if (ok && memcmp(state_rsp, &state_end, sizeof(state_end)) != 0) { + printf("FAILED vadpcm hermite %dbit: nf=%d step=%#lx seed=%d: state mismatch (end_frame=%d)\n", + bits, nframes, (long)step, seed, end_frame); + ok = false; + } + + free_uncached(codebook_raw); + free_uncached(state_rsp); + free_uncached(frames); + free_uncached(out); + free(ref_pcm); + return ok; +} + +////////////////////////////////////////////////////////////////////////////// +// Mono PCM resampling +////////////////////////////////////////////////////////////////////////////// + +static bool test_pcm_resample(bool is16, uint32_t step, int nsamples, int seed) +{ + my_srand(seed); + + // Waveform, padded so that the tap window and the RSP overread past the + // last played sample always land on readable (zero) memory. + int bps = is16 ? 1 : 0; + int nwave = 512; + int wave_bytes = (nwave << bps) + 256; + uint8_t *wave = malloc_uncached(wave_bytes); + memset(wave, 0, wave_bytes); + for (int i = 0; i < nwave; i++) { + if (is16) ((int16_t*)wave)[i] = (int16_t)my_rand(); + else ((int8_t*)wave)[i] = (int8_t)my_rand(); + } + + int32_t *out = malloc_uncached(nsamples * 4); + memset(out, 0, nsamples * 4); + + int16_t vol = 0x7FFF; + uint32_t flags = CH_FLAGS_CLEAR_ACCUM | (is16 ? (CH_FLAGS_16BIT | 1) : 0); + uint32_t len = (uint32_t)(nwave << bps) << MIXER_FX64_FRAC; + + rspq_highpri_begin(); + emit_channel(0, flags, vol, 0, step, len, 0, wave, nsamples); + emit_flush(nsamples, out); + rspq_highpri_end(); + rspq_wait(); + + bool ok = true; + int16_t *stereo = (int16_t*)out; + for (int i = 0; i < nsamples; i++) { + uint32_t pos = step * (uint32_t)i; + int si = pos >> (MIXER_FX64_FRAC + bps); + uint16_t x = (uint16_t)(pos << (is16 ? 3 : 4)); + + int16_t y[4]; + for (int t = 0; t < 4; t++) + y[t] = is16 ? ((int16_t*)wave)[si+t] : (int16_t)(((int8_t*)wave)[si+t] << 8); + int16_t res = hermite_ref(y[0], y[1], y[2], y[3], x); + + int32_t v = (int32_t)(((int64_t)res * vol * 2) >> 16); + int16_t l = stereo[i*2], r = stereo[i*2+1]; + int d0 = l - v, d1 = r - v; + if (d0 < -2 || d0 > 2 || d1 < -2 || d1 > 2) { + printf("FAILED resample %dbit step=%#lx: sample %d: L=%d R=%d ref=%ld\n", + is16 ? 16 : 8, step, i, l, r, v); + printf(" si=%d x=%#x taps=%d %d %d %d\n", si, x, y[0], y[1], y[2], y[3]); + ok = false; + break; + } + } + + free_uncached(wave); + free_uncached(out); + return ok; +} + +// Stereo PCM: interleaved L/R in the waveform, two MIX_CHANNELs (owner=L, +// STEREO_SUB=R) that share the Hermite loop with mono. +static bool test_pcm_resample_stereo(bool is16, uint32_t step, int nsamples, int seed) +{ + my_srand(seed); + + int bps = (is16 ? 1 : 0) + 1; // stereo + int nframes = 512; + int wave_bytes = (nframes << bps) + 256; + uint8_t *wave = malloc_uncached(wave_bytes); + memset(wave, 0, wave_bytes); + for (int i = 0; i < nframes * 2; i++) { + if (is16) ((int16_t*)wave)[i] = (int16_t)my_rand(); + else ((int8_t*)wave)[i] = (int8_t)my_rand(); + } + + int32_t *out = malloc_uncached(nsamples * 4); + memset(out, 0, nsamples * 4); + + int16_t vol = 0x7FFF; + uint32_t flags_l = CH_FLAGS_CLEAR_ACCUM | CH_FLAGS_STEREO | + (is16 ? (CH_FLAGS_16BIT | 2) : 1); + uint32_t flags_r = CH_FLAGS_STEREO | CH_FLAGS_STEREO_SUB | + (is16 ? (CH_FLAGS_16BIT | 2) : 1); + uint32_t len = (uint32_t)(nframes << bps) << MIXER_FX64_FRAC; + + rspq_highpri_begin(); + emit_channel_lr(0, flags_l, vol, 0, 0, step, len, 0, wave, nsamples); + emit_channel_lr(1, flags_r, 0, vol, 0, step, len, 0, wave, nsamples); + emit_flush(nsamples, out); + rspq_highpri_end(); + rspq_wait(); + + bool ok = true; + int16_t *stereo = (int16_t*)out; + for (int i = 0; i < nsamples; i++) { + uint32_t pos = step * (uint32_t)i; + int fi = pos >> (MIXER_FX64_FRAC + bps); + uint16_t x = (uint16_t)(pos << (4 - bps)); + + int16_t yl[4], yr[4]; + for (int t = 0; t < 4; t++) { + if (is16) { + yl[t] = ((int16_t*)wave)[(fi + t) * 2]; + yr[t] = ((int16_t*)wave)[(fi + t) * 2 + 1]; + } else { + yl[t] = (int16_t)(((int8_t*)wave)[(fi + t) * 2] << 8); + yr[t] = (int16_t)(((int8_t*)wave)[(fi + t) * 2 + 1] << 8); + } + } + int16_t res_l = hermite_ref(yl[0], yl[1], yl[2], yl[3], x); + int16_t res_r = hermite_ref(yr[0], yr[1], yr[2], yr[3], x); + int32_t vl = (int32_t)(((int64_t)res_l * vol * 2) >> 16); + int32_t vr = (int32_t)(((int64_t)res_r * vol * 2) >> 16); + + int16_t l = stereo[i*2], r = stereo[i*2+1]; + int d0 = l - vl, d1 = r - vr; + if (d0 < -2 || d0 > 2 || d1 < -2 || d1 > 2) { + printf("FAILED resample stereo %dbit step=%#lx: sample %d: L=%d/%ld R=%d/%ld\n", + is16 ? 16 : 8, step, i, l, vl, r, vr); + printf(" fi=%d x=%#x\n", fi, x); + ok = false; + break; + } + } + + free_uncached(wave); + free_uncached(out); + return ok; +} + +////////////////////////////////////////////////////////////////////////////// +// Streamed stereo VADPCM through the mixer +// +// These cover the CPU side of the mixer (channel pairing, streamed refills, +// position bookkeeping) rather than the ucode, so the waveform is procedural: +// the reader synthesizes the 9-byte frames from the absolute frame index, and +// the two planes carry the same content. Two invariants follow, and each bug +// fixed in this area used to break at least one of them: +// +// - the two output channels must stay sample-identical, since the planes are +// identical and their volumes equal: any drift between the left and right +// rings is immediately visible; +// - the mixed output must not depend on where in the stream the content sits, +// which is what test_mixer_posfold compares. +////////////////////////////////////////////////////////////////////////////// + +// Long enough for the fixed point position to cross the 31st bit, which is +// where the position field sent to the RSP ends and the CPU has to start +// folding the excess into the sample pointer. +#define SV_FOLD_FRAME (1 << (31 - MIXER_FX64_FRAC - 4)) +#define SV_FRAMES (SV_FOLD_FRAME + 4096) +#define SV_FREQ 32000 +#define SV_CHANNEL 0 + +static wav64_vadpcm_vector_t *sv_codebook; +static int sv_shift; // frame f of the waveform carries content f + sv_shift +static int16_t *sv_out; // mixing buffer, sized for the longest capture +static int sv_bits = 4; // residual width of the streamed waveforms + +// Frame bytes as a pure function of the content index: any mismatch between +// the frame the mixer meant to play and the one it played changes the audio. +static void sv_gen_frame(uint8_t *out, int idx, int bits) +{ + uint32_t h = (uint32_t)idx * 2654435761u + 1; + h ^= h >> 15; + // Large scaling factors: the checks below have to be able to tell a + // mistake from silence. Narrower residuals are scaled up by as much as + // they lost, so that every width decodes to the same amplitude. + out[0] = (uint8_t)(((6 + (4 - bits) + h % 4) << 4) | ((h >> 8) % NPREDICTORS)); + for (int i = 1; i < VADPCM_FRAME_BYTES(bits); i++) { + h = h * 1103515245 + 12345; + out[i] = (uint8_t)(h >> 24); + } +} + +static void sv_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + samplebuffer_t *sbuf_r = sbuf + 1; + wav64_state_vadpcm_t *st = sbuf->state; + (void)ctx; + + + // Same rule as wav64_vadpcm.c: the right plane has no reader of its own, + // so its ring restarts with the left one, but only when the left one + // really restarted. A loop wrap also arrives as a seek, and there the left + // ring keeps its live window: restarting the right one would leave the + // planes at different stream positions. + if (seeking && sbuf->widx == 0) { + // Every frame here decodes from a zero predictor state, so a restart + // is just a matter of clearing it (a real codec restores the state it + // saved for that frame). Only on a restart: the RSP runs behind the + // CPU, and rewriting the state under it would corrupt the frames it + // has not decoded yet. + memset(st->state, 0, sizeof(st->state)); + samplebuffer_flush(sbuf_r); + sbuf_r->wpos = wpos; + sbuf_r->head = sbuf->head; + } + + int fb = VADPCM_FRAME_BYTES(sv_bits); + while (wlen > 0) { + int n = wlen < SAMPLEBUFFER_MARGIN_UNITS ? wlen : SAMPLEBUFFER_MARGIN_UNITS; + uint8_t *dl = samplebuffer_append(sbuf, n); + uint8_t *dr = samplebuffer_append(sbuf_r, n); + for (int i = 0; i < n; i++) { + sv_gen_frame(dl + i*fb, wpos + i + sv_shift, sv_bits); + memcpy(dr + i*fb, dl + i*fb, fb); + } + sbuf_r->wnext = sbuf_r->wpos + sbuf_r->widx; + wlen -= n; + wpos += n; + } +} + +static waveform_vadpcm_t sv_codec; + +static waveform_t sv_wave = { + .name = "sv-stereo", .bits = 16, .channels = 2, .frequency = SV_FREQ, + .len = SV_FRAMES * 16, .read = sv_read, .format = WAVEFORM_FORMAT_VADPCM, + .codec = &sv_codec, .state_size = sizeof(wav64_state_vadpcm_t), +}; + +// Same stream, distinct waveform object: playing this over the other one makes +// the mixer reconfigure a channel that already holds a stereo VADPCM pair. +static waveform_t sv_wave2 = { + .name = "sv-stereo-2", .bits = 16, .channels = 2, .frequency = SV_FREQ, + .len = SV_FRAMES * 16, .read = sv_read, .format = WAVEFORM_FORMAT_VADPCM, + .codec = &sv_codec, .state_size = sizeof(wav64_state_vadpcm_t), +}; + +// Short enough to wrap a few times over a test, long enough that the mixer +// keeps streaming it instead of pinning it into the samplebuffer. +#define SV_LOOP_FRAMES 1024 + +static waveform_t sv_wave_loop = { + .name = "sv-stereo-loop", .bits = 16, .channels = 2, .frequency = SV_FREQ, + .len = SV_LOOP_FRAMES * 16, .loop_len = SV_LOOP_FRAMES * 16, + .read = sv_read, .format = WAVEFORM_FORMAT_VADPCM, + .codec = &sv_codec, .state_size = sizeof(wav64_state_vadpcm_t), +}; + +static bool sv_inited; + +static void sv_init(void) +{ + if (sv_inited) + return; + sv_inited = true; + my_srand(4321); + // Two codebooks, one per plane, with the same content: identical frames + // must decode to identical samples on both sides. Each plane is + // VADPCM_CODEBOOK_STRIDE bytes (predictors + zeroed loop taps). + sv_codebook = malloc_uncached(2 * VADPCM_CODEBOOK_STRIDE); + memset(sv_codebook, 0, 2 * VADPCM_CODEBOOK_STRIDE); + gen_codebook(sv_codebook); + memcpy((uint8_t *)sv_codebook + VADPCM_CODEBOOK_STRIDE, sv_codebook, + 8 * sizeof(wav64_vadpcm_vector_t)); + sv_codec.codebook = sv_codebook; + sv_codec.loop_state = NULL; + sv_codec.bits = sv_bits; + sv_out = malloc_uncached(8192 * 4); + // The sweep goes above the output rate, so the buffers must be sized for + // the fastest playback the channel will ever see. + mixer_ch_set_limits(SV_CHANNEL, 0, 48000, 0); +} + +// The frames are generated on the fly, so switching the residual width of the +// streamed waveforms updates the shared codec; mixer_ch_play must pick up the +// new VADPCM frame size even though the waveform uuid is unchanged. +static void sv_set_bits(int bits) +{ + sv_bits = bits; + sv_codec.bits = bits; +} + +// Samples per #mixer_poll: one call enqueues that many samples worth of mix +// rounds before syncing the RSP, so this is how far the CPU is allowed to run +// ahead of it. Games poll one audio buffer at a time (~1000 samples). +static int sv_chunk = 1024; + +static void sv_mix(int nsamples) +{ + int16_t *out = sv_out; + while (nsamples > 0) { + int n = nsamples < sv_chunk ? nsamples : sv_chunk; + mixer_poll(out, n); + out += n * 2; + nsamples -= n; + } +} + +// Silence everything and let the volume ramps decay, so that each test starts +// from the same state. +static void sv_silence(void) +{ + mixer_ch_stop(SV_CHANNEL); + sv_mix(2048); +} + +// Start the waveform, then mix (and discard) enough samples for the volume +// ramps of both channels of the pair to settle on their target. +static void sv_start(waveform_t *wave, int startsample, float freq) +{ + sv_silence(); + mixer_ch_play(SV_CHANNEL, wave); + mixer_ch_set_freq(SV_CHANNEL, freq); + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + if (startsample) + mixer_ch_set_pos(SV_CHANNEL, startsample); + sv_mix(2048); +} + +// The planes are identical and the volumes equal, so the two output channels +// must carry the same audio: they do not if the rings drift apart. +static bool sv_check_lr(const char *tag, int nsamples) +{ + // Not bit-identical though: each channel runs its own volume ramp, and + // integer truncation gives that filter a small band of fixed points rather + // than a single one, so two ramps that started from different values can + // settle a few units apart and stay there. + int peak = 0, nbad = 0, first = -1, maxdiff = 0; + for (int i = 0; i < nsamples; i++) { + int16_t l = sv_out[i*2], r = sv_out[i*2+1]; + int d = l - r; if (d < 0) d = -d; + if (d > 32) { + nbad++; + if (first < 0) first = i; + if (d > maxdiff) maxdiff = d; + } + int a = l < 0 ? -l : l; + if (a > peak) peak = a; + } + if (nbad) { + printf("FAILED %s: L/R differ on %d/%d samples (max %d) from %d\n", + tag, nbad, nsamples, maxdiff, first); + for (int i = first; i < first + 6 && i < nsamples; i++) + printf(" [%d] %d / %d\n", i, sv_out[i*2], sv_out[i*2+1]); + return false; + } + // A silent output would satisfy every check in this file. + if (peak < 4096) { + printf("FAILED %s: output is silent (peak %d)\n", tag, peak); + return false; + } + return true; +} + +// Stream at several playback rates and from several start positions, including +// ones that are not frame aligned: the size of a refill depends on both, and +// underestimating it by a single frame trips an assertion in samplebuffer_get. +static bool test_mixer_stream(float freq, int startsample) +{ + sv_shift = 0; + sv_start(&sv_wave, startsample, freq); + + for (int i = 0; i < 8; i++) { + sv_mix(4096); + if (!sv_check_lr("mixer stream", 4096)) { + printf(" freq=%d startsample=%d iter=%d\n", (int)freq, startsample, i); + return false; + } + } + return true; +} + +// Every mix round the CPU enqueues keeps its window of the samplebuffer live +// until the RSP runs it, and the ring is only sized for the rounds of a couple +// of polls: past that, mixer_poll_async waits for the oldest poll before +// starting. Play the same stream three times — syncing after every short +// chunk, in poll-sized chunks, and finally queueing polls back-to-back without +// syncing, as mixer_try_play does. All three must produce the same audio: a +// refill landing on a window the RSP had still to read makes the runs diverge. +extern void mixer_poll_async(int16_t *out, int nsamples); + +static bool test_mixer_queue_depth(float freq) +{ + int nsamples = 8192; + int16_t *ref = malloc(nsamples * 4); + int saved = sv_chunk; + + sv_shift = 0; + sv_chunk = 64; + sv_start(&sv_wave, 0, freq); + sv_mix(nsamples); + memcpy(ref, sv_out, nsamples * 4); + bool ok = sv_check_lr("mixer queue depth (reference)", nsamples); + + // Same tolerance as sv_check_lr: each run settles its volume ramps + // independently. + #define CHECK_AGAINST_REF(tag) ({ \ + for (int i = 0; ok && i < nsamples * 2; i++) { \ + int d = ref[i] - sv_out[i]; if (d < 0) d = -d; \ + if (d > 32) { \ + printf("FAILED %s: freq=%d: sample %d: %d/%d, expected %d/%d\n", \ + tag, (int)freq, i/2, sv_out[(i/2)*2], sv_out[(i/2)*2+1], \ + ref[(i/2)*2], ref[(i/2)*2+1]); \ + ok = false; \ + } \ + } \ + }) + + int blen = audio_get_buffer_length() & ~1; + + sv_chunk = blen; + sv_start(&sv_wave, 0, freq); + sv_mix(nsamples); + ok = ok && sv_check_lr("mixer queue depth", nsamples); + CHECK_AGAINST_REF("mixer queue depth"); + + sv_start(&sv_wave, 0, freq); + for (int done = 0; done < nsamples; done += blen) { + int n = nsamples - done < blen ? nsamples - done : blen; + mixer_poll_async(sv_out + done * 2, n); + } + rspq_highpri_sync(); + ok = ok && sv_check_lr("mixer queue depth (async)", nsamples); + CHECK_AGAINST_REF("mixer queue depth (async)"); + + #undef CHECK_AGAINST_REF + sv_chunk = saved; + free(ref); + return ok; +} + +// The position the RSP receives is 31 bits wide, so past 2^31 the CPU has to +// carry the excess in the sample pointer instead. Play the same content twice, +// once from the start of the stream and once from around the boundary: the +// mixed output must be identical. +static bool test_mixer_posfold(void) +{ + int startframe = SV_FOLD_FRAME - 16; + int nsamples = 4096; + int16_t *ref = malloc(nsamples * 4); + + // Reference: the same frames, but early enough in the stream that the + // position never reaches the boundary. + sv_shift = startframe; + sv_start(&sv_wave, 0, SV_FREQ); + sv_mix(nsamples); + memcpy(ref, sv_out, nsamples * 4); + bool ok = sv_check_lr("mixer posfold (reference)", nsamples); + + // Same content, now sitting across the 2^31 boundary. + sv_shift = 0; + sv_start(&sv_wave, startframe * 16, SV_FREQ); + sv_mix(nsamples); + ok = ok && sv_check_lr("mixer posfold", nsamples); + + // Same tolerance as sv_check_lr: the two runs settle their volume ramps + // independently. + for (int i = 0; ok && i < nsamples * 2; i++) { + int d = ref[i] - sv_out[i]; if (d < 0) d = -d; + if (d > 32) { + printf("FAILED mixer posfold: sample %d: %d/%d, expected %d/%d\n", + i/2, sv_out[(i/2)*2], sv_out[(i/2)*2+1], ref[(i/2)*2], ref[(i/2)*2+1]); + ok = false; + } + } + + free(ref); + return ok; +} + +// Stopping a stereo channel releases the secondary one. If it is left armed, +// the mixer keeps it as an independent channel: it advances it and tries to +// refill its samplebuffer, which has no reader of its own. +static bool test_mixer_stop_releases_sub(void) +{ + sv_shift = 0; + sv_start(&sv_wave, 0, SV_FREQ); + sv_mix(4096); + if (!sv_check_lr("mixer stop", 4096)) + return false; + + mixer_ch_stop(SV_CHANNEL); + if (mixer_ch_playing(SV_CHANNEL + 1)) { + printf("FAILED mixer stop: channel %d still playing after stopping %d\n", + SV_CHANNEL + 1, SV_CHANNEL); + return false; + } + + // Mix well past the silence ramp: this is what used to run the orphaned + // channel off the end of its buffered window. + for (int i = 0; i < 8; i++) + sv_mix(4096); + for (int i = 0; i < 4096; i++) { + if (sv_out[i*2] || sv_out[i*2+1]) { + printf("FAILED mixer stop: still audible at sample %d: %d/%d\n", + i, sv_out[i*2], sv_out[i*2+1]); + return false; + } + } + return true; +} + +// Replacing the waveform of a channel that holds a stereo VADPCM pair has to +// reconfigure both rings, including the secondary one that only the owner ever +// fills. +static bool test_mixer_switch_waveform(void) +{ + sv_shift = 0; + sv_start(&sv_wave, 0, SV_FREQ); + sv_mix(4096); + + waveform_t *waves[] = { &sv_wave2, &sv_wave, &sv_wave2 }; + for (int i = 0; i < 3; i++) { + mixer_ch_play(SV_CHANNEL, waves[i]); + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + sv_mix(2048); + sv_mix(4096); + if (!sv_check_lr("mixer switch waveform", 4096)) { + printf(" after switching to %s\n", waves[i]->name); + return false; + } + } + return true; +} + +// A loop wrap refills the ring from the loop start without restarting it: the +// two planes must go through that together. +// +// The wrap also re-seeds the decoder, which is where the state the RSP reads +// and the one the CPU writes have to be kept apart: the epilog of the rounds +// still queued saves its own state for each plane at a different moment, and +// used to overwrite the seed of one of them. This waveform declares no +// loop_state, so nothing else covers up a seed that gets lost. +static bool test_mixer_loop(void) +{ + sv_shift = 0; + sv_start(&sv_wave_loop, (SV_LOOP_FRAMES - 512) * 16, SV_FREQ); + + // Enough to go around the loop a few times. + for (int i = 0; i < 16; i++) { + sv_mix(4096); + if (!sv_check_lr("mixer loop", 4096)) { + printf(" iter=%d bits=%d\n", i, sv_bits); + return false; + } + } + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// Reconfigure after the samplebuffer is no longer configured for the wave +// +// mixer_ch_play skips set_unit_bytes when wave_uuid still matches. Several +// paths close/reinit the ring (or just the stereo-R one) without clearing that +// uuid: the next play of the same waveform then leaves unit_bytes at 0 and +// mixer_channel_window divides by it. The start hook records the ring so the +// tests can observe the state without going through Mixer internals. +////////////////////////////////////////////////////////////////////////////// + +static samplebuffer_t *ru_sbuf; + +static void ru_start(void *ctx, samplebuffer_t *sbuf) +{ + (void)ctx; + ru_sbuf = sbuf; +} + +// Drop any ring left by a previous test and forget the waveform uuid so the +// next play is a full configure. set_limits closes the buffer when the limit +// actually changes. +static void ru_reset(waveform_t *w) +{ + sv_silence(); + w->__uuid = 0; + w->start = ru_start; + mixer_ch_set_limits(SV_CHANNEL, 0, 48000, 2048); + mixer_ch_set_limits(SV_CHANNEL, 0, 48000, 0); + mixer_ch_set_limits(SV_CHANNEL + 1, 0, 48000, 2048); + mixer_ch_set_limits(SV_CHANNEL + 1, 0, 0, 0); + ru_sbuf = NULL; +} + +// End state of "ring closed and reinited, configure skipped": uuid and wave +// pointer still say this waveform, but unit_bytes is 0. Replay must restore it. +static bool test_mixer_stale_unit_bytes(void) +{ + waveform_t *w = &sv_wave; + WaveformStart prev = w->start; + ru_reset(w); + + mixer_ch_play(SV_CHANNEL, w); + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + if (!ru_sbuf || ru_sbuf->unit_bytes == 0) { + printf("FAILED stale unit_bytes: first play did not configure the ring\n"); + w->start = prev; + return false; + } + samplebuffer_t *sbuf = ru_sbuf; + int ub = sbuf->unit_bytes; + + mixer_ch_stop(SV_CHANNEL); + // step=0 so mixer_refresh_max_ns does not divide by the zeroed unit_bytes + // during the next play (the bug would otherwise teq there already). + mixer_ch_set_freq(SV_CHANNEL, 0); + sbuf->unit_bytes = 0; + + ru_sbuf = NULL; + mixer_ch_play(SV_CHANNEL, w); + w->start = prev; + + if (sbuf->unit_bytes == 0) { + printf("FAILED stale unit_bytes: replay of %s left unit_bytes=0 " + "(uuid reuse without reconfigure)\n", w->name); + sbuf->unit_bytes = ub; + mixer_ch_stop(SV_CHANNEL); + return false; + } + if (sbuf->unit_bytes != ub) { + printf("FAILED stale unit_bytes: unit_bytes %d, expected %d\n", + sbuf->unit_bytes, ub); + mixer_ch_stop(SV_CHANNEL); + return false; + } + + mixer_ch_set_freq(SV_CHANNEL, SV_FREQ); + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + sv_mix(2048); + sv_mix(4096); + if (!sv_check_lr("stale unit_bytes", 4096)) + return false; + return true; +} + +// mixer_ch_set_limits on the secondary channel frees its ring and clears only +// that channel's uuid. Replaying the same stereo VADPCM on the owner used to +// keep the owner's uuid, skip reconfigure, and leave R with unit_bytes=0. +static bool test_mixer_stereo_r_reopen(void) +{ + waveform_t *w = &sv_wave; + WaveformStart prev = w->start; + ru_reset(w); + + mixer_ch_play(SV_CHANNEL, w); + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + if (!ru_sbuf || !samplebuffer_is_inited(ru_sbuf + 1) || + (ru_sbuf + 1)->unit_bytes == 0) { + printf("FAILED stereo R reopen: first play did not configure R\n"); + w->start = prev; + return false; + } + samplebuffer_t *sbuf = ru_sbuf; + + mixer_ch_stop(SV_CHANNEL); + // Any real change closes the ring; max_buf_sz is independent of the + // output rate so this stays a no-op-free trigger across audio_init values. + mixer_ch_set_limits(SV_CHANNEL + 1, 0, 0, 2048); + if (samplebuffer_is_inited(sbuf + 1)) { + printf("FAILED stereo R reopen: set_limits did not free R\n"); + w->start = prev; + return false; + } + + ru_sbuf = NULL; + mixer_ch_play(SV_CHANNEL, w); + w->start = prev; + + samplebuffer_t *cur = ru_sbuf ? ru_sbuf : sbuf; + if (!samplebuffer_is_inited(cur + 1) || (cur + 1)->unit_bytes == 0) { + printf("FAILED stereo R reopen: replay of %s left R unconfigured " + "(owner uuid kept after secondary ring was freed)\n", w->name); + mixer_ch_stop(SV_CHANNEL); + return false; + } + + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + sv_mix(2048); + sv_mix(4096); + if (!sv_check_lr("stereo R reopen", 4096)) + return false; + return true; +} + +// Same waveform again after the ring was closed and reinited while wave_uuid +// was kept: the capacity_bytes < need path inside mixer_ch_play does exactly +// that. We replay the end state here (close+init, wave pointer restored so the +// debug assertf does not fire first) and require that play reconfigures. +static bool test_mixer_realloc_same_wave(void) +{ + waveform_t *w = &sv_wave; + WaveformStart prev = w->start; + ru_reset(w); + + mixer_ch_play(SV_CHANNEL, w); + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + if (!ru_sbuf || ru_sbuf->unit_bytes == 0) { + printf("FAILED realloc same wave: first play did not configure the ring\n"); + w->start = prev; + return false; + } + samplebuffer_t *sbuf = ru_sbuf; + int ub = sbuf->unit_bytes; + int cap = sbuf->capacity_bytes; + int state_size = sbuf->state_size; + + mixer_ch_stop(SV_CHANNEL); + // End state of the capacity-too-small branch: ring reinited, unit_bytes 0, + // channel uuid still matching. Restoring wave avoids the assertf that + // currently guards this path in debug builds; without a reconfigure, + // unit_bytes stays 0 either way. + samplebuffer_close(sbuf); + void *ptr = malloc_uncached(cap + state_size); + if (!ptr) { + printf("FAILED realloc same wave: out of memory\n"); + w->start = prev; + return false; + } + samplebuffer_init(sbuf, ptr, cap, state_size); + sbuf->wave = w; + + ru_sbuf = NULL; + mixer_ch_set_freq(SV_CHANNEL, 0); + mixer_ch_play(SV_CHANNEL, w); + w->start = prev; + + if (sbuf->unit_bytes == 0) { + printf("FAILED realloc same wave: replay of %s left unit_bytes=0 " + "after close+reinit with uuid kept\n", w->name); + mixer_ch_stop(SV_CHANNEL); + return false; + } + if (sbuf->unit_bytes != ub) { + printf("FAILED realloc same wave: unit_bytes %d, expected %d\n", + sbuf->unit_bytes, ub); + mixer_ch_stop(SV_CHANNEL); + return false; + } + if (!ru_sbuf) { + printf("FAILED realloc same wave: configure/start did not run on replay\n"); + mixer_ch_stop(SV_CHANNEL); + return false; + } + + mixer_ch_set_freq(SV_CHANNEL, SV_FREQ); + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + sv_mix(2048); + sv_mix(4096); + if (!sv_check_lr("realloc same wave", 4096)) + return false; + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// Streamed mono PCM through the mixer +// +// Covers two bugs that only show up with uncompressed PCM (the path XM takes +// with --xm-compress 0), neither of which the VADPCM stream tests reach: +// +// - mixer_refresh_max_ns must reserve one unit for the inclusive span that +// mixer_channel_window counts (last-pos+1). Without it, a step below 1.0 +// asks samplebuffer_get for SAMPLEBUFFER_MARGIN_UNITS+1 and asserts. +// - 8-bit mono has unit_bytes 1, so an odd-length waveform leaves the ring +// write cursor on an odd index after a discard; samplebuffer_append must +// accept that state as long as head and wpos keep the same byte parity. +////////////////////////////////////////////////////////////////////////////// + +#define SP_FREQ 44100 +#define SP_LEN 10001 // odd on purpose +#define SP_CHANNEL 2 + +static void sp_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + (void)ctx; (void)seeking; + while (wlen > 0) { + int n = wlen < SAMPLEBUFFER_MARGIN_UNITS ? wlen : SAMPLEBUFFER_MARGIN_UNITS; + int8_t *d = samplebuffer_append(sbuf, n); + for (int i = 0; i < n; i++) + d[i] = (int8_t)((wpos + i) * 17 + 3); + wlen -= n; + wpos += n; + } +} + +static waveform_t sp_wave = { + .name = "sp-mono8", .bits = 8, .channels = 1, .frequency = SP_FREQ, + .len = SP_LEN, .loop_len = SP_LEN, .read = sp_read, +}; + +// Stream 8-bit mono PCM below the sample rate, so the resampler step is less +// than one and the inclusive window is one unit larger than the position +// advance. Crossing the loop point also forces odd-sized fragments through +// waveform_read, which is what leaves the ring with an odd wpos. +static bool test_mixer_stream_pcm8(float freq, int startsample) +{ + mixer_ch_stop(SP_CHANNEL); + sv_mix(2048); + + mixer_ch_set_limits(SP_CHANNEL, 8, 48000, 0); + mixer_ch_play(SP_CHANNEL, &sp_wave); + mixer_ch_set_freq(SP_CHANNEL, freq); + mixer_ch_set_vol(SP_CHANNEL, 0.5f, 0.5f); + if (startsample) + mixer_ch_set_pos(SP_CHANNEL, startsample); + sv_mix(2048); + + int peak = 0; + for (int i = 0; i < 16; i++) { + sv_mix(4096); + for (int j = 0; j < 4096 * 2; j++) { + int a = sv_out[j] < 0 ? -sv_out[j] : sv_out[j]; + if (a > peak) peak = a; + } + } + mixer_ch_stop(SP_CHANNEL); + sv_mix(2048); + + if (peak < 256) { + printf("FAILED mixer stream pcm8: silent (peak %d) freq=%d start=%d\n", + peak, (int)freq, startsample); + return false; + } + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// Block codec at a loop point +// +// A block codec (ULC, Opus) decodes whole blocks and undoes whatever crosses +// the end of the waveform; the mixer then appends the loop overread right +// after, in the same fetch and with no flush in between. Both writes reach +// RDRAM through SP DMA, which needs an 8-byte aligned destination, so the trim +// cannot stop at an arbitrary sample: it rounds up and keeps a few of the +// samples it already decoded past the end. +////////////////////////////////////////////////////////////////////////////// + +#define BC_LEN 4099 // not a multiple of the 4 samples of an 8-byte slot +#define BC_BLOCK 1024 +#define BC_CHANNEL 2 + +// Triangle over the whole loop, so the waveform is continuous across the wrap +// and any jump in the output is a defect rather than the signal. +static int16_t bc_sample(int pos) +{ + int half = BC_LEN / 2; + int v = pos < half ? pos : BC_LEN - pos; + return (int16_t)(v * 12000 / half); +} + +static int bc_calls, bc_units; + +static void bc_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + (void)ctx; (void)seeking; + int n = ROUND_UP(wlen, BC_BLOCK); + bc_calls++; bc_units += n; + int16_t *out = samplebuffer_append(sbuf, n); + for (int i = 0; i < n; i++) + out[i] = bc_sample((wpos + i) % BC_LEN); + + // Keep the samples the block already holds up to the next 8-byte boundary, + // which is what leaves the write cursor where the next block can land. + int valid = ROUND_UP(BC_LEN - wpos, 4); + if (n > valid) + samplebuffer_undo(sbuf, n - valid); +} + +static waveform_t bc_wave = { + .name = "bc-block", .bits = 16, .channels = 1, .frequency = 44100, + .len = BC_LEN, .loop_len = BC_LEN, .read = bc_read, + .append_units = BC_BLOCK, .rsp_written = true, +}; + +static bool test_mixer_block_codec_loop(float freq) +{ + mixer_ch_stop(BC_CHANNEL); + sv_mix(2048); + + mixer_ch_set_limits(BC_CHANNEL, 16, 48000, 0); + mixer_ch_play(BC_CHANNEL, &bc_wave); + mixer_ch_set_freq(BC_CHANNEL, freq); + mixer_ch_set_vol(BC_CHANNEL, 0.5f, 0.5f); + sv_mix(2048); + + // Several laps of the loop, so the trim and the refill happen many times. + bc_calls = bc_units = 0; + int peak = 0, maxjump = 0, worst = -1, prev = 0; + for (int i = 0; i < 4; i++) { + sv_mix(4096); + for (int j = 0; j < 4096; j++) { + int s = sv_out[j*2]; + int a = s < 0 ? -s : s; + int d = s - prev; if (d < 0) d = -d; + if (a > peak) peak = a; + if ((i || j) && d > maxjump) { maxjump = d; worst = i*4096 + j; } + prev = s; + } + } + mixer_ch_stop(BC_CHANNEL); + sv_mix(2048); + + if (peak < 1024) { + printf("FAILED block codec loop: silent (peak %d) freq=%d\n", peak, (int)freq); + return false; + } + // The slope is a handful of units per sample at most, so anything beyond + // this is the ring handing out samples from the wrong place. + if (maxjump > 200) { + printf("FAILED block codec loop: discontinuity %d at %d freq=%d (reads=%d units=%d)\n", + maxjump, worst, (int)freq, bc_calls, bc_units); + return false; + } + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// Partial VADPCM frames +// +// A waveform whose length is not a multiple of 16 ends with a partial frame, +// and audioconv64 pads the encoded stream past it. The samples in that frame +// are as playable as any other, so the mixer has to fetch it: dropping it +// leaves a hole right before the loop point, filled with whatever the pinned +// copy holds there (the loop start) and played as noise on every wrap. +// +// Sizes taken from instrument 1 of Arcade_S900.xm, where this showed up. +////////////////////////////////////////////////////////////////////////////// + +#define LV_LEN 1045 +#define LV_LOOP_LEN 277 + +static int lv_frames; // frames the mixer asked for, as a one-past-the-end index +static bool lv_oob; // ...and whether it went past what a file would hold + +static void lv_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + wav64_state_vadpcm_t *st = sbuf->state; + (void)ctx; + + if (seeking && sbuf->widx == 0) + memset(st->state, 0, sizeof(st->state)); + if (wpos + wlen > WAV64_VADPCM_FILE_FRAMES(LV_LEN)) + lv_oob = true; + if (wpos + wlen > lv_frames) + lv_frames = wpos + wlen; + + int fb = VADPCM_FRAME_BYTES(sv_bits); + while (wlen > 0) { + int n = wlen < SAMPLEBUFFER_MARGIN_UNITS ? wlen : SAMPLEBUFFER_MARGIN_UNITS; + uint8_t *dst = samplebuffer_append(sbuf, n); + for (int i = 0; i < n; i++) + sv_gen_frame(dst + i*fb, wpos + i, sv_bits); + wlen -= n; + wpos += n; + } +} + +static waveform_t lv_wave = { + .name = "lv-partial", .bits = 16, .channels = 1, .frequency = SV_FREQ, + .len = LV_LEN, .loop_len = LV_LOOP_LEN, .read = lv_read, + .format = WAVEFORM_FORMAT_VADPCM, + .codec = &sv_codec, .state_size = sizeof(wav64_state_vadpcm_t), +}; + +static bool test_mixer_vadpcm_partial_frame(float freq) +{ + lv_frames = 0; + lv_oob = false; + sv_start(&lv_wave, 0, freq); + sv_mix(4096); + + int need = WAV64_VADPCM_FRAMES(LV_LEN); + if (lv_frames < need) { + printf("FAILED vadpcm partial frame: fetched %d frames out of %d (freq=%d)\n", + lv_frames, need, (int)freq); + return false; + } + if (lv_oob) { + printf("FAILED vadpcm partial frame: read past the padding (freq=%d)\n", (int)freq); + return false; + } + int peak = 0; + for (int i = 0; i < 4096 * 2; i++) { + int a = sv_out[i] < 0 ? -sv_out[i] : sv_out[i]; + if (a > peak) peak = a; + } + // Just a sanity check: silence would satisfy everything above. + if (peak < 1024) { + printf("FAILED vadpcm partial frame: output is silent (freq=%d peak=%d)\n", (int)freq, peak); + return false; + } + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// Resampling across a loop point +// +// The resampler reads up to three taps past the sample it interpolates, so at +// the end of a waveform it needs whatever follows it: for a loop, the samples +// at the loop start. The VADPCM path used to hand it four zeroes there, which +// on a short loop is a dip to silence a few hundred times a second. +// +// A constant waveform makes any such tap visible: Hermite reproduces a +// constant exactly, so the output must be flat everywhere. +////////////////////////////////////////////////////////////////////////////// + +#define LP_SHIFT 12 +#define LP_RESIDUAL 4 + +static wav64_vadpcm_vector_t *lp_codebook; // zeroed: sample = residual << shift +static waveform_vadpcm_t lp_codec; + +static void lp_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + (void)ctx; (void)wpos; (void)seeking; + while (wlen > 0) { + int n = wlen < SAMPLEBUFFER_MARGIN_UNITS ? wlen : SAMPLEBUFFER_MARGIN_UNITS; + uint8_t *dst = samplebuffer_append(sbuf, n); + for (int i = 0; i < n; i++) { + dst[i*9] = LP_SHIFT << 4; + memset(dst + i*9 + 1, (LP_RESIDUAL << 4) | LP_RESIDUAL, 8); + } + wlen -= n; + wpos += n; + } +} + +static waveform_t lp_wave = { + .name = "lp-flat", .bits = 16, .channels = 1, .frequency = SV_FREQ, + .len = LV_LEN, .loop_len = LV_LOOP_LEN, .read = lp_read, + .format = WAVEFORM_FORMAT_VADPCM, + .codec = &lp_codec, .state_size = sizeof(wav64_state_vadpcm_t), +}; + +// A loop too long to be pinned into the samplebuffer is wrapped by the CPU +// between rounds, with a seek back to the loop point: sizes and loop point of +// instrument 5 of Arcade_S900, which a tracker plays three octaves up (eight +// input samples per output one). +static waveform_t lp_wave_big = { + .name = "lp-flat-bigloop", .bits = 16, .channels = 1, .frequency = SV_FREQ, + .len = 57872, .loop_len = 8752, .read = lp_read, + .format = WAVEFORM_FORMAT_VADPCM, + .codec = &lp_codec, .state_size = sizeof(wav64_state_vadpcm_t), +}; + +static bool test_mixer_loop_taps(waveform_t *wave, float freq, int startsample) +{ + if (!lp_codebook) { + // Zero predictors: sample = residual << shift. Loop taps match the + // constant the frames decode to so Hermite stays flat across wraps. + int16_t flat = (int16_t)(LP_RESIDUAL << LP_SHIFT); + lp_codebook = malloc_uncached(VADPCM_CODEBOOK_STRIDE); + memset(lp_codebook, 0, VADPCM_CODEBOOK_STRIDE); + int16_t *taps = (int16_t *)((uint8_t *)lp_codebook + 128); + taps[0] = taps[1] = taps[2] = flat; + lp_codec.codebook = lp_codebook; + lp_codec.bits = 4; + // A non-NULL loop_state arms the tap path in the ucode (the vector + // itself is unused for this constant waveform). + wav64_vadpcm_vector_t *lp_loop_state = malloc_uncached(sizeof(*lp_loop_state)); + memset(lp_loop_state, 0, sizeof(*lp_loop_state)); + lp_codec.loop_state = lp_loop_state; + } + sv_silence(); + mixer_ch_set_limits(SV_CHANNEL, 0, freq > 48000 ? freq : 48000, 0); + sv_start(wave, startsample, freq); + sv_mix(4096); + + int lo = INT_MAX, hi = INT_MIN, first = -1; + for (int i = 0; i < 4096 * 2; i++) { + if (sv_out[i] < lo) lo = sv_out[i]; + if (sv_out[i] > hi) { hi = sv_out[i]; } + } + for (int i = 0; i < 4096 * 2 && first < 0; i++) + if (hi - sv_out[i] > 64) first = i; + if (hi - lo > 64) { + printf("FAILED loop taps %s: output swings %d..%d (freq=%d), first dip at %d\n", + wave->name, lo, hi, (int)freq, first/2); + for (int i = first - 2; i < first + 4 && i < 4096*2; i++) + if (i >= 0) printf(" [%d] %d\n", i/2, sv_out[i]); + return false; + } + if (hi < 1024) { + printf("FAILED loop taps %s: output is silent (freq=%d)\n", wave->name, (int)freq); + return false; + } + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// Looping VADPCM, sample by sample +// +// The tests above play a constant, which shows samples that are dropped or +// zeroed but not samples played at the wrong position, twice, or not at all. +// Playing at the output rate turns the resampler into an identity (Hermite +// with a null fraction returns the sample it interpolates from), so every +// output sample can be matched against the waveform decoded on the CPU, with +// the loop unrolled by hand. +////////////////////////////////////////////////////////////////////////////// + +// Instrument 5 of Arcade_S900.xm: only its loop fits the samplebuffer, so it +// is streamed until playback reaches the loop point and pinned from there, and +// neither the length nor the loop length is a whole number of frames. +#define RF_LEN 10377 +#define RF_LOOP_LEN 1513 +#define RF_FRAMES ((RF_LEN + 15) / 16) +#define RF_LOOP_START (RF_LEN - RF_LOOP_LEN) + +static wav64_vadpcm_vector_t *rf_codebook; +static wav64_vadpcm_vector_t *rf_loop_state; +static uint8_t *rf_frames; // the waveform, encoded... +static int16_t *rf_ref; // ...and the samples it decodes to +static bool rf_oob; // reader was asked for frames the file has not +static waveform_vadpcm_t rf_codec; +static int rf_bits = 4; // residual width of the encoded waveform + +static void rf_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + (void)ctx; + if (wpos < 0 || wpos + wlen > RF_FRAMES) + rf_oob = true; + if (seeking) { + // The state a frame decodes from is just the eight samples before it. + wav64_state_vadpcm_t *st = sbuf->state; + for (int i = 0; i < 8; i++) { + int p = wpos * 16 - 8 + i; + st->state[0].v[i] = p >= 0 ? rf_ref[p] : 0; + } + } + int fb = VADPCM_FRAME_BYTES(rf_bits); + while (wlen > 0) { + int n = wlen < SAMPLEBUFFER_MARGIN_UNITS ? wlen : SAMPLEBUFFER_MARGIN_UNITS; + uint8_t *dst = samplebuffer_append(sbuf, n); + memcpy(dst, rf_frames + wpos * fb, n * fb); + wlen -= n; + wpos += n; + } +} + +static waveform_t rf_wave = { + .name = "rf-loop", .bits = 16, .channels = 1, .frequency = 44100, + .len = RF_LEN, .loop_len = RF_LOOP_LEN, .read = rf_read, + .format = WAVEFORM_FORMAT_VADPCM, + .codec = &rf_codec, .state_size = sizeof(wav64_state_vadpcm_t), +}; + +// Same stream without the loop: what happens between two rounds does not +// depend on it. +static waveform_t rf_wave_plain = { + .name = "rf-plain", .bits = 16, .channels = 1, .frequency = 44100, + .len = RF_FRAMES * 16, .read = rf_read, + .format = WAVEFORM_FORMAT_VADPCM, + .codec = &rf_codec, .state_size = sizeof(wav64_state_vadpcm_t), +}; + +static void rf_init(int bits) +{ + my_srand(20250802); + rf_bits = bits; + int fb = VADPCM_FRAME_BYTES(bits); + if (!rf_codebook) + rf_codebook = malloc_uncached(VADPCM_CODEBOOK_STRIDE); + memset(rf_codebook, 0, VADPCM_CODEBOOK_STRIDE); + gen_codebook(rf_codebook); + free(rf_frames); + rf_frames = malloc(RF_FRAMES * fb); + for (int f = 0; f < RF_FRAMES; f++) + sv_gen_frame(rf_frames + f * fb, f, bits); + free(rf_ref); + rf_ref = malloc(RF_FRAMES * 16 * 2); + wav64_vadpcm_vector_t state = {0}; + vadpcm_decode(NPREDICTORS, ORDER, rf_codebook, &state, RF_FRAMES, rf_ref, rf_frames, bits); + // The loop start is frame aligned (as audioconv64 makes sure it is), so + // the state to resume it from is the one of that frame. The first three + // samples of that frame ride after the codebook, as in a real WAV64. + if (!rf_loop_state) + rf_loop_state = malloc_uncached(sizeof(*rf_loop_state)); + for (int i = 0; i < 8; i++) + rf_loop_state->v[i] = rf_ref[RF_LOOP_START - 8 + i]; + int16_t *taps = (int16_t *)((uint8_t *)rf_codebook + 128); + taps[0] = rf_ref[RF_LOOP_START + 0]; + taps[1] = rf_ref[RF_LOOP_START + 1]; + taps[2] = rf_ref[RF_LOOP_START + 2]; + rf_codec.codebook = rf_codebook; + rf_codec.loop_state = rf_loop_state; + rf_codec.bits = bits; +} + +// Sample played when the position is @p pos, with the loop unrolled. The +// resampler interpolates between its second and third tap, so a position sits +// one sample before what it plays. +static int rf_len, rf_loop_len; // waveform being played + +static int rf_play(int pos) +{ + pos++; + if (rf_loop_len && pos >= rf_len) + pos = rf_len - rf_loop_len + (pos - rf_len) % rf_loop_len; + return pos; +} + +static int rf_expect(int pos) +{ + return rf_ref[rf_play(pos)]; +} + +static bool test_mixer_loop_exact(waveform_t *wave, int startsample) +{ + const int nsamples = 4096; + rf_len = wave->len; + rf_loop_len = wave->loop_len; + sv_silence(); + rf_oob = false; + sv_start(wave, startsample, 44100); + + // The warm-up mixed an unknown number of samples: ask the mixer where the + // capture starts from. + int pos = (int)mixer_ch_get_pos(SV_CHANNEL); + sv_mix(nsamples); + + int nbad = 0, first = -1, maxdiff = 0, peak = 0; + for (int i = 0; i < nsamples; i++) { + // Half volume, and the volume filter settles a couple of units short + // of its target. + int want = rf_expect(pos + i) / 2; + int got = sv_out[i*2]; + int d = got - want; if (d < 0) d = -d; + if (d > 64) { + nbad++; + if (first < 0) first = i; + if (d > maxdiff) maxdiff = d; + } + if (got > peak) peak = got; else if (-got > peak) peak = -got; + } + if (nbad) { + printf("FAILED exact %s (start=%d): %d/%d samples off (max %d)\n", + wave->name, startsample, nbad, nsamples, maxdiff); + // Where each burst of wrong samples begins, and how the position it + // plays sits inside its frame. + for (int i = 0, nrun = 0; i < nsamples && nrun < 5; i++) { + int d = sv_out[i*2] - rf_expect(pos+i)/2; if (d < 0) d = -d; + if (d <= 64) continue; + int j = i + 1, quiet = 0; + while (j < nsamples && quiet < 16) { + int d2 = sv_out[j*2] - rf_expect(pos+j)/2; if (d2 < 0) d2 = -d2; + quiet = d2 > 64 ? 0 : quiet + 1; + j++; + } + printf(" [%d..%d] play=%d (frame %d + %d)\n", i, j-1-quiet, + rf_play(pos+i), rf_play(pos+i) / 16, rf_play(pos+i) % 16); + nrun++; + i = j; + } + for (int i = first - 2; i < first + 6 && i < nsamples; i++) + if (i >= 0) printf(" [%d] play=%d got %d want %d\n", + i, rf_play(pos+i), sv_out[i*2], rf_expect(pos+i) / 2); + return false; + } + if (rf_oob) { + printf("FAILED exact %s (start=%d): read past the end of the waveform\n", + wave->name, startsample); + return false; + } + if (peak < 4096) { + printf("FAILED exact %s (start=%d): output is silent (peak %d)\n", + wave->name, startsample, peak); + return false; + } + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// Tracker-style retriggering +// +// A tracker restarts a channel from its playback callback several times a +// second, usually on a different instrument: a different codebook, a different +// stream, and a decoder state that has to go back to zero — all of it while +// the rounds of the previous note are still queued for the RSP. +// +// Each waveform here carries a codebook of its own, unlike the pair the stereo +// tests switch between: pointers that reach the ucode out of order decode the +// new frames into garbage that two identical planes would agree on. +////////////////////////////////////////////////////////////////////////////// + +#define IS_NWAVES 3 +#define IS_FRAMES 768 // 12288 samples: longer than any note played +#define IS_ROW 1024 // output samples between two retriggers +#define IS_NROWS 8 + +static wav64_vadpcm_vector_t *is_codebook[IS_NWAVES]; +static uint8_t *is_frames[IS_NWAVES]; +static int16_t *is_ref[IS_NWAVES]; +static waveform_vadpcm_t is_codec[IS_NWAVES]; +static waveform_t is_wave[IS_NWAVES]; +static bool is_oob; // reader was asked for frames outside the waveform + +static void is_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + int w = sbuf->wave - is_wave; + (void)ctx; + + if (wpos < 0 || wpos + wlen > IS_FRAMES) + is_oob = true; + if (seeking) { + // The state a frame decodes from is just the eight samples before it. + wav64_state_vadpcm_t *st = sbuf->state; + for (int i = 0; i < 8; i++) { + int p = wpos * 16 - 8 + i; + st->state[0].v[i] = p >= 0 ? is_ref[w][p] : 0; + } + } + while (wlen > 0) { + int n = wlen < SAMPLEBUFFER_MARGIN_UNITS ? wlen : SAMPLEBUFFER_MARGIN_UNITS; + uint8_t *dst = samplebuffer_append(sbuf, n); + memcpy(dst, is_frames[w] + wpos * 9, n * 9); + wlen -= n; + wpos += n; + } +} + +static void is_init(void) +{ + static const char *names[IS_NWAVES] = { "is-note0", "is-note1", "is-note2" }; + for (int w = 0; w < IS_NWAVES; w++) { + my_srand(0x51D0 + w * 7717); + is_codebook[w] = malloc_uncached(VADPCM_CODEBOOK_STRIDE); + memset(is_codebook[w], 0, VADPCM_CODEBOOK_STRIDE); + gen_codebook(is_codebook[w]); + is_frames[w] = malloc(IS_FRAMES * 9); + for (int f = 0; f < IS_FRAMES; f++) + sv_gen_frame(is_frames[w] + f * 9, w * 100000 + f, 4); + is_ref[w] = malloc(IS_FRAMES * 16 * 2); + wav64_vadpcm_vector_t state = {0}; + vadpcm_decode(NPREDICTORS, ORDER, is_codebook[w], &state, IS_FRAMES, + is_ref[w], is_frames[w], 4); + is_codec[w] = (waveform_vadpcm_t){ + .codebook = is_codebook[w], .loop_state = NULL, .bits = 4, + }; + is_wave[w] = (waveform_t){ + .name = names[w], .bits = 16, .channels = 1, .frequency = 44100, + .len = IS_FRAMES * 16, .read = is_read, + .format = WAVEFORM_FORMAT_VADPCM, + .codec = &is_codec[w], .state_size = sizeof(wav64_state_vadpcm_t), + }; + } +} + +static int is_cur; // waveform the tick is playing +static bool is_same_wave; // retrigger the same one instead of switching +static float is_freq; // playback rate of every note +static uint8_t is_row_wave[IS_NROWS + 2]; +static int is_nrows; + +static int is_tick(void *ctx) +{ + (void)ctx; + if (!is_same_wave) + is_cur = (is_cur + 1) % IS_NWAVES; + // Same shape as the xm64 tick: play the waveform only if it is not the one + // on the channel already, then restart it from the top. + if (mixer_ch_playing_waveform(SV_CHANNEL) != &is_wave[is_cur]) + mixer_ch_play(SV_CHANNEL, &is_wave[is_cur]); + mixer_ch_set_pos(SV_CHANNEL, 0); + mixer_ch_set_freq(SV_CHANNEL, is_freq); + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + if (is_nrows < (int)sizeof(is_row_wave)) + is_row_wave[is_nrows++] = is_cur; + return IS_ROW; +} + +// Output sample @p j of a note, at half volume: the same step the mixer +// derives from the playback rate, and the resampler of the ucode. +static int is_expect(int w, int j, float freq) +{ + uint32_t step = (uint32_t)(int64_t)((freq / 44100.0f) * (1 << MIXER_FX64_FRAC)); + uint32_t pos = step * (uint32_t)j; + const int16_t *r = is_ref[w] + (pos >> MIXER_FX64_FRAC); + return hermite_ref(r[0], r[1], r[2], r[3], (uint16_t)(pos << 4)) / 2; +} + +static bool test_mixer_retrigger(bool same_wave, float freq) +{ + const int nsamples = IS_ROW * IS_NROWS; + const char *tag = same_wave ? "retrigger" : "instrument switch"; + + sv_silence(); + is_oob = false; + is_cur = 0; + is_same_wave = same_wave; + is_freq = freq; + is_nrows = 0; + mixer_ch_play(SV_CHANNEL, &is_wave[0]); + mixer_ch_set_freq(SV_CHANNEL, freq); + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + sv_mix(2048); // settle the volume ramp + + mixer_add_event(IS_ROW, is_tick, NULL); + sv_mix(nsamples); + mixer_remove_event(is_tick, NULL); + + int peak = 0; + for (int i = 0; i < nsamples * 2; i++) { + int a = sv_out[i] < 0 ? -sv_out[i] : sv_out[i]; + if (a > peak) peak = a; + } + + // A retrigger can be served up to one sample late (a round is an even + // number of samples), so line each row up with the note it plays before + // comparing it. + for (int row = 1; row < IS_NROWS; row++) { + int w = is_row_wave[row-1]; + int base = row * IS_ROW; + int bestbad = INT_MAX, bestd = 0; + for (int d = -2; d <= 2; d++) { + int bad = 0; + for (int j = 8; j < IS_ROW - 8; j++) { + int e = sv_out[(base + j) * 2] - is_expect(w, j + d, freq); + if (e < -64 || e > 64) bad++; + } + if (bad < bestbad) { bestbad = bad; bestd = d; } + } + if (bestbad) { + printf("FAILED mixer %s (freq=%d chunk=%d): row %d (%s): %d/%d samples off (d=%d)\n", + tag, (int)freq, sv_chunk, row, is_wave[w].name, bestbad, IS_ROW - 16, bestd); + int first = 8; + while (first < IS_ROW - 8) { + int e = sv_out[(base + first) * 2] - is_expect(w, first + bestd, freq); + if (e < -64 || e > 64) break; + first++; + } + for (int j = first - 4; j < first + 12 && j < IS_ROW - 8; j++) + printf(" [%d] pos=%d got %6d want %6d\n", base + j, j, + sv_out[(base + j) * 2], is_expect(w, j + bestd, freq)); + return false; + } + } + if (is_oob) { + printf("FAILED mixer %s (freq=%d): read outside the waveform\n", tag, (int)freq); + return false; + } + if (peak < 4096) { + printf("FAILED mixer %s (freq=%d): output is silent (peak %d)\n", + tag, (int)freq, peak); + return false; + } + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// Pinned loops +// +// A loop short enough to fit the samplebuffer is copied there once and left +// pinned, so that the RSP can wrap it by itself. The copy must be followed by +// the samples the resampler reads past the loop end, and those are the ones at +// the loop start: taking them from past the end of the waveform instead turns +// every wrap into a burst of noise. +// +// The reference is the same signal with the loop already unrolled, which is +// long enough that the mixer streams it normally instead of pinning it. +////////////////////////////////////////////////////////////////////////////// + +static int lc_len, lc_loop_len; // loop being played, in samples +static bool lc_oob; // reader was asked for samples past the end + +// Noise-like, so that a tap taken from the wrong place cannot pass for a +// plausible sample. +static int16_t lc_sample(int pos) +{ + uint32_t h = (uint32_t)pos * 2654435761u + 1; + h ^= h >> 13; + return (int16_t)(h >> 8); +} + +// Position folded into the waveform: what an ideal looping playback reads. +static int lc_wrap(int pos) +{ + if (pos >= lc_len) + pos = lc_len - lc_loop_len + (pos - lc_len) % lc_loop_len; + return pos; +} + +static void lc_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + waveform_t *wave = sbuf->wave; + bool unrolled = (wave->loop_len == 0); + (void)ctx; (void)seeking; + + // The mixer clamps every read to [0, len): a codec reads its own samples + // and has no way to produce anything outside of them. + if (wpos < 0 || wpos + wlen > wave->len) + lc_oob = true; + + while (wlen > 0) { + int n = wlen < SAMPLEBUFFER_MARGIN_UNITS ? wlen : SAMPLEBUFFER_MARGIN_UNITS; + int16_t *dst = samplebuffer_append(sbuf, n); + for (int i = 0; i < n; i++) + dst[i] = lc_sample(unrolled ? lc_wrap(wpos + i) : wpos + i); + wlen -= n; + wpos += n; + } +} + +// The case that turned up the bug: instrument 1 of Arcade_S900.xm, a 16-bit +// sample of about a thousand samples with a short forward loop at the end. +// Only the loop fits the samplebuffer, so the pin happens on the fly, when +// playback first reaches the loop point. +static waveform_t lc_wave_loop = { + .name = "loopcache", .bits = 16, .channels = 1, .frequency = 32000, + .len = 1066, .loop_len = 277, .read = lc_read, +}; + +// Short enough for the whole waveform to be pinned, attack included, which the +// mixer does upfront rather than at the loop point. +static waveform_t lc_wave_whole = { + .name = "loopcache-whole", .bits = 16, .channels = 1, .frequency = 32000, + .len = 400, .loop_len = 100, .read = lc_read, +}; + +static waveform_t lc_wave_unrolled = { + .name = "loopcache-unrolled", .bits = 16, .channels = 1, .frequency = 32000, + .len = 1 << 20, .read = lc_read, +}; + +static bool test_mixer_loop_cache(waveform_t *wave, float freq) +{ + int nsamples = 4096; + int16_t *ref = malloc(nsamples * 4); + lc_len = wave->len; + lc_loop_len = wave->loop_len; + lc_oob = false; + + sv_start(&lc_wave_unrolled, 0, freq); + sv_mix(nsamples); + memcpy(ref, sv_out, nsamples * 4); + + sv_start(wave, 0, freq); + sv_mix(nsamples); + + bool ok = true; + if (lc_oob) { + printf("FAILED %s: read past the end of the waveform\n", wave->name); + ok = false; + } + // Same tolerance as sv_check_lr: the two runs settle their volume ramps + // independently. + for (int i = 0, bad = 0; i < nsamples * 2 && bad < 4; i++) { + int d = ref[i] - sv_out[i]; if (d < 0) d = -d; + if (d > 32) { + printf("FAILED %s: sample %d: %d, expected %d (freq=%d)\n", + wave->name, i/2, sv_out[i], ref[i], (int)freq); + ok = false; + bad++; + } + } + + free(ref); + return ok; +} + +////////////////////////////////////////////////////////////////////////////// +// Baseline mixer scenarios + mixer_ch_set_loop matrix +// +// Synthetic asset: rising-ramp intro + square-wave loop + descending release. +// Variants cover PCM/VADPCM × resident/streamed × mono/stereo × cached/tiny, +// and note-off before / inside / near loop_end / just after a wrap. +////////////////////////////////////////////////////////////////////////////// + +// Longer than sv_start's volume-ramp warmup (2048), so a oneshot is still +// playing when the measured capture begins. +#define BL_INTRO 2048 +#define BL_LOOP 512 +#define BL_RELEASE 256 // shorter than MIXER_MAX_SAMPLES_PER_ROUND +#define BL_LOOP_END (BL_INTRO + BL_LOOP) +#define BL_LEN (BL_LOOP_END + BL_RELEASE) +#define BL_TINY_LOOP 32 // shorter than MIXER_LOOP_OVERREAD +#define BL_PIN_INTRO 200 +#define BL_PIN_LOOP 100 // whole waveform pins in the samplebuffer +#define BL_PIN_REL 80 +#define BL_PIN_END (BL_PIN_INTRO + BL_PIN_LOOP) +#define BL_PIN_LEN (BL_PIN_END + BL_PIN_REL) +#define BL_AMP 16000 +#define BL_SQ 16 +#define BL_FREQ 44100 + +static int16_t bl_sample_geom(int pos, int intro, int loop, int release) +{ + int loop_end = intro + loop, len = loop_end + release; + if (pos < intro) + return (int16_t)(-BL_AMP + (int32_t)(2 * BL_AMP) * pos / intro); + if (pos < loop_end) { + int i = pos - intro; + return (i % BL_SQ < BL_SQ / 2) ? BL_AMP : (int16_t)-BL_AMP; + } + if (pos < len) { + int i = pos - loop_end; + return (int16_t)(BL_AMP - (int32_t)(2 * BL_AMP) * i / release); + } + return 0; +} + +static int16_t bl_sample(int pos) { + return bl_sample_geom(pos, BL_INTRO, BL_LOOP, BL_RELEASE); +} +static int16_t bl_tiny_sample(int pos) { + return bl_sample_geom(pos, BL_INTRO, BL_TINY_LOOP, BL_RELEASE); +} +static int16_t bl_pin_sample(int pos) { + return bl_sample_geom(pos, BL_PIN_INTRO, BL_PIN_LOOP, BL_PIN_REL); +} + +static int bl_play(int pos, int loop_end, int loop_len) +{ + // Hermite with a null fraction returns the second tap, one past pos. + pos++; + if (loop_len && pos >= loop_end) + pos = loop_end - loop_len + (pos - loop_end) % loop_len; + return pos; +} + +static void bl_read_with(samplebuffer_t *sbuf, int wpos, int wlen, + int16_t (*sample)(int), int ch) +{ + while (wlen > 0) { + int n = wlen < SAMPLEBUFFER_MARGIN_UNITS ? wlen : SAMPLEBUFFER_MARGIN_UNITS; + int16_t *dst = samplebuffer_append(sbuf, n); + for (int i = 0; i < n; i++) { + int16_t s = sample(wpos + i); + if (ch == 2) dst[2*i] = dst[2*i+1] = s; + else dst[i] = s; + } + wlen -= n; + wpos += n; + } +} + +static void bl_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + (void)ctx; (void)seeking; + bl_read_with(sbuf, wpos, wlen, bl_sample, 1); +} + +static void bl_tiny_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + (void)ctx; (void)seeking; + bl_read_with(sbuf, wpos, wlen, bl_tiny_sample, 1); +} + +static void bl_read_stereo(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + (void)ctx; (void)seeking; + bl_read_with(sbuf, wpos, wlen, bl_sample, 2); +} + +static void bl_pin_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + (void)ctx; (void)seeking; + bl_read_with(sbuf, wpos, wlen, bl_pin_sample, 1); +} + +static waveform_t bl_wave_oneshot_res = { + .name = "bl-oneshot-res", .bits = 16, .channels = 1, .frequency = BL_FREQ, + .len = BL_LEN, +}; + +// Terminal loop (loop_end == 0 ⇒ len). Resident overread past len is padded +// with loop-start samples, as wav64 preload does. +static waveform_t bl_wave_loop_res = { + .name = "bl-loop-res", .bits = 16, .channels = 1, .frequency = BL_FREQ, + .len = BL_LOOP_END, .loop_len = BL_LOOP, +}; + +// Resident sustain + release. Overread past loop_end bleeds into the release +// while looping (WAV64 preload of the rotated prefix is a later PR); this +// asset is only used for a mid-loop note-off that never wraps after disable. +static waveform_t bl_wave_loop_res_rel = { + .name = "bl-loop-res-rel", .bits = 16, .channels = 1, .frequency = BL_FREQ, + .len = BL_LEN, .loop_len = BL_LOOP, .loop_end = BL_LOOP_END, +}; + +static waveform_t bl_wave_oneshot_stream = { + .name = "bl-oneshot-stream", .bits = 16, .channels = 1, .frequency = BL_FREQ, + .len = BL_LEN, .read = bl_read, +}; + +// Sustain loop + release tail (streamed): loop_end < len. Loop is large enough +// that the mixer keeps streaming it (non-cached). +static waveform_t bl_wave_loop_stream = { + .name = "bl-loop-stream", .bits = 16, .channels = 1, .frequency = BL_FREQ, + .len = BL_LEN, .loop_len = BL_LOOP, .loop_end = BL_LOOP_END, .read = bl_read, +}; + +static waveform_t bl_wave_loop_stereo = { + .name = "bl-loop-stereo", .bits = 16, .channels = 2, .frequency = BL_FREQ, + .len = BL_LEN, .loop_len = BL_LOOP, .loop_end = BL_LOOP_END, .read = bl_read_stereo, +}; + +// Loop shorter than MIXER_LOOP_OVERREAD: overread refill wraps the tiny loop +// more than once per fetch. +static waveform_t bl_wave_tiny = { + .name = "bl-tiny-loop", .bits = 16, .channels = 1, .frequency = BL_FREQ, + .len = BL_INTRO + BL_TINY_LOOP + BL_RELEASE, + .loop_len = BL_TINY_LOOP, .loop_end = BL_INTRO + BL_TINY_LOOP, .read = bl_tiny_read, +}; + +// Short enough that the mixer pins the whole waveform in the samplebuffer. +static waveform_t bl_wave_pin = { + .name = "bl-pin-loop", .bits = 16, .channels = 1, .frequency = BL_FREQ, + .len = BL_PIN_LEN, .loop_len = BL_PIN_LOOP, .loop_end = BL_PIN_END, + .read = bl_pin_read, +}; + +// Resident copy of the first @p len samples. The RSP overreads +// MIXER_LOOP_OVERREAD past the end, and for a looping waveform those samples +// must be the ones at the loop start, as a wav64 preload lays them out. +static int16_t *bl_resident(int len, int loop_len) +{ + int16_t *mem = malloc_uncached((len + MIXER_LOOP_OVERREAD) * 2); + for (int i = 0; i < len + MIXER_LOOP_OVERREAD; i++) + mem[i] = bl_sample(i < len || !loop_len ? i + : len - loop_len + (i - len) % loop_len); + return mem; +} + +static void bl_init(void) +{ + if (bl_wave_oneshot_res.mem) + return; + bl_wave_oneshot_res.mem = bl_resident(BL_LEN, 0); + bl_wave_loop_res.mem = bl_resident(BL_LOOP_END, BL_LOOP); + // Full asset including release; no loop-start pad at loop_end (see above). + bl_wave_loop_res_rel.mem = bl_resident(BL_LEN, 0); +} + +// Sample-by-sample check at the output rate: Hermite is an identity there. +// @p loop_end doubles as the physical end when there is no loop. +// @p sample is NULL for the default bl_sample shape. +static bool bl_check_exact(const char *tag, int start_pos, int nsamples, + int loop_end, int loop_len, int16_t (*sample)(int)) +{ + if (!sample) sample = bl_sample; + int nbad = 0, first = -1, maxdiff = 0, peak = 0; + for (int i = 0; i < nsamples; i++) { + int p = bl_play(start_pos + i, loop_end, loop_len); + if (!loop_len && p >= loop_end) + break; + int want = sample(p) / 2; + int got = sv_out[i * 2]; + int d = got - want; if (d < 0) d = -d; + if (d > 64) { + nbad++; + if (first < 0) first = i; + if (d > maxdiff) maxdiff = d; + } + int a = got < 0 ? -got : got; + if (a > peak) peak = a; + } + if (nbad) { + printf("FAILED %s: %d/%d samples off (max %d) from %d\n", + tag, nbad, nsamples, maxdiff, first); + for (int i = first; i < first + 6 && i < nsamples; i++) + printf(" [%d] play=%d got %d want %d\n", i, + bl_play(start_pos + i, loop_end, loop_len), + sv_out[i * 2], + sample(bl_play(start_pos + i, loop_end, loop_len)) / 2); + return false; + } + if (peak < 4096) { + printf("FAILED %s: output is silent (peak %d)\n", tag, peak); + return false; + } + return true; +} + +static int bl_wave_loop_end(const waveform_t *wave) { + return wave->loop_end ? wave->loop_end : wave->len; +} + +static bool test_mixer_baseline_pcm(waveform_t *wave, int startsample, int nsamples) +{ + bl_init(); + sv_silence(); + mixer_ch_set_limits(SV_CHANNEL, 0, 48000, 0); + sv_start(wave, startsample, BL_FREQ); + int pos = (int)mixer_ch_get_pos(SV_CHANNEL); + sv_mix(nsamples); + return bl_check_exact(wave->name, pos, nsamples, + bl_wave_loop_end(wave), wave->loop_len, NULL); +} + +// Frame a test explicitly asked for with #mixer_ch_set_pos, which is the one +// case where the caller owns the restriction (and a real asset would have to +// be built with a seek point there). +static int slv_seek_asked = -1; + +// Where the note-off (#mixer_ch_set_loop false) lands relative to the loop. +typedef enum { + SL_BEFORE, // still in the intro + SL_INSIDE, // mid-loop + SL_NEAR_END, // a few samples before loop_end + SL_AFTER_WRAP, // just after wrapping back to loop_start +} sl_when_t; + +// Disable the sustain loop at @p when, then verify a seamless one-shot through +// the release tail and a silent stop. PCM exact match when @p sample is set; +// otherwise only L/R identity + audible + idle (VADPCM / stereo stress). +static bool test_mixer_set_loop(waveform_t *wave, sl_when_t when, + int16_t (*sample)(int)) +{ + int loop_end = bl_wave_loop_end(wave); + int loop_start = loop_end - wave->loop_len; + int start, target; + char tag[64]; + const char *wname[] = { "before", "inside", "near_end", "after_wrap" }; + + switch (when) { + case SL_BEFORE: + start = loop_start > 64 ? loop_start - 64 : 0; + target = start + 16; + break; + case SL_INSIDE: + start = loop_start + 8; + target = loop_start + wave->loop_len / 2; + break; + case SL_NEAR_END: + // Land a few samples before loop_end; mix in steps of 2 so a round + // cannot jump past the window and wrap. + start = loop_end - 8; + if (start < loop_start) start = loop_start; + target = loop_end - 4; + break; + case SL_AFTER_WRAP: + start = loop_end - 6; + if (start < loop_start) start = loop_start; + target = loop_start + 16; // after the wrap + break; + } + snprintf(tag, sizeof(tag), "set_loop %s/%s", wave->name, wname[when]); + + bl_init(); + sv_silence(); + mixer_ch_set_limits(SV_CHANNEL, 0, 48000, 0); + // Settle the volume ramp at step 0 so the playhead stays on @p start + // (sv_start's 2048-sample warmup would walk past the intro / near-end). + mixer_ch_play(SV_CHANNEL, wave); + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + slv_seek_asked = start / 16; + mixer_ch_set_pos(SV_CHANNEL, start); + mixer_ch_set_freq(SV_CHANNEL, 0); + sv_mix(2048); + mixer_ch_set_freq(SV_CHANNEL, BL_FREQ); + + int prev = start; + for (int guard = 0; guard < 256; guard++) { + int pos = (int)mixer_ch_get_pos(SV_CHANNEL); + if (when == SL_AFTER_WRAP) { + if (pos < prev && pos >= loop_start && pos < loop_start + 64) + break; + } else if (when == SL_NEAR_END) { + if (pos >= target && pos < loop_end) break; + } else if (when == SL_BEFORE) { + break; // already parked in the intro + } else if (pos >= target && pos < loop_end) { + break; + } + prev = pos; + // Steps of 2: mixer_poll requires an even count, and near loop_end a + // larger step would wrap past the NEAR_END window. + sv_mix(when == SL_INSIDE ? 32 : 2); + } + + int pos = (int)mixer_ch_get_pos(SV_CHANNEL); + if (when == SL_BEFORE && !(pos < loop_start)) { + printf("FAILED %s: pos %d not in intro [0,%d)\n", tag, pos, loop_start); + return false; + } + if (when == SL_INSIDE && !(pos >= loop_start && pos < loop_end)) { + printf("FAILED %s: pos %d not inside the loop\n", tag, pos); + return false; + } + if (when == SL_NEAR_END && !(pos >= loop_end - 16 && pos < loop_end)) { + printf("FAILED %s: pos %d not near loop_end=%d\n", tag, pos, loop_end); + return false; + } + if (when == SL_AFTER_WRAP && !(pos >= loop_start && pos < loop_start + 64)) { + printf("FAILED %s: pos %d not just after wrap (loop_start=%d)\n", + tag, pos, loop_start); + return false; + } + + // From here on nothing asked for a jump: every seek the mixer makes has to + // be one the asset could serve. + slv_seek_asked = -1; + mixer_ch_set_loop(SV_CHANNEL, false); + pos = (int)mixer_ch_get_pos(SV_CHANNEL); + int remain = wave->len - pos; + if (remain < 8) { + printf("FAILED %s: only %d samples left after disable\n", tag, remain); + return false; + } + // mixer_poll rejects odd counts; a release of 37 samples is the point. + sv_mix((remain + 65) & ~1); + + if (sample) { + char rtag[80]; + snprintf(rtag, sizeof(rtag), "%s (release)", tag); + int ncheck = remain < 4096 ? remain : 4096; + if (!bl_check_exact(rtag, pos, ncheck, wave->len, 0, sample)) + return false; + } else { + // Audible release; stereo planes must stay together. + int peak = 0, n = remain < 512 ? remain : 512; + for (int i = 0; i < n * 2; i++) { + int a = sv_out[i] < 0 ? -sv_out[i] : sv_out[i]; + if (a > peak) peak = a; + } + if (peak < 1024) { + printf("FAILED %s: silent release (peak %d)\n", tag, peak); + return false; + } + } + if (wave->channels == 2) { + // Skip the last overread past sample_end: silence padding of the two + // VADPCM planes can settle one sample apart there. + int n = remain < 512 ? remain : 512; + if (n > 8) n -= 8; + if (!sv_check_lr(tag, n)) + return false; + } + + sv_mix(2048); + if (mixer_ch_playing(SV_CHANNEL)) { + printf("FAILED %s: still playing after release (pos=%.1f len=%d)\n", + tag, mixer_ch_get_pos(SV_CHANNEL), wave->len); + return false; + } + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// VADPCM sustain + release (streamed / resident) +// +// Release length is deliberately not a multiple of 16, so the last frame is +// partial — the case WAV64 v10 has to get right at sample_end. +////////////////////////////////////////////////////////////////////////////// + +#define SLV_INTRO 128 +#define SLV_LOOP 64 +#define SLV_RELEASE 37 +#define SLV_LOOP_END (SLV_INTRO + SLV_LOOP) +#define SLV_LEN (SLV_LOOP_END + SLV_RELEASE) +#define SLV_FRAMES ((SLV_LEN + 15) / 16) + +static uint8_t *slv_frames; +static int16_t *slv_ref; +static wav64_vadpcm_vector_t *slv_codebook; +static wav64_vadpcm_vector_t *slv_loop_state; +static waveform_vadpcm_t slv_codec; +static waveform_vadpcm_t slv_codec_stereo; + +// A real VADPCM asset can only re-seed its decoder on the points audioconv64 +// wrote into the file: the start of the waveform and its loop point. Deriving +// the state from the reference PCM would let the mixer seek anywhere, and hide +// the seeks that a wav64 could not serve. +static void slv_check_seek(int wpos) +{ + assertf(wpos == 0 || wpos == SLV_INTRO / 16 || wpos == slv_seek_asked, + "VADPCM seek to frame %d, which is not a seek point", wpos); +} + +static void slv_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + (void)ctx; + if (seeking) { + slv_check_seek(wpos); + wav64_state_vadpcm_t *st = sbuf->state; + for (int i = 0; i < 8; i++) { + int p = wpos * 16 - 8 + i; + st->state[0].v[i] = p >= 0 ? slv_ref[p] : 0; + } + } + int fb = VADPCM_FRAME_BYTES(4); + while (wlen > 0) { + int n = wlen < SAMPLEBUFFER_MARGIN_UNITS ? wlen : SAMPLEBUFFER_MARGIN_UNITS; + memcpy(samplebuffer_append(sbuf, n), slv_frames + wpos * fb, n * fb); + wlen -= n; + wpos += n; + } +} + +// Stereo VADPCM with identical planes: L/R must stay together across a note-off. +static void slv_read_stereo(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + samplebuffer_t *sbuf_r = sbuf + 1; + (void)ctx; + if (seeking && sbuf->widx == 0) { + slv_check_seek(wpos); + wav64_state_vadpcm_t *st = sbuf->state; + for (int i = 0; i < 8; i++) { + int p = wpos * 16 - 8 + i; + st->state[0].v[i] = st->state[1].v[i] = p >= 0 ? slv_ref[p] : 0; + } + samplebuffer_flush(sbuf_r); + sbuf_r->wpos = wpos; + sbuf_r->head = sbuf->head; + } + int fb = VADPCM_FRAME_BYTES(4); + while (wlen > 0) { + int n = wlen < SAMPLEBUFFER_MARGIN_UNITS ? wlen : SAMPLEBUFFER_MARGIN_UNITS; + uint8_t *dl = samplebuffer_append(sbuf, n); + uint8_t *dr = samplebuffer_append(sbuf_r, n); + memcpy(dl, slv_frames + wpos * fb, n * fb); + memcpy(dr, dl, n * fb); + sbuf_r->wnext = sbuf_r->wpos + sbuf_r->widx; + wlen -= n; + wpos += n; + } +} + +static waveform_t slv_wave_stream = { + .name = "slv-stream", .bits = 16, .channels = 1, .frequency = BL_FREQ, + .len = SLV_LEN, .loop_len = SLV_LOOP, .loop_end = SLV_LOOP_END, + .read = slv_read, .format = WAVEFORM_FORMAT_VADPCM, + .codec = &slv_codec, .state_size = sizeof(wav64_state_vadpcm_t), +}; + +static waveform_t slv_wave_stereo = { + .name = "slv-stereo", .bits = 16, .channels = 2, .frequency = BL_FREQ, + .len = SLV_LEN, .loop_len = SLV_LOOP, .loop_end = SLV_LOOP_END, + .read = slv_read_stereo, .format = WAVEFORM_FORMAT_VADPCM, + .codec = &slv_codec_stereo, .state_size = sizeof(wav64_state_vadpcm_t), +}; + +static waveform_t slv_wave_res; +static uint8_t *slv_mem_res; + +static void slv_init(void) +{ + if (slv_frames) + return; + int fb = VADPCM_FRAME_BYTES(4); + slv_codebook = malloc_uncached(2 * VADPCM_CODEBOOK_STRIDE); + memset(slv_codebook, 0, 2 * VADPCM_CODEBOOK_STRIDE); + gen_codebook(slv_codebook); + memcpy((uint8_t *)slv_codebook + VADPCM_CODEBOOK_STRIDE, slv_codebook, + 8 * sizeof(wav64_vadpcm_vector_t)); + slv_frames = malloc(SLV_FRAMES * fb); + for (int f = 0; f < SLV_FRAMES; f++) + sv_gen_frame(slv_frames + f * fb, f + 9000, 4); + slv_ref = malloc(SLV_FRAMES * 16 * 2); + wav64_vadpcm_vector_t state = {0}; + vadpcm_decode(NPREDICTORS, ORDER, slv_codebook, &state, + SLV_FRAMES, slv_ref, slv_frames, 4); + slv_loop_state = malloc_uncached(2 * sizeof(*slv_loop_state)); + for (int i = 0; i < 8; i++) + slv_loop_state[0].v[i] = slv_loop_state[1].v[i] = slv_ref[SLV_INTRO - 8 + i]; + int16_t *taps = (int16_t *)((uint8_t *)slv_codebook + 128); + taps[0] = slv_ref[SLV_INTRO + 0]; + taps[1] = slv_ref[SLV_INTRO + 1]; + taps[2] = slv_ref[SLV_INTRO + 2]; + memcpy((uint8_t *)slv_codebook + VADPCM_CODEBOOK_STRIDE + 128, taps, 6); + slv_codec.codebook = slv_codebook; + slv_codec.loop_state = slv_loop_state; + slv_codec.bits = 4; + slv_codec_stereo = slv_codec; + + slv_mem_res = malloc_uncached(SLV_FRAMES * fb + 64); + memcpy(slv_mem_res, slv_frames, SLV_FRAMES * fb); + slv_wave_res = slv_wave_stream; + slv_wave_res.name = "slv-res"; + slv_wave_res.mem = slv_mem_res; + slv_wave_res.__uuid = 0; +} + +static int16_t slv_sample(int pos) +{ + if (pos < 0 || pos >= SLV_FRAMES * 16) return 0; + return slv_ref[pos]; +} + +// Switch the playback rate mid-note: the playhead must advance at the rate +// that is in force for each half, and both halves must stay audible. +// Use a oneshot so a full-rate block of N samples cannot land on the same +// wrapped position as a looping waveform would after exactly one loop_len. +static bool test_mixer_freq_change(void) +{ + const int n1 = 512, n2 = 512; + bl_init(); + sv_silence(); + mixer_ch_set_limits(SV_CHANNEL, 0, 48000, 0); + // Settle the volume filter at step 0 so warmup does not consume the oneshot + // (sv_start's 2048-sample warmup would land in the quiet middle of the ramp). + mixer_ch_play(SV_CHANNEL, &bl_wave_oneshot_res); + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + mixer_ch_set_pos(SV_CHANNEL, BL_INTRO - 64); + mixer_ch_set_freq(SV_CHANNEL, 0); + sv_mix(2048); + mixer_ch_set_freq(SV_CHANNEL, 22050); + + double pos0 = mixer_ch_get_pos(SV_CHANNEL); + sv_mix(n1); + int peak = 0; + for (int i = 0; i < n1 * 2; i++) { + int a = sv_out[i] < 0 ? -sv_out[i] : sv_out[i]; + if (a > peak) peak = a; + } + if (peak < 4096) { + printf("FAILED freq change: silent at half rate (peak %d)\n", peak); + return false; + } + + double pos1 = mixer_ch_get_pos(SV_CHANNEL); + double advanced = pos1 - pos0; + if (advanced < n1 / 2.0 - 2 || advanced > n1 / 2.0 + 2) { + printf("FAILED freq change: half-rate advance %.2f, expected ~%d\n", + advanced, n1 / 2); + return false; + } + + mixer_ch_set_freq(SV_CHANNEL, 44100); + double pos2 = mixer_ch_get_pos(SV_CHANNEL); + sv_mix(n2); + peak = 0; + for (int i = 0; i < n2 * 2; i++) { + int a = sv_out[i] < 0 ? -sv_out[i] : sv_out[i]; + if (a > peak) peak = a; + } + if (peak < 4096) { + printf("FAILED freq change: silent at full rate (peak %d)\n", peak); + return false; + } + + advanced = mixer_ch_get_pos(SV_CHANNEL) - pos2; + if (advanced < n2 - 2 || advanced > n2 + 2) { + printf("FAILED freq change: full-rate advance %.2f, expected ~%d\n", + advanced, n2); + return false; + } + return true; +} + +// A frequency ramp is piecewise-constant per mix round: the waveform position +// must advance faster in the second half than in the first. Use a oneshot so +// the playhead cannot wrap mid-measurement. +static bool test_mixer_freq_ramp(void) +{ + const int nramp = 2048, half = nramp / 2; + + bl_init(); + sv_silence(); + mixer_ch_set_limits(SV_CHANNEL, 0, 48000, 0); + mixer_ch_play(SV_CHANNEL, &bl_wave_oneshot_res); + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + mixer_ch_set_pos(SV_CHANNEL, 0); + mixer_ch_set_freq(SV_CHANNEL, BL_FREQ / 2); + sv_mix(128); + + mixer_ch_set_freq_ramp(SV_CHANNEL, BL_FREQ, nramp, mixer_ramp_linear, 0); + double p0 = mixer_ch_get_pos(SV_CHANNEL); + sv_mix(half); + double p1 = mixer_ch_get_pos(SV_CHANNEL); + sv_mix(half); + double p2 = mixer_ch_get_pos(SV_CHANNEL); + + if (!mixer_ch_playing(SV_CHANNEL)) { + printf("FAILED freq ramp: oneshot ended early (pos %.1f)\n", p2); + return false; + } + + double a0 = p1 - p0, a1 = p2 - p1; + if (a0 >= a1) { + printf("FAILED freq ramp: first-half advance %.2f >= second-half %.2f\n", + a0, a1); + return false; + } + // Ideal mean rates 0.625 and 0.875 of output rate; allow round granularity. + if (a0 < half * 0.50 || a0 > half * 0.75 || + a1 < half * 0.75 || a1 > half * 1.05) { + printf("FAILED freq ramp: advances %.2f / %.2f (half=%d)\n", a0, a1, half); + return false; + } + + double p3 = mixer_ch_get_pos(SV_CHANNEL); + sv_mix(256); + double a2 = mixer_ch_get_pos(SV_CHANNEL) - p3; + if (a2 < 256 - 4 || a2 > 256 + 4) { + printf("FAILED freq ramp: post-ramp advance %.2f, expected ~256\n", a2); + return false; + } + return true; +} + +// Stop while the playhead is still inside the waveform, not at the end. +static bool test_mixer_stop_mid(void) +{ + bl_init(); + sv_silence(); + mixer_ch_set_limits(SV_CHANNEL, 0, 48000, 0); + sv_start(&bl_wave_oneshot_stream, 0, BL_FREQ); + sv_mix(128); + + double pos = mixer_ch_get_pos(SV_CHANNEL); + if (pos < 32 || pos >= BL_LEN - 32) { + printf("FAILED stop mid: position %.1f not inside the waveform\n", pos); + return false; + } + if (!mixer_ch_playing(SV_CHANNEL)) { + printf("FAILED stop mid: channel already idle at position %.1f\n", pos); + return false; + } + + mixer_ch_stop(SV_CHANNEL); + if (mixer_ch_playing(SV_CHANNEL)) { + printf("FAILED stop mid: channel still playing after stop\n"); + return false; + } + + // Let the volume ramp decay, then the output must be silent. + sv_mix(2048); + for (int i = 0; i < 8; i++) + sv_mix(4096); + for (int i = 0; i < 4096; i++) { + if (sv_out[i * 2] || sv_out[i * 2 + 1]) { + printf("FAILED stop mid: still audible at sample %d: %d/%d\n", + i, sv_out[i * 2], sv_out[i * 2 + 1]); + return false; + } + } + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// Volume ramps +// +// The loop of the baseline asset is a square wave of constant absolute +// amplitude, so playing it at the output rate makes the envelope of the +// capture the channel volume itself, sample by sample. +////////////////////////////////////////////////////////////////////////////// + +// Deviation of the envelope from a straight line, and from the volume that was +// asked for. The ucode walks the volume one output sample at a time, in steps +// it recomputes every four of them, so the envelope follows the line it is +// meant to within the rounding of those steps. +#define VR_TOL_LINE 64 +#define VR_TOL_LEVEL 128 + +static int vr_amp(int i) { int a = sv_out[i*2]; return a < 0 ? -a : a; } +static int vr_amp_r(int i) { int a = sv_out[i*2+1]; return a < 0 ? -a : a; } + +static void vr_start(waveform_t *wave, float lvol, float rvol) +{ + bl_init(); + sv_silence(); + mixer_ch_set_limits(SV_CHANNEL, 0, 48000, 0); + sv_start(wave, BL_INTRO, BL_FREQ); + mixer_ch_set_vol(SV_CHANNEL, lvol, rvol); + sv_mix(2048); // let the declick ramp settle +} + +// A linear ramp comes out as a straight line whatever the staircase of the +// ucode does to it, so the capture is checked against the line through two of +// its own interior points: a ramp that moves once per mix round (or once per +// poll) is caught this way. The endpoints of that line are then checked +// against the volumes that were asked for. +static bool vr_check_linear(const char *tag, int nramp, float v0, float v1, + int (*amp)(int)) +{ + int i1 = nramp/4, i2 = nramp*3/4; + int a1 = amp(i1), a2 = amp(i2); + int worst = 0, wi = -1; + + for (int i = nramp/8; i <= nramp*7/8; i++) { + int want = a1 + (int)((int64_t)(a2 - a1) * (i - i1) / (i2 - i1)); + int d = amp(i) - want; if (d < 0) d = -d; + if (d > worst) { worst = d; wi = i; } + } + if (worst > VR_TOL_LINE) { + printf("FAILED %s: envelope off its own line by %d at %d\n", tag, worst, wi); + printf(" [%d]=%d [%d]=%d [%d]=%d\n", i1, a1, wi, amp(wi), i2, a2); + return false; + } + for (int k = 0; k < 2; k++) { + int i = k ? i2 : i1, a = k ? a2 : a1; + int want = (int)(BL_AMP * (v0 + (v1 - v0) * i / (float)nramp)); + int d = a - want; if (d < 0) d = -d; + if (d > VR_TOL_LEVEL) { + printf("FAILED %s: envelope is %d at %d, expected %d\n", tag, a, i, want); + return false; + } + } + return true; +} + +static bool vr_check_level(const char *tag, int from, int to, float v, + int (*amp)(int)) +{ + int want = (int)(BL_AMP * v), worst = 0, wi = -1; + for (int i = from; i < to; i++) { + int d = amp(i) - want; if (d < 0) d = -d; + if (d > worst) { worst = d; wi = i; } + } + if (worst > VR_TOL_LEVEL) { + printf("FAILED %s: volume %d at %d, expected %d\n", tag, amp(wi), wi, want); + return false; + } + return true; +} + +// A ramp spanning several rounds and several polls must come out as one +// straight line, with no step at the boundaries. +static bool test_mixer_vol_ramp(waveform_t *wave) +{ + const int nramp = 4096, ntail = 2048; + + vr_start(wave, 0.5f, 0.5f); + mixer_ch_set_vol_ramp(SV_CHANNEL, 0.0f, 0.0f, nramp); + sv_mix(nramp + ntail); + if (!vr_check_linear("vol ramp down", nramp, 0.5f, 0.0f, vr_amp)) + return false; + if (!vr_check_level("vol ramp down", nramp + 512, nramp + ntail, 0.0f, vr_amp)) + return false; + + vr_start(wave, 0.0f, 0.0f); + mixer_ch_set_vol_ramp(SV_CHANNEL, 0.75f, 0.75f, nramp); + sv_mix(nramp + ntail); + if (!vr_check_linear("vol ramp up", nramp, 0.0f, 0.75f, vr_amp)) + return false; + if (!vr_check_level("vol ramp up", nramp + 512, nramp + ntail, 0.75f, vr_amp)) + return false; + + // The two sides ramp independently, and on a stereo waveform they are two + // separate MIX_CHANNELs that must stay in step. + vr_start(wave, 0.5f, 0.5f); + mixer_ch_set_vol_ramp(SV_CHANNEL, 0.0f, 1.0f, nramp); + sv_mix(nramp + ntail); + if (!vr_check_linear("vol ramp pan L", nramp, 0.5f, 0.0f, vr_amp)) + return false; + if (!vr_check_linear("vol ramp pan R", nramp, 0.5f, 1.0f, vr_amp_r)) + return false; + return true; +} + +// A ramp shorter than a mix round still has to run: what the ucode is given +// is an increment per block of 4 samples, not one per round. +static bool test_mixer_vol_ramp_short(void) +{ + vr_start(&bl_wave_loop_res, 0.5f, 0.5f); + mixer_ch_set_vol_ramp(SV_CHANNEL, 0.0f, 0.0f, 64); + sv_mix(1024); + // Well before the round the ramp lives in is over. + if (vr_amp(200) > BL_AMP / 8) { + printf("FAILED vol ramp short: still at %d after 200 samples\n", vr_amp(200)); + return false; + } + return vr_check_level("vol ramp short", 512, 1024, 0.0f, vr_amp); +} + +// A new ramp starts from the volume the channel has now, not from the target +// of the ramp it replaces; #mixer_ch_set_vol cancels it outright. +static bool test_mixer_vol_ramp_replace(void) +{ + const int nramp = 8192; + + vr_start(&bl_wave_loop_res, 1.0f, 1.0f); + mixer_ch_set_vol_ramp(SV_CHANNEL, 0.0f, 0.0f, nramp); + sv_mix(nramp/2); + if (!vr_check_level("vol ramp replace", nramp/2 - 64, nramp/2, 0.5f, vr_amp)) + return false; + mixer_ch_set_vol_ramp(SV_CHANNEL, 1.0f, 1.0f, 4096); + sv_mix(4096 + 1024); + if (!vr_check_linear("vol ramp replace", 4096, 0.5f, 1.0f, vr_amp)) + return false; + if (!vr_check_level("vol ramp replace", 4096 + 512, 4096 + 1024, 1.0f, vr_amp)) + return false; + + vr_start(&bl_wave_loop_res, 1.0f, 1.0f); + mixer_ch_set_vol_ramp(SV_CHANNEL, 0.0f, 0.0f, nramp); + sv_mix(nramp/2); + mixer_ch_set_vol(SV_CHANNEL, 0.25f, 0.25f); + sv_mix(4096); + // The cancelled ramp must not resume, now or later. + if (!vr_check_level("vol ramp cancel", 512, 4096, 0.25f, vr_amp)) + return false; + sv_mix(4096); + return vr_check_level("vol ramp cancel", 0, 4096, 0.25f, vr_amp); +} + +// The ucode is given a slope and the value it is heading for, and clamps to +// it: a ramp may therefore end anywhere inside a round, at a sample that is +// not even a multiple of the 4 the slope is recomputed at. What this looks for +// is the volume sailing past its target, which is what would happen if the +// slope kept being applied to the end of the round. +static bool test_mixer_vol_ramp_land(void) +{ + static const int durations[] = { 13, 77, 300, 1001 }; + + for (int i = 0; i < 4; i++) { + int n = durations[i]; + + vr_start(&bl_wave_loop_res, 1.0f, 1.0f); + mixer_ch_set_vol_ramp(SV_CHANNEL, 0.0f, 0.0f, n); + sv_mix(2048); + if (!vr_check_level("vol ramp land down", n + 8, 2048, 0.0f, vr_amp)) + return false; + + // Upwards, from and to a volume that leaves room on both sides, so + // that an overshoot shows up instead of saturating out of sight. + vr_start(&bl_wave_loop_res, 0.25f, 0.25f); + mixer_ch_set_vol_ramp(SV_CHANNEL, 0.5f, 0.5f, n); + sv_mix(2048); + if (!vr_check_level("vol ramp land up", n + 8, 2048, 0.5f, vr_amp)) + return false; + } + return true; +} + +// Mono gain ramps are independent of L/R volume: a linear gain fade matches +// vol_ramp shape, a dB fade follows exp(-9.226·t/T) toward silence, and a +// mid-ramp set_vol scales the envelope without restarting it. +static bool test_mixer_gain_ramp(void) +{ + const int nramp = 4096, ntail = 1024; + + vr_start(&bl_wave_loop_res, 1.0f, 1.0f); + mixer_ch_set_gain_ramp(SV_CHANNEL, 0.0f, nramp, mixer_ramp_linear, 0); + sv_mix(nramp + ntail); + if (!vr_check_linear("gain ramp linear", nramp, 1.0f, 0.0f, vr_amp)) + return false; + if (!vr_check_level("gain ramp linear", nramp + 512, nramp + ntail, 0.0f, vr_amp)) + return false; + + vr_start(&bl_wave_loop_res, 1.0f, 1.0f); + mixer_ch_set_gain_ramp(SV_CHANNEL, 0.0f, nramp, mixer_ramp_exp, 0); + sv_mix(nramp + ntail); + // Piecewise-linear per round: check a few interior points against the + // SF2 silence curve, not every sample. + for (int k = 1; k <= 3; k++) { + int i = nramp * k / 4; + float g = expf(-9.226f * (float)i / (float)nramp); + int want = (int)(BL_AMP * g); + int d = vr_amp(i) - want; if (d < 0) d = -d; + if (d > VR_TOL_LEVEL * 2) { + printf("FAILED gain ramp db: amp %d at %d, expected %d\n", + vr_amp(i), i, want); + return false; + } + } + if (!vr_check_level("gain ramp db", nramp + 512, nramp + ntail, 0.0f, vr_amp)) + return false; + + // Vol can move under a running gain ramp without restarting it. + vr_start(&bl_wave_loop_res, 1.0f, 1.0f); + mixer_ch_set_gain_ramp(SV_CHANNEL, 0.0f, nramp, mixer_ramp_linear, 0); + sv_mix(nramp / 2); + int a_full = vr_amp(nramp / 2 - 32); + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + sv_mix(256); // past #MIXER_DECLICK_SAMPLES + int a_half = vr_amp(200); + // Must be quieter than before set_vol (gain also keeps falling), but not + // silent: a tight absolute curve is hard here because the RSP walks the + // product of two ramps as one linear chord per round. + if (a_half > a_full * 3 / 4 || a_half < a_full / 5) { + printf("FAILED gain×vol: amp %d after set_vol 0.5 (was %d)\n", + a_half, a_full); + return false; + } + sv_mix(nramp / 2 - 256 + ntail); + return vr_check_level("gain×vol", nramp / 2 - 256 + 512, + nramp / 2 - 256 + ntail, 0.0f, vr_amp); +} + +// The ramp moves at every output sample, and not once per group of eight: a +// steep ramp comes out as a line and not as a staircase. What this looks for +// is the jump between two neighbouring samples, which stepping once per group +// would make eight times the slope at every group boundary. +static bool test_mixer_vol_ramp_smooth(void) +{ + const int nramp = 256; + const int slope = BL_AMP / nramp; // envelope units per output sample + + vr_start(&bl_wave_loop_res, 0.0f, 0.0f); + mixer_ch_set_vol_ramp(SV_CHANNEL, 1.0f, 1.0f, nramp); + sv_mix(1024); + + int worst = 0, wi = -1; + for (int i = 1; i < nramp - 1; i++) { + int d = vr_amp(i) - vr_amp(i - 1); if (d < 0) d = -d; + if (d > worst) { worst = d; wi = i; } + } + if (worst > slope * 2) { + printf("FAILED vol ramp smooth: envelope jumps by %d at %d, slope is %d\n", + worst, wi, slope); + return false; + } + // ...and it is a ramp at all: an envelope that never moves would sail + // through the test above. + int mid = vr_amp(nramp/2); + if (mid < BL_AMP/4 || mid > BL_AMP*3/4) { + printf("FAILED vol ramp smooth: %d halfway, expected around %d\n", + mid, BL_AMP/2); + return false; + } + return true; +} + +// mixer_ch_set_vol walks to the new volume instead of stepping to it, which is +// what keeps a music player from clicking on every tick. +static bool test_mixer_declick(void) +{ + const int nd = 128; // MIXER_DECLICK_SAMPLES + + vr_start(&bl_wave_loop_res, 0.0f, 0.0f); + mixer_ch_set_vol(SV_CHANNEL, 1.0f, 1.0f); + sv_mix(1024); + + // Not a step: a quarter into the ramp the volume is still around a quarter. + int a = vr_amp(nd/4); + if (a > BL_AMP/2) { + printf("FAILED declick: %d at sample %d, expected around %d\n", + a, nd/4, BL_AMP/4); + return false; + } + // ...but over and done with well within a mix round. + return vr_check_level("declick", nd + 8, 1024, 1.0f, vr_amp); +} + +// Dolby Pro Logic II matrix encoding needs negative L/R coefficients so that +// a decoder can recover the surrounds from the stereo sum. A [0..1] clamp on +// the volume path zeros those signs and collapses surround into a quiet mono +// bleed — which is exactly the regression the volume-ramp work introduced. +static bool test_mixer_vol_dolby(void) +{ + // Same matrix constants as mixer_ch_set_vol_dolby. + const float ktot = 1.0f + 0.7071067811865476f + 0.8660254037844386f + 0.5f; + const float kan = 0.8660254037844386f / ktot; // ~0.282 + const float kbn = 0.5f / ktot; // ~0.163 + const float kfn = 1.0f / ktot; // ~0.325 + const int n = 1024; + + // Surround left alone: L ≈ -KAn, R ≈ +KBn — opposite phase, unequal levels. + vr_start(&bl_wave_loop_res, 0.0f, 0.0f); + mixer_ch_set_vol_dolby(SV_CHANNEL, 0, 0, 0, 1.0f, 0); + sv_mix(n); + if (!vr_check_level("dolby SL L", 256, n, kan, vr_amp)) + return false; + if (!vr_check_level("dolby SL R", 256, n, kbn, vr_amp_r)) + return false; + for (int i = 256; i < n; i++) { + int l = sv_out[i*2], r = sv_out[i*2+1]; + // Both buses carry the square wave; opposite signs are the phase cue. + if ((l < 0) == (r < 0) && (l > 256 || l < -256) && (r > 256 || r < -256)) { + printf("FAILED dolby SL: L and R same sign at %d (%d, %d)\n", i, l, r); + return false; + } + } + + // Surround right alone: L ≈ -KBn, R ≈ +KAn — same phase cue, levels swapped. + vr_start(&bl_wave_loop_res, 0.0f, 0.0f); + mixer_ch_set_vol_dolby(SV_CHANNEL, 0, 0, 0, 0, 1.0f); + sv_mix(n); + if (!vr_check_level("dolby SR L", 256, n, kbn, vr_amp)) + return false; + if (!vr_check_level("dolby SR R", 256, n, kan, vr_amp_r)) + return false; + for (int i = 256; i < n; i++) { + int l = sv_out[i*2], r = sv_out[i*2+1]; + if ((l < 0) == (r < 0) && (l > 256 || l < -256) && (r > 256 || r < -256)) { + printf("FAILED dolby SR: L and R same sign at %d (%d, %d)\n", i, l, r); + return false; + } + } + + // Front left alone stays positive on L and silent on R (no phase flip). + vr_start(&bl_wave_loop_res, 0.0f, 0.0f); + mixer_ch_set_vol_dolby(SV_CHANNEL, 1.0f, 0, 0, 0, 0); + sv_mix(n); + if (!vr_check_level("dolby FL L", 256, n, kfn, vr_amp)) + return false; + if (!vr_check_level("dolby FL R", 256, n, 0.0f, vr_amp_r)) + return false; + + // Negative volumes are first-class: set_vol itself can invert phase. + vr_start(&bl_wave_loop_res, 0.0f, 0.0f); + mixer_ch_set_vol(SV_CHANNEL, -0.5f, 0.5f); + sv_mix(n); + if (!vr_check_level("vol signed L", 256, n, 0.5f, vr_amp)) + return false; + if (!vr_check_level("vol signed R", 256, n, 0.5f, vr_amp_r)) + return false; + for (int i = 256; i < n; i++) { + int l = sv_out[i*2], r = sv_out[i*2+1]; + if ((l < 0) == (r < 0) && (l > 256 || l < -256) && (r > 256 || r < -256)) { + printf("FAILED vol signed: L and R same sign at %d (%d, %d)\n", i, l, r); + return false; + } + } + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// Looping VADPCM that is too long to keep in the sample buffer +// +// VADPCM is decoded in frames of 16 samples. The decoder cannot start at an +// arbitrary sample: it needs a saved state for that frame, and a wav64 only +// stores those at the beginning of the file and at the loop start (if the +// file has one). Looping a file that has no loop marker (wav64_set_loop) +// means the only legal restart is sample 0. A real reader asserts on any +// other jump ("invalid VADPCM seeking point"). +// +// rf_read in this file does not: it rebuilds decoder state from PCM, so the +// mixer can jump to any frame and the test still passes. This reader matches +// wav64 and allows a seek only at sample 0. +// +// When a loop fits in the sample buffer, the mixer copies it once and the +// RSP wraps it; no seek. When it does not fit, the CPU has to restart the +// stream each time playback reaches the end. A mix round cannot stop on an +// exact sample (it emits an even number of output samples, and resampling +// consumes a fractional number of input samples), so the position ends a +// few samples past the end. The mixer then asks the reader to continue from +// that leftover, as if those samples were already the start of the next +// iteration. +// +// At 1:1 the leftover is 1-2 samples, still inside the first VADPCM frame, +// so a restart at 0 is legal. Played 16× faster, each leftover output sample +// consumes 16 input samples, so the restart lands in the next frame. +// +// mixer_ch_set_loop(false) is another way to get a large leftover at 1:1: +// with looping off the mixer no longer stops at the end, so a whole poll +// can run past it. Turning looping back on then seeks to that leftover. +// Toggling every poll (a game that re-asserts looping each frame) is how +// the in-game crash at seeking point 0x1e0 was reproduced. +////////////////////////////////////////////////////////////////////////////// + +// Same length as the in-game WAV that crashed (273 VADPCM frames). The +// whole sample is the loop, so the only legal restart is 0. +#define LL_LEN 4368 + +static void ll_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) +{ + wav64_state_vadpcm_t *st = sbuf->state; + (void)ctx; + if (seeking) { + assertf(wpos == 0, "wav64: ll-loop: invalid VADPCM seeking point: 0x%x", + wpos * 16); + memset(st->state, 0, sizeof(st->state)); + } + int fb = VADPCM_FRAME_BYTES(4); + while (wlen > 0) { + int n = wlen < SAMPLEBUFFER_MARGIN_UNITS ? wlen : SAMPLEBUFFER_MARGIN_UNITS; + uint8_t *dst = samplebuffer_append(sbuf, n); + for (int i = 0; i < n; i++) + sv_gen_frame(dst + i * fb, wpos + i, 4); + wlen -= n; + wpos += n; + } +} + +static waveform_t ll_wave = { + .name = "ll-loop", .bits = 16, .channels = 1, .frequency = 16000, + .len = LL_LEN, .loop_len = LL_LEN, .read = ll_read, + .format = WAVEFORM_FORMAT_VADPCM, + .codec = &sv_codec, .state_size = sizeof(wav64_state_vadpcm_t), +}; + +// Long enough that the default sample buffer cannot hold the loop, even +// at 16× playback. That rate is what pushes the leftover into the second +// VADPCM frame (see the section comment). +#define LL8_LEN (4096 * 16) + +static waveform_t ll_wave_8x = { + .name = "ll-loop-8x", .bits = 16, .channels = 1, .frequency = 44100, + .len = LL8_LEN, .loop_len = LL8_LEN, .read = ll_read, + .format = WAVEFORM_FORMAT_VADPCM, + .codec = &sv_codec, .state_size = sizeof(wav64_state_vadpcm_t), +}; + +static bool test_mixer_vadpcm_large_loop_seek(waveform_t *wave, float freq) +{ + sv_silence(); + sv_set_bits(4); + // The 4368-sample loop would otherwise fit and wrap without seeking. + // Shrink the buffer so the CPU has to restart the stream at the end. + // The longer waveform already does not fit at 16× (default size). + int max_buf = (wave->len == LL_LEN) ? 1024 : 0; + mixer_ch_set_limits(SV_CHANNEL, 16, freq > 48000 ? freq : 48000, max_buf); + mixer_ch_play(SV_CHANNEL, wave); + mixer_ch_set_freq(SV_CHANNEL, freq); + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + + // Cross the end of the waveform at least once. ll_read asserts if the + // mixer then tries to resume at any sample other than 0. + for (int i = 0; i < 8; i++) + sv_mix(4096); + + mixer_ch_stop(SV_CHANNEL); + mixer_ch_set_freq(SV_CHANNEL, 44100); + mixer_ch_set_limits(SV_CHANNEL, 0, 48000, 0); + return true; +} + +// With looping on, the mixer stops a mix round at the end of the waveform, +// so the leftover is 1-2 samples (still in frame 0 at 1:1). +// mixer_ch_set_loop(false) removes that stop: a whole poll can run past the +// end. Turning looping on again then seeks to that leftover. +// +// A game that re-asserts looping every frame (true, false, true, ...) hits +// this whenever a "false" poll crosses the end. Play at the mixer rate so +// one output sample is one waveform sample: a 512-sample poll past the end +// is a leftover of hundreds of samples, not 1-2. That leftover was 0x1e0 +// in the in-game crash. +static bool test_mixer_vadpcm_set_loop_toggle(void) +{ + sv_silence(); + sv_set_bits(4); + mixer_ch_set_limits(SV_CHANNEL, 16, 48000, 1024); + mixer_ch_play(SV_CHANNEL, &ll_wave); + mixer_ch_set_freq(SV_CHANNEL, 44100); + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + + for (int i = 0; i < 32; i++) { + mixer_ch_set_loop(SV_CHANNEL, (i & 1) == 0); + sv_mix(512); + if (!mixer_ch_playing(SV_CHANNEL)) + break; + } + + mixer_ch_stop(SV_CHANNEL); + mixer_ch_set_freq(SV_CHANNEL, 44100); + mixer_ch_set_limits(SV_CHANNEL, 0, 48000, 0); + return true; +} + +// Same leftover, isolated: walk up to the end while still looping, disable +// looping, mix one poll past it, re-enable. The next mix wraps and seeks. +static bool test_mixer_vadpcm_set_loop_overshoot(void) +{ + sv_silence(); + sv_set_bits(4); + mixer_ch_set_limits(SV_CHANNEL, 16, 48000, 1024); + mixer_ch_play(SV_CHANNEL, &ll_wave); + mixer_ch_set_freq(SV_CHANNEL, 44100); + mixer_ch_set_vol(SV_CHANNEL, 0.5f, 0.5f); + + sv_mix((LL_LEN - 256) & ~1); + mixer_ch_set_loop(SV_CHANNEL, false); + sv_mix(512); + mixer_ch_set_loop(SV_CHANNEL, true); + sv_mix(64); + + mixer_ch_stop(SV_CHANNEL); + mixer_ch_set_freq(SV_CHANNEL, 44100); + mixer_ch_set_limits(SV_CHANNEL, 0, 48000, 0); + return true; +} + +// Resident VADPCM loop: same content as rf_wave, but addressed from RDRAM. +static uint8_t *rf_mem_res; +static waveform_t rf_wave_resident; + +static bool test_mixer_resident_vadpcm_loop(void) +{ + if (!rf_mem_res) { + int fb = VADPCM_FRAME_BYTES(rf_bits); + rf_mem_res = malloc_uncached(RF_FRAMES * fb + 64); + memcpy(rf_mem_res, rf_frames, RF_FRAMES * fb); + rf_wave_resident = rf_wave; + rf_wave_resident.name = "rf-loop-res"; + rf_wave_resident.mem = rf_mem_res; + // Keep read for seek side-effects (decoder state). + rf_wave_resident.__uuid = 0; + } + return test_mixer_loop_exact(&rf_wave_resident, RF_LOOP_START - 64); +} + +////////////////////////////////////////////////////////////////////////////// +// Channel allocation / mixer_play +////////////////////////////////////////////////////////////////////////////// + +static void alloc_silence(void) +{ + for (int i = 0; i < 8; i++) + mixer_ch_stop(i); + sv_mix(256); +} + +/** Free preferred over steal; quieter over louder; older over newer. */ +static bool test_mixer_alloc_order(void) +{ + alloc_silence(); + int out[4]; + + // Fill ch0-2; ch3 free → alloc prefers the free channel. + mixer_ch_play(0, &bl_wave_loop_res); + mixer_ch_set_priority(0, MIXER_PRIORITY_SFX); + mixer_ch_play(1, &bl_wave_loop_res); + mixer_ch_set_priority(1, MIXER_PRIORITY_SFX); + mixer_ch_play(2, &bl_wave_loop_res); + mixer_ch_set_priority(2, MIXER_PRIORITY_SFX); + if (mixer_ch_alloc(0, 4, 1, false, MIXER_PRIORITY_SFX, NULL, out) != 1 || + out[0] != 3) { + printf("FAILED alloc order: free channel not preferred (got %d)\n", + out[0]); + return false; + } + + // All busy, same prio: quieter wins. + mixer_ch_play(3, &bl_wave_loop_res); + mixer_ch_set_priority(3, MIXER_PRIORITY_SFX); + mixer_ch_set_vol_ramp(0, 1.0f, 1.0f, 0); + mixer_ch_set_vol_ramp(1, 0.25f, 0.25f, 0); + mixer_ch_set_vol_ramp(2, 0.75f, 0.75f, 0); + mixer_ch_set_vol_ramp(3, 0.5f, 0.5f, 0); + if (mixer_ch_alloc(0, 4, 1, false, MIXER_PRIORITY_SFX, NULL, out) != 1 || + out[0] != 1) { + printf("FAILED alloc order: quietest not preferred (got %d)\n", out[0]); + return false; + } + + // Equal volume: oldest wins. Restart 1/2/3 after aging ch0. + alloc_silence(); + mixer_ch_play(0, &bl_wave_loop_res); + mixer_ch_set_priority(0, MIXER_PRIORITY_SFX); + mixer_ch_set_vol_ramp(0, 0.5f, 0.5f, 0); + sv_mix(512); + mixer_ch_play(1, &bl_wave_loop_res); + mixer_ch_set_priority(1, MIXER_PRIORITY_SFX); + mixer_ch_set_vol_ramp(1, 0.5f, 0.5f, 0); + mixer_ch_play(2, &bl_wave_loop_res); + mixer_ch_set_priority(2, MIXER_PRIORITY_SFX); + mixer_ch_set_vol_ramp(2, 0.5f, 0.5f, 0); + if (mixer_ch_alloc(0, 3, 1, false, MIXER_PRIORITY_SFX, NULL, out) != 1 || + out[0] != 0) { + printf("FAILED alloc order: oldest not preferred (got %d)\n", out[0]); + return false; + } + + return true; +} + +/** Victims with prio > request are not stealable. */ +static bool test_mixer_alloc_priority(void) +{ + alloc_silence(); + int out[2]; + + for (int i = 0; i < 4; i++) { + mixer_ch_play(i, &bl_wave_loop_res); + // Leave default MIXER_PRIORITY_MAX from mixer_ch_play. + } + if (mixer_ch_alloc(0, 4, 1, false, MIXER_PRIORITY_SFX, NULL, out) != 0) { + printf("FAILED alloc prio: stole a MAX-priority channel\n"); + return false; + } + + mixer_ch_set_priority(2, MIXER_PRIORITY_SFX); + if (mixer_ch_alloc(0, 4, 1, false, MIXER_PRIORITY_SFX, NULL, out) != 1 || + out[0] != 2) { + printf("FAILED alloc prio: expected ch2 (got n=%d ch=%d)\n", + out[0] >= 0 ? 1 : 0, out[0]); + return false; + } + + // Equal priority is stealable. + mixer_ch_set_priority(0, 100); + mixer_ch_set_priority(1, 100); + mixer_ch_set_priority(2, 100); + mixer_ch_set_priority(3, 100); + if (mixer_ch_alloc(0, 4, 1, false, 100, NULL, out) != 1) { + printf("FAILED alloc prio: equal priority should be stealable\n"); + return false; + } + return true; +} + +/** count larger than availability returns a partial plan. */ +static bool test_mixer_alloc_partial(void) +{ + alloc_silence(); + int out[8]; + mixer_ch_play(0, &bl_wave_loop_res); + mixer_ch_set_priority(0, MIXER_PRIORITY_SFX); + mixer_ch_play(1, &bl_wave_loop_res); + mixer_ch_set_priority(1, MIXER_PRIORITY_SFX); + // Only 2 free in a window of 4. + int n = mixer_ch_alloc(0, 4, 4, false, MIXER_PRIORITY_MIN, NULL, out); + if (n != 2 || out[0] != 2 || out[1] != 3) { + printf("FAILED alloc partial: n=%d out=%d,%d (want 2: 2,3)\n", + n, out[0], out[1]); + return false; + } + return true; +} + +/** Plan-only: channels keep playing; repeated calls are stable. */ +static bool test_mixer_alloc_plan_only(void) +{ + alloc_silence(); + int out1[2], out2[2]; + mixer_ch_play(0, &bl_wave_loop_res); + mixer_ch_set_priority(0, MIXER_PRIORITY_SFX); + mixer_ch_set_vol_ramp(0, 0.5f, 0.5f, 0); + mixer_ch_play(1, &bl_wave_loop_res); + mixer_ch_set_priority(1, MIXER_PRIORITY_SFX); + mixer_ch_set_vol_ramp(1, 0.5f, 0.5f, 0); + + if (mixer_ch_alloc(0, 2, 1, false, MIXER_PRIORITY_SFX, NULL, out1) != 1) { + printf("FAILED alloc plan-only: first plan empty\n"); + return false; + } + if (!mixer_ch_playing(0) || !mixer_ch_playing(1)) { + printf("FAILED alloc plan-only: plan stopped a channel\n"); + return false; + } + if (mixer_ch_alloc(0, 2, 1, false, MIXER_PRIORITY_SFX, NULL, out2) != 1 || + out2[0] != out1[0]) { + printf("FAILED alloc plan-only: second plan differs (%d vs %d)\n", + out1[0], out2[0]); + return false; + } + return true; +} + +/** Stereo pairs: contiguous primary, never last channel, never scattered. */ +static bool test_mixer_alloc_stereo(void) +{ + alloc_silence(); + int out[4]; + + // Free window: lowest contiguous pair. + if (mixer_ch_alloc(0, 8, 1, true, MIXER_PRIORITY_SFX, NULL, out) != 1 || + out[0] != 0) { + printf("FAILED alloc stereo: expected primary 0, got %d\n", out[0]); + return false; + } + + // Occupy 0; next pair is 1-2? Actually 0-1 is busy if 0 plays stereo. + mixer_ch_play(0, &bl_wave_loop_stereo); + mixer_ch_set_priority(0, MIXER_PRIORITY_SFX); + if (mixer_ch_alloc(0, 8, 1, true, MIXER_PRIORITY_MIN, NULL, out) != 1 || + out[0] != 2) { + printf("FAILED alloc stereo: expected next free pair at 2, got %d\n", + out[0]); + return false; + } + + // Never return the stereo sub as a primary (mono alloc over a stereo pair). + if (mixer_ch_alloc(0, 8, 1, false, MIXER_PRIORITY_MIN, NULL, out) != 1 || + out[0] == 1) { + printf("FAILED alloc stereo: returned STEREO_SUB as primary (%d)\n", + out[0]); + return false; + } + + // Window with no contiguous free pair even if scattered frees exist. + alloc_silence(); + mixer_ch_play(1, &bl_wave_loop_res); + mixer_ch_play(3, &bl_wave_loop_res); + // Free: 0,2,4,5,6,7 — but in window [0,4) free are 0 and 2 (not a pair). + if (mixer_ch_alloc(0, 4, 1, true, MIXER_PRIORITY_MIN, NULL, out) != 0) { + printf("FAILED alloc stereo: found pair in scattered window (ch=%d)\n", + out[0]); + return false; + } + + // Last mixer channel cannot be a stereo primary. + alloc_silence(); + for (int i = 0; i < 7; i++) + mixer_ch_play(i, &bl_wave_loop_res); + if (mixer_ch_alloc(0, 8, 1, true, MIXER_PRIORITY_MIN, NULL, out) != 0) { + printf("FAILED alloc stereo: planned last channel as stereo primary\n"); + return false; + } + return true; +} + +/** Channels whose limits reject the waveform are skipped. */ +static bool test_mixer_alloc_limits(void) +{ + alloc_silence(); + int out[2]; + + mixer_ch_set_limits(0, 8, BL_FREQ, 0); + mixer_ch_set_limits(1, 16, 8000, 0); + // ch2 keeps defaults (16-bit, full rate) and is free. + if (mixer_ch_alloc(0, 3, 1, false, MIXER_PRIORITY_SFX, + &bl_wave_loop_res, out) != 1 || out[0] != 2) { + printf("FAILED alloc limits: expected ch2, got %d\n", out[0]); + mixer_ch_set_limits(0, 16, BL_FREQ, 0); + mixer_ch_set_limits(1, 16, BL_FREQ, 0); + return false; + } + mixer_ch_set_limits(0, 16, BL_FREQ, 0); + mixer_ch_set_limits(1, 16, BL_FREQ, 0); + return true; +} + +/** mixer_play: free channel, steal SFX, refuse music, stereo, state reset. */ +static bool test_mixer_play(void) +{ + alloc_silence(); + + int ch = mixer_play(&bl_wave_oneshot_res, MIXER_PRIORITY_SFX); + if (ch < 0 || !mixer_ch_playing(ch)) { + printf("FAILED mixer_play: free-channel play failed (ch=%d)\n", ch); + return false; + } + + // Saturate with SFX; next play steals one of them. + alloc_silence(); + int occupied[8]; + for (int i = 0; i < 8; i++) { + occupied[i] = mixer_play(&bl_wave_loop_res, MIXER_PRIORITY_SFX); + if (occupied[i] < 0) { + printf("FAILED mixer_play: could not fill channel %d\n", i); + return false; + } + } + int stolen = mixer_play(&bl_wave_oneshot_res, MIXER_PRIORITY_SFX); + if (stolen < 0) { + printf("FAILED mixer_play: failed to steal another SFX\n"); + return false; + } + + // Music-priority channels are not stolen by SFX. + alloc_silence(); + for (int i = 0; i < 8; i++) { + mixer_ch_play(i, &bl_wave_loop_res); + mixer_ch_set_priority(i, MIXER_PRIORITY_MUSIC); + } + if (mixer_play(&bl_wave_oneshot_res, MIXER_PRIORITY_SFX) != -1) { + printf("FAILED mixer_play: stole a MUSIC-priority channel\n"); + return false; + } + + // Stereo picks a contiguous pair. + alloc_silence(); + mixer_ch_play(0, &bl_wave_loop_res); + mixer_ch_set_priority(0, MIXER_PRIORITY_SFX); + ch = mixer_play(&bl_wave_loop_stereo, MIXER_PRIORITY_SFX); + if (ch != 1) { + printf("FAILED mixer_play: stereo expected ch1, got %d\n", ch); + return false; + } + if (!mixer_ch_playing(1) || !mixer_ch_playing(2)) { + printf("FAILED mixer_play: stereo pair not both playing\n"); + return false; + } + + // Volume/pan and force_mono are reset before play. + alloc_silence(); + mixer_ch_play(0, &bl_wave_loop_res); + mixer_ch_set_vol_pan(0, 0.25f, 1.0f); + mixer_ch_set_force_mono(0, true); + mixer_ch_stop(0); + ch = mixer_play(&bl_wave_oneshot_res, MIXER_PRIORITY_SFX); + if (ch != 0) { + printf("FAILED mixer_play: reset test expected ch0, got %d\n", ch); + return false; + } + if (mixer_ch_get_force_mono(0)) { + printf("FAILED mixer_play: force_mono not cleared\n"); + return false; + } + // Settled volume should be full (1,1): mix a little and check amplitude. + sv_mix(256); + int peak = 0; + for (int i = 0; i < 256; i++) { + int a = sv_out[i * 2]; + if (a < 0) a = -a; + if (a > peak) peak = a; + } + if (peak < BL_AMP * 3 / 4) { + printf("FAILED mixer_play: volume not reset (peak=%d)\n", peak); + return false; + } + return true; +} + +int main(void) +{ + debug_init_emulog(); + debug_init_usblog(); + emux_ioctl_fast(); + + console_init(); + + printf("WAV64 mixer tests\n\n"); + + audio_init(44100, 4); + mixer_init(8); + + int total = 0, failed = 0; + int inmix_frames[] = { 1, 2, 8, 16 }; + const double vad_ratios[] = { 0.5, 1.0, 1.2891, 1.5 }; + // 4-bit residuals keep the classic nibble packing, 3 and 2 bit pack the + // payload densely: same framing, same codebook, shorter frames. + const int vad_bits[] = { 4, 3, 2 }; + for (int b = 0; b < 3; b++) { + for (int s = 0; s < 4; s++) { + for (int fc = 0; fc < 4; fc++) { + for (int r = 0; r < 4; r++) { + uint32_t step = (uint32_t)(vad_ratios[r] * (1 << MIXER_FX64_FRAC)); + int nwave = inmix_frames[fc] * 16; + // Stay inside the waveform including Hermite overread. + int nout = nwave / 2; + if (nout > 96) nout = 96; + if (nout < 8) nout = nwave > 8 ? 8 : nwave; + total++; + if (!test_vadpcm_inmixer(inmix_frames[fc], step, nout, s + 1, vad_bits[b])) + failed++; + } + } + } + } + + printf("Mono PCM resampling tests\n"); + fflush(stdout); + // Enough output samples to force several cache refills, but few enough + // that even the fastest ratio stays inside the 512-sample waveform. + const double ratios[] = { 0.5, 1.0, 1.2891, 1.5, 2.0, 3.0, 3.7 }; + for (int b = 0; b < 2; b++) { + bool is16 = b != 0; + for (int r = 0; r < 7; r++) { + uint32_t step = (uint32_t)(ratios[r] * (1 << (MIXER_FX64_FRAC + (is16 ? 1 : 0)))); + total++; + if (!test_pcm_resample(is16, step, 96, r + 1)) + failed++; + } + } + + printf("Stereo PCM resampling tests\n"); + fflush(stdout); + for (int b = 0; b < 2; b++) { + bool is16 = b != 0; + int bps = (is16 ? 1 : 0) + 1; + for (int r = 0; r < 7; r++) { + uint32_t step = (uint32_t)(ratios[r] * (1 << (MIXER_FX64_FRAC + bps))); + total++; + if (!test_pcm_resample_stereo(is16, step, 96, r + 10)) + failed++; + } + } + + printf("Streamed mono PCM tests\n"); + fflush(stdout); + sv_init(); + // Below the sample rate: step < 1 makes the inclusive window one unit + // larger than the position advance, which is what used to overrun the + // samplebuffer margin. Odd starts exercise the 8-bit ring parity path. + const float sp_freqs[] = { 22050, 11025, 16000, 32000 }; + const int sp_starts[] = { 0, 1, 5779, SP_LEN - 64 }; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + total++; + if (!test_mixer_stream_pcm8(sp_freqs[i], sp_starts[j])) + failed++; + } + } + + const float bc_freqs[] = { 48000, 44100, 32000, 22050 }; + for (int i = 0; i < 4; i++) { + total++; + if (!test_mixer_block_codec_loop(bc_freqs[i])) + failed++; + } + + printf("Streamed stereo VADPCM tests\n"); + fflush(stdout); + const float sv_freqs[] = { 32000, 44100, 22050, 33333, 48000 }; + const int sv_starts[] = { 0, 16, 7, 12345, 65537 }; + for (int c = 0; c < 3; c++) { + sv_chunk = 512 << c; + for (int i = 0; i < 5; i++) { + total++; + if (!test_mixer_stream(sv_freqs[i], sv_starts[i])) + failed++; + } + } + sv_chunk = 1024; + for (int i = 0; i < 5; i++) { + total++; + if (!test_mixer_queue_depth(sv_freqs[i])) + failed++; + } + total++; if (!test_mixer_posfold()) failed++; + total++; if (!test_mixer_stop_releases_sub()) failed++; + total++; if (!test_mixer_switch_waveform()) failed++; + total++; if (!test_mixer_loop()) failed++; + total++; if (!test_mixer_stale_unit_bytes()) failed++; + total++; if (!test_mixer_stereo_r_reopen()) failed++; + total++; if (!test_mixer_realloc_same_wave()) failed++; + + // The same stream at the narrower widths. Frames get shorter, so every + // size and offset the mixer derives from them changes with the width. + for (int b = 3; b >= 2; b--) { + printf("Streamed stereo VADPCM tests (%d bit)\n", b); + fflush(stdout); + sv_set_bits(b); + for (int i = 0; i < 5; i++) { + total++; + if (!test_mixer_stream(sv_freqs[i], sv_starts[i])) + failed++; + } + total++; if (!test_mixer_posfold()) failed++; + total++; if (!test_mixer_loop()) failed++; + } + sv_set_bits(4); + + printf("Retrigger tests\n"); + fflush(stdout); + is_init(); + // A tracker plays these an octave or two below the output rate (Amiga + // periods), so a note advances slower than the mix and its window covers + // fewer frames than the output samples it feeds. + const float is_freqs[] = { 44100, 28125, 14063, 8363 }; + for (int c = 0; c < 3; c++) { + sv_chunk = 512 << c; + for (int i = 0; i < 4; i++) { + total++; if (!test_mixer_retrigger(false, is_freqs[i])) failed++; + total++; if (!test_mixer_retrigger(true, is_freqs[i])) failed++; + } + } + sv_chunk = 1024; + + printf("Pinned loop tests\n"); + fflush(stdout); + const float lc_freqs[] = { 32000, 44100, 21237 }; + for (int i = 0; i < 3; i++) { + total++; if (!test_mixer_loop_cache(&lc_wave_loop, lc_freqs[i])) failed++; + total++; if (!test_mixer_loop_cache(&lc_wave_whole, lc_freqs[i])) failed++; + total++; if (!test_mixer_vadpcm_partial_frame(lc_freqs[i])) failed++; + total++; if (!test_mixer_loop_taps(&lp_wave, lc_freqs[i], 0)) failed++; + // Large loop, wrapped by the CPU: start just before the loop point so + // that the mix crosses it several times, and go up to three octaves + // above the sample rate, where a round covers thousands of samples of + // the stream. + total++; if (!test_mixer_loop_taps(&lp_wave_big, lc_freqs[i], 48000)) failed++; + total++; if (!test_mixer_loop_taps(&lp_wave_big, lc_freqs[i] * 8, 48000)) failed++; + } + // Start before the loop point (so that the pin happens on the fly) and + // inside it (so that it happens right away). Every residual width is + // compared sample by sample against the C decoder, which is where a + // mistake in the unpacking of a frame turns into audible noise. + static const int rf_starts[] = { -700, -1, 64, 777 }; + for (int b = 4; b >= 2; b--) { + printf("Exact loop tests (%d bit)\n", b); + fflush(stdout); + rf_init(b); + // A round is a whole number of frames, so the start decides where every + // round boundary of the test falls inside a frame, and the last samples + // of a frame are where the resampler needs the one after it. + for (int i = 0; i < (b == 4 ? 16 : 4); i++) { + total++; if (!test_mixer_loop_exact(&rf_wave_plain, 16 + i)) failed++; + } + // Reaching the loop point while streaming, and starting inside a loop + // that is pinned right away. The wrap moves across the rounds as it + // repeats, so a single run covers several alignments between the two. + for (int i = 0; i < 4; i++) { + total++; + if (!test_mixer_loop_exact(&rf_wave, RF_LOOP_START + rf_starts[i])) failed++; + } + } + + // Resident / frequency-change / mid-stop / set_loop matrix. + printf("Baseline mixer scenarios\n"); + fflush(stdout); + bl_init(); + total++; if (!test_mixer_baseline_pcm(&bl_wave_oneshot_res, 0, 300)) failed++; + total++; if (!test_mixer_baseline_pcm(&bl_wave_oneshot_stream, 0, 300)) failed++; + total++; if (!test_mixer_baseline_pcm(&bl_wave_loop_res, BL_INTRO - 32, 1024)) failed++; + total++; if (!test_mixer_baseline_pcm(&bl_wave_loop_stream, BL_INTRO - 32, 1024)) failed++; + total++; if (!test_mixer_baseline_pcm(&bl_wave_loop_res, BL_INTRO + 8, 1024)) failed++; + + printf("mixer_ch_set_loop matrix\n"); + fflush(stdout); + #define SL_RUN(w, when, s) do { \ + total++; if (!test_mixer_set_loop((w), (when), (s))) failed++; \ + } while (0) + // PCM streamed mono: every note-off placement, including the fundamentals + // near loop_end and just after a wrap. Release is shorter than a round. + SL_RUN(&bl_wave_loop_stream, SL_BEFORE, bl_sample); + SL_RUN(&bl_wave_loop_stream, SL_INSIDE, bl_sample); + SL_RUN(&bl_wave_loop_stream, SL_NEAR_END, bl_sample); + SL_RUN(&bl_wave_loop_stream, SL_AFTER_WRAP, bl_sample); + // PCM resident with release (mid-loop only: wraps would Hermite into release). + SL_RUN(&bl_wave_loop_res_rel, SL_INSIDE, bl_sample); + // PCM stereo streamed. + SL_RUN(&bl_wave_loop_stereo, SL_INSIDE, bl_sample); + // Loop shorter than MIXER_LOOP_OVERREAD. + SL_RUN(&bl_wave_tiny, SL_INSIDE, bl_tiny_sample); + SL_RUN(&bl_wave_tiny, SL_NEAR_END, bl_tiny_sample); + // Loop pinned in the samplebuffer. + SL_RUN(&bl_wave_pin, SL_INSIDE, bl_pin_sample); + SL_RUN(&bl_wave_pin, SL_AFTER_WRAP, bl_pin_sample); + + slv_init(); + // VADPCM streamed/resident; release length not a multiple of 16. + // BEFORE is the note-off that never reached the sustain loop: the refill + // after the unpin has to carry on from the intro, where no seek point is. + SL_RUN(&slv_wave_stream, SL_BEFORE, slv_sample); + SL_RUN(&slv_wave_stream, SL_INSIDE, slv_sample); + SL_RUN(&slv_wave_stream, SL_NEAR_END, slv_sample); + SL_RUN(&slv_wave_stream, SL_AFTER_WRAP, slv_sample); + SL_RUN(&slv_wave_res, SL_INSIDE, slv_sample); + // VADPCM stereo: exact left + L/R identity across unpin + release, with + // both planes going through the intro and the post-wrap re-seed. + SL_RUN(&slv_wave_stereo, SL_BEFORE, slv_sample); + SL_RUN(&slv_wave_stereo, SL_INSIDE, slv_sample); + SL_RUN(&slv_wave_stereo, SL_AFTER_WRAP, slv_sample); + #undef SL_RUN + + printf("Volume ramps\n"); + fflush(stdout); + total++; if (!test_mixer_vol_ramp(&bl_wave_loop_res)) failed++; + total++; if (!test_mixer_vol_ramp(&bl_wave_loop_stereo)) failed++; + total++; if (!test_mixer_vol_ramp_short()) failed++; + total++; if (!test_mixer_vol_ramp_replace()) failed++; + total++; if (!test_mixer_vol_ramp_land()) failed++; + total++; if (!test_mixer_vol_ramp_smooth()) failed++; + total++; if (!test_mixer_gain_ramp()) failed++; + total++; if (!test_mixer_declick()) failed++; + total++; if (!test_mixer_vol_dolby()) failed++; + + total++; if (!test_mixer_freq_change()) failed++; + total++; if (!test_mixer_freq_ramp()) failed++; + total++; if (!test_mixer_stop_mid()) failed++; + rf_init(4); + total++; if (!test_mixer_resident_vadpcm_loop()) failed++; + + printf("Channel allocation\n"); + fflush(stdout); + total++; if (!test_mixer_alloc_order()) failed++; + total++; if (!test_mixer_alloc_priority()) failed++; + total++; if (!test_mixer_alloc_partial()) failed++; + total++; if (!test_mixer_alloc_plan_only()) failed++; + total++; if (!test_mixer_alloc_stereo()) failed++; + total++; if (!test_mixer_alloc_limits()) failed++; + total++; if (!test_mixer_play()) failed++; + + printf("VADPCM large-loop wrap seek\n"); + fflush(stdout); + total++; if (!test_mixer_vadpcm_set_loop_toggle()) failed++; + total++; if (!test_mixer_vadpcm_set_loop_overshoot()) failed++; + total++; if (!test_mixer_vadpcm_large_loop_seek(&ll_wave_8x, 44100.f * 16)) failed++; + total++; if (!test_mixer_vadpcm_large_loop_seek(&ll_wave, 16000)) failed++; + + sv_silence(); + mixer_ch_set_limits(SV_CHANNEL, 0, 48000, 0); + + if (failed) { + printf("\n%d/%d TESTS FAILED\n", failed, total); + abort(); + } + printf("\nALL TESTS PASSED (%d)\n", total); + return 0; +} diff --git a/tests/testrom.c b/tests/testrom.c index 861086da3..1299b2cc2 100644 --- a/tests/testrom.c +++ b/tests/testrom.c @@ -124,7 +124,7 @@ static uint32_t myrand(void) { } \ }) -// ASSERT_EQUAL_FLAOT(a, b, msg): fail the test if a!=b (and log a/b as float values) +// ASSERT_EQUAL_FLOAT(a, b, msg): fail the test if a!=b (and log a/b as float values) #define ASSERT_EQUAL_FLOAT(_a, _b, msg, ...) ({ \ float a = _a; float b = _b; \ if (a != b) { \ @@ -189,13 +189,23 @@ int assert_equal_mem(TestContext *ctx, const char *file, int line, const uint8_t } \ }) +// Return the amount of memory used in the heap. +#define MEMORY_USED() ({ \ + heap_stats_t heap_stats; \ + sys_get_heap_stats(&heap_stats); \ + heap_stats.used; \ +}) + /********************************************************************** * TEST FILES **********************************************************************/ +#include "test_kernel.c" #include "test_dfs.c" #include "test_eepromfs.c" #include "test_cache.c" +#include "test_sys.c" +#include "test_scratch.c" #include "test_ticks.c" #include "test_timer.c" #include "test_irq.c" @@ -207,12 +217,18 @@ int assert_equal_mem(TestContext *ctx, const char *file, int line, const uint8_t #include "test_backtrace.c" #include "test_rspq.c" #include "test_rdpq.c" +#include "test_rdpq_text_layout.c" #include "test_rdpq_tri.c" #include "test_rdpq_tex.c" #include "test_rdpq_attach.c" #include "test_rdpq_sprite.c" +#include "test_rdpq_mat.c" +#include "test_mpeg1.c" +#include "test_gl.c" #include "test_dl.c" +#include "test_math.c" #include "test_fm.c" +#include "test_magma.c" /********************************************************************** * MAIN @@ -247,8 +263,26 @@ static const struct Testsuite TEST_FUNC(test_timer_disabled_start, 733, TEST_FLAGS_RESET_COUNT), TEST_FUNC(test_timer_disabled_restart, 733, TEST_FLAGS_RESET_COUNT), TEST_FUNC(test_irq_reentrancy, 230, TEST_FLAGS_RESET_COUNT), + TEST_FUNC(test_sys_hwmemset, 0, TEST_FLAGS_NO_BENCHMARK | TEST_FLAGS_NO_EMULATOR), + TEST_FUNC(test_sys_hwmemset_uncached, 0, TEST_FLAGS_NO_BENCHMARK | TEST_FLAGS_NO_EMULATOR), + TEST_FUNC(test_scratch_basics, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_scratch_calloc, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_scratch_stats_and_peak, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_scratch_collapse_ordering, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_scratch_realloc, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_scratch_exhaustion_recovery, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_kernel_basic, 5, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_kernel_mutex_1, 5, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_kernel_priority, 5, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_kernel_sleep, 5, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_kernel_libc1, 5, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_kernel_libc2, 5, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_kernel_thread_local, 5, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_dfs_read, 948, TEST_FLAGS_IO), TEST_FUNC(test_dfs_rom_addr, 25, TEST_FLAGS_IO), + TEST_FUNC(test_dfs_rom_size, 25, TEST_FLAGS_IO), + TEST_FUNC(test_dfs_ioctl, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_dfs_directory, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_eepromfs, 0, TEST_FLAGS_IO), TEST_FUNC(test_cache_invalidate, 1763, TEST_FLAGS_NONE), TEST_FUNC(test_debug_sdfs, 0, TEST_FLAGS_NO_BENCHMARK), @@ -261,10 +295,11 @@ static const struct Testsuite TEST_FUNC(test_backtrace_exception_leaf, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_backtrace_exception_fp, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_backtrace_invalidptr, 0, TEST_FLAGS_NO_BENCHMARK), - TEST_FUNC(test_rspq_queue_single, 0, TEST_FLAGS_NO_BENCHMARK), - TEST_FUNC(test_rspq_queue_multiple, 0, TEST_FLAGS_NO_BENCHMARK), - TEST_FUNC(test_rspq_queue_rapid, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rspq_cmd_single, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rspq_cmd_multiple, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rspq_cmd_rapid, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rspq_wrap, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rspq_buffer_handoff_atomic, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rspq_high_load, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rspq_load_overlay, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rspq_switch_overlay, 0, TEST_FLAGS_NO_BENCHMARK), @@ -274,6 +309,7 @@ static const struct Testsuite TEST_FUNC(test_rspq_flush, 0, TEST_FLAGS_NO_BENCHMARK | TEST_FLAGS_NO_EMULATOR), TEST_FUNC(test_rspq_rapid_flush, 0, TEST_FLAGS_NO_BENCHMARK | TEST_FLAGS_NO_EMULATOR), TEST_FUNC(test_rspq_block, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rspq_block_begin_reuse, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rspq_wait_sync_in_block, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rspq_highpri_basic, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rspq_highpri_multiple, 0, TEST_FLAGS_NO_BENCHMARK), @@ -281,11 +317,18 @@ static const struct Testsuite TEST_FUNC(test_rspq_big_command, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rspq_rdp_dynamic, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rspq_rdp_dynamic_switch, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rspq_deferred_call, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rspq_queue_basic, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rspq_queue_clear, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rspq_queue_growth, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rspq_queue_order, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rspq_queue_block_nesting, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_rspqwait, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_clear, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_dynamic, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_passthrough_big, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_block, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_block_begin_reuse, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_block_coalescing, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_block_contiguous, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_block_dynamic, 0, TEST_FLAGS_NO_BENCHMARK), @@ -306,24 +349,85 @@ static const struct Testsuite TEST_FUNC(test_rdpq_fog, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_mode_antialias, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_mode_alphacompare, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_mode_zmode, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_mode_freeze, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_mode_freeze_stack, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_mipmap, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_mipmap_interpolate, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_autotmem, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_autotmem_reuse, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_texrect_passthrough, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_triangle_block_order, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_triangle, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_triangle_w1, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_metrics_empty, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_metrics_simple_ascii, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_newline_only, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_escape_dollar_font, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_escape_caret_style, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_escape_literal_dollar_caret, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_indent_first_line, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_wrap_char_basic, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_wrap_word_basic, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_wrap_word_long_token, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_wrap_none_truncate, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_wrap_ellipses, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_char_spacing, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_line_spacing, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_tab_default_32, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_tab_custom_stops, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_max_chars_stop_at_space, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_nbytes_wrap_word_plaintext, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_nbytes_wrap_word_with_escapes, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_nbytes_wrap_char_with_escapes, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_nbytes_ellipsis, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_nbytes_wrap_word_consumes_newline, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_nbytes_wrap_word_consecutive_escapes, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_max_chars_clamp_end, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_max_chars_with_word_wrap, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_typewriter, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_wrap_ellipses_narrow_width, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_wrap_word_short_then_long_token, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_height_truncates_vertical, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_valign_center_height, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_align_center_width, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_align_right_width, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_utf8_multibyte_in_range, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_utf8_invalid_sequence, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_preserve_overlap_order, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_disable_aa_fix_flag, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_builder_api_matches_build, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_kerning_var_font, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_register_get_unregister, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_printf_small_buf, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_text_printf_large_buf, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_attach_clear, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_attach_stack, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_tex_upload, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_tex_upload_multi, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_tex_can_upload, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_tex_blit_normal, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_tex_blit_filtering, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_tex_multi_i4, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_tex_upload_tlut, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_tex_upload_tlut_alignments, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_tex_4bpp_odd, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_sprite_upload, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_rdpq_sprite_lod, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_mat_basic, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_mat_empty, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_mat_blender, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_rdpq_mat_ext, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mpeg1_idct, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mpeg1_block_decode, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mpeg1_block_dequant, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mpeg1_block_predict, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_gl_clear, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_gl_draw_arrays, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_gl_draw_elements, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_gl_texture_completeness, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_gl_list, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_gl_cull, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_dl_syms, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_dladdr, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_dl_relocs, 0, TEST_FLAGS_NO_BENCHMARK), @@ -331,15 +435,47 @@ static const struct Testsuite TEST_FUNC(test_dlsym_rtld_default, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_dlclose, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_dl_ctors, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mat4_mul_two_identities, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mat4_mul_scale_translation, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mat3_mul_two_identities, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mat3_mul_scale_translation, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_quat_from_euler_zero_identity, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_quat_slerp_same_quaternion, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_quat_slerp_near_identical_is_finite, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_fm_truncf, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_fm_ceilf, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_fm_floorf, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_fm_roundf, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_fm_fmodf, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_fm_wrapf, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_fm_sinf, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_fm_cosf, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_fm_sincosf, 0, TEST_FLAGS_NO_BENCHMARK), TEST_FUNC(test_fm_atan2f, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_fm_expf, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_fm_lerp_angle, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_fm_wrap_angle, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_triangle_list, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_triangle_list_full_cache, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_triangle_strip, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_triangle_strip_full_cache, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_triangle_fan, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_triangle_fan_full_cache, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_indexed_one_tri, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_indexed_two_tris, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_indexed_full_cache, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_indexed_full_one_extra, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_indexed_fragmented_batch, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_indexed_frag_backwards, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_indexed_holes, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_indexed_out_of_order, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_indexed_coalescing, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_indexed_strip, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_indexed_strip_full, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_indexed_fan, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_indexed_fan_full, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_indexed_restart_strip, 0, TEST_FLAGS_NO_BENCHMARK), + TEST_FUNC(test_mg_draw_indexed_restart_fan, 0, TEST_FLAGS_NO_BENCHMARK), }; int main() { @@ -347,6 +483,7 @@ int main() { console_set_debug(false); debug_init_emulog(); debug_init_usblog(); + emux_ioctl_fast(); // ask emulator to run as fast as possible if (dfs_init( DFS_DEFAULT_LOCATION ) != DFS_ESUCCESS) { printf("Invalid ROM: cannot initialize DFS\n"); @@ -389,6 +526,7 @@ int main() { data_cache_writeback_invalidate_all(); inst_cache_invalidate_all(); + uint32_t test_start_mem = MEMORY_USED(); uint32_t test_start = TICKS_READ(); // Run the test! @@ -396,6 +534,15 @@ int main() { // Compute the test duration uint32_t test_stop = TICKS_READ(); + uint32_t test_stop_mem = MEMORY_USED(); + + if (test_stop_mem > test_start_mem) { + // If the test didn't release the memory, run it again to make sure + // it's not a spurious long-running libdragon allocations. + test_start_mem = test_stop_mem; + tests[i].fn(&ctx); + test_stop_mem = MEMORY_USED(); + } // If the test reset the hardware counter, just consider its timing // as relative to 0, so move test_stop to realign, and update the @@ -440,6 +587,12 @@ int main() { printf("Duration changed by %.1f%%\n", (float)test_diff * 100.0 / (float)test_duration); printf("(expected: %ldK, measured: %ldK)\n\n", tests[i].duration, test_duration); + } else if (test_stop_mem > test_start_mem) { + failures++; + printf("FAIL\n\n"); + debugf("MEMORY LEAK\n"); + printf("Memory leaked by %ld bytes\n", test_stop_mem - test_start_mem); + debugf("Memory leaked by %ld bytes\n", test_stop_mem - test_start_mem); } else { successes++; printf("PASS\n"); @@ -451,5 +604,5 @@ int main() { console_set_debug(true); printf("\nTestsuite finished in %02lld:%02lld\n", total_time/60, total_time%60); - printf("Passed: %d out of %d (%d skipped)\n", successes, NUM_TESTS, skipped); + printf("Passed: %d out of %d (%d skipped)\n", successes, NUM_TESTS-skipped, skipped); } diff --git a/tools/Makefile b/tools/Makefile index bfed97b45..47cb9b991 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -8,9 +8,22 @@ C_CXX_FLAGS += -O2 -pthread -Wall -Werror C_CXX_FLAGS += -I../include -MMD C_CXX_FLAGS += -fdiagnostics-color=always C_CXX_FLAGS += -Wno-unused-result -Wno-error=unknown-pragmas -Wno-sign-compare +C_CXX_FLAGS += -D__LIBDRAGON_INTERNAL_BUILD LDFLAGS += -pthread +# With mingw (even if crosscompiling), we need to link with ntdll +# and produce .exe files. Also disable -Werror: MinGW updates at +# random intervals and often emits extra warnings that break the build. +ifeq ($(findstring mingw,$(TARGET)),mingw) +C_CXX_FLAGS += -Wno-error +LDLIBS += -lntdll +EXE = .exe +else +LDLIBS = +EXE = +endif + CFLAGS += -std=gnu11 $(C_CXX_FLAGS) CXXFLAGS += -std=gnu++17 $(C_CXX_FLAGS) CXXFLAGS += -Wno-c++11-narrowing -Wno-narrowing -Wno-error=conversion-null @@ -33,15 +46,25 @@ ifeq ($(OS),Windows_NT) endif # Define a macro with an estimate of the MSYS2 environment's age so that we can check it for compatibility reasons. - MSYS2_AGE := $(shell date -d "$$(pacman -Qi msys2-runtime | sed -n 's/^Install Date *: *//p')" +%Y%m%d) - ifeq ($(MSYS2_AGE),) - $(error "Failed to determine MSYS2 age. Please make sure that pacman, date, and sed are in PATH and that msys2-runtime is installed.") - else - CFLAGS += -DMSYS2_RUNTIME_PACMAN_AGE=$(MSYS2_AGE) - CXXFLAGS += -DMSYS2_RUNTIME_PACMAN_AGE=$(MSYS2_AGE) - endif + MSYS2_AGE := $(shell date -d "$$(pacman -Qi msys2-runtime | sed -n 's/^Install Date *: *//p')" +%Y%m%d) + + ifeq ($(MSYS2_AGE),) + $(error Failed to determine MSYS2 age. Please make sure that pacman, date, and sed are in PATH and that msys2-runtime is installed.) + else + $(info MSYS2 age determined as: $(MSYS2_AGE)) + CFLAGS += -DMSYS2_RUNTIME_PACMAN_AGE=$(MSYS2_AGE) + CXXFLAGS += -DMSYS2_RUNTIME_PACMAN_AGE=$(MSYS2_AGE) + endif +else +# When cross-building, set a default value for MSYS2_RUNTIME_PACMAN_AGE + CFLAGS += -DMSYS2_RUNTIME_PACMAN_AGE=0 + CXXFLAGS += -DMSYS2_RUNTIME_PACMAN_AGE=0 endif +# Decomment to rebuild tools with ASAN +#C_CXX_FLAGS += -O0 -g -fsanitize=address +#LDFLAGS += -g -fsanitize=address + all: %.o: %.c @@ -61,6 +84,12 @@ common/shrinkler_compress.o: CFLAGS += -Wno-all -Wno-error mkfont/freetype/FreeTypeAmalgam.o: CFLAGS += -Wno-all -Wno-error mkfont/plutosvg/plutosvg-amalgam.o:CFLAGS += -Imkfont/plutosvg -Wno-unused-function -Wno-error mkfont/mkfont.o: CXXFLAGS += -Imkfont/plutosvg -Imkfont/freetype +mksprite/x264_amalgam.o: CFLAGS += -Wno-all -Wno-error -Wno-constant-logical-operand -fno-strict-aliasing +mksprite/x264_amalgam.o: CFLAGS += -Imksprite/x264 +mksprite/x264_amalgam2.o: CFLAGS += -Wno-all -Wno-error -Wno-constant-logical-operand -fno-strict-aliasing +mksprite/x264_amalgam2.o: CFLAGS += -Imksprite/x264 +audioconv64/libvadpcm.o: CFLAGS += -Iaudioconv64/vadpcm -ffp-contract=off -fno-fast-math +audioconv64/libulc.o: CFLAGS += -Wno-unused-but-set-variable -Wno-error common-clean: @@ -83,24 +112,35 @@ $(DECOMP_STUBS): -include $(wildcard common/*.d) mkasset_OBJS = mkasset/mkasset.o common/assetcomp.a -mksprite_OBJS = mksprite/mksprite.o common/assetcomp.a -audioconv64_OBJS = audioconv64/audioconv64.o -mkdfs_OBJS = mkdfs/mkdfs.o +mksprite_OBJS = mksprite/mksprite.o mksprite/mksprite_h264i.o mksprite/mksprite_bc1q.o mksprite/x264_amalgam.o mksprite/x264_amalgam2.o common/assetcomp.a mkfont_OBJS = mkfont/mkfont.o mkfont/freetype/FreeTypeAmalgam.o mkfont/plutosvg/plutosvg-amalgam.o common/assetcomp.a +mkmodel_OBJS = mkmodel/mkmodel.o common/assetcomp.a +mkmaterial_OBJS = mkmaterial/mkmaterial.o mkmaterial/mkmaterial_parse.o mkmaterial/mkmaterial_export.o common/assetcomp.a +combexpr_OBJS = mkmaterial/combexpr_cli.o mkmaterial/combexpr_disasm.o n64dso_OBJS = n64dso/n64dso.o common/assetcomp.a n64dso-extern_OBJS = n64dso/n64dso-extern.o n64dso-msym_OBJS = n64dso/n64dso-msym.o +audioconv64_OBJS = audioconv64/audioconv64.o audioconv64/conv_wav64.o audioconv64/conv_xm64.o audioconv64/conv_ym64.o audioconv64/conv_sf64.o audioconv64/conv_mid64.o audioconv64/libvadpcm.o audioconv64/libopus.o audioconv64/libsamplerate.o audioconv64/liblzh5.o audioconv64/libxm.o audioconv64/libulc.o common/binout.o common/assetcomp.a +videoconv64_OBJS = videoconv64/videoconv64.o videoconv64/vconv_analyze.o videoconv64/vconv_encode.o videoconv64/vconv_encode_mpeg1.o videoconv64/vconv_encode_h264.o videoconv64/vconv_audio.o videoconv64/vconv_subtitles.o videoconv64/vconv_utils.o videoconv64/vconv_seek.o common/binout.o common/assetcomp.a +rdpvalidate_OBJS = rdpvalidate/rdpvalidate.o +mkdfs_OBJS = mkdfs/mkdfs.o dumpdfs_OBJS = dumpdfs/dumpdfs.o +cpaktool_OBJS = cpaktool/cpaktool.o cpaktool/cpakcmds.o cpaktool/cpaklib.o cpaktool/cpakwrapper.o n64tool_OBJS = n64tool.o -n64sym_OBJS = n64sym.o +n64sym_OBJS = n64sym/n64sym.o n64sym/n64sym_cppshorten.o n64sym/n64sym_huffman.o common/assetcomp.a +n64symdump_OBJS = n64sym/n64symdump.o n64sym/n64sym_huffman.o n64sym/n64sym_shrinkler_host.o +n64metadata_OBJS = n64metadata.o ed64romconfig_OBJS = ed64romconfig.o n64elfcompress_OBJS = n64elfcompress/n64elfcompress.o common/assetcomp.a n64elfcompress/n64elfcompress.o: n64elfcompress/n64elfcompress.c $(DECOMP_STUBS) +TOOLS = n64tool n64metadata n64sym n64symdump n64elfcompress ed64romconfig audioconv64 videoconv64 mkdfs dumpdfs mkasset mksprite mkfont mkmodel mkmaterial n64dso n64dso-msym n64dso-extern rdpvalidate combexpr cpaktool -TOOLS = n64tool n64sym n64elfcompress ed64romconfig audioconv64 mkdfs dumpdfs mkasset mksprite mkfont n64dso n64dso-msym n64dso-extern - -# Define a variable that has value ".exe" on Windows and "" on other platforms -EXE = $(if $(findstring Windows,$(OS)),.exe,) +ifeq ($(OS),Windows_NT) +MANIFEST = $(if $(findstring Windows,$(OS)),manifest.o,) +manifest.o: manifest.rc tool.manifest + @echo " [RC] $@" + windres manifest.rc -O coff -o $@ +endif define TOOL_template .PHONY: $(1)-install $(1)-clean @@ -110,9 +150,13 @@ ifneq ($$($(1)_DIR),./) .PHONY: $(1) $(1): $$($(1)_BIN) endif -$$($(1)_BIN): $$($(1)_OBJS) +ifneq ($(EXE),) +.PHONY: $(1) +$(1): $$($(1)_BIN) +endif +$$($(1)_BIN): $$($(1)_OBJS) ${MANIFEST} @echo " [TOOL] $(1)" - $(CXX) $(LDFLAGS) -o $$@ $$^ + $(CXX) $(LDFLAGS) -o $$@ $$^ $(LDLIBS) $(1)-install: $(1) mkdir -p $(INSTALLDIR)/bin install -m 0755 $$($(1)_BIN) $(INSTALLDIR)/bin @@ -125,7 +169,7 @@ $(foreach tool,$(TOOLS),$(eval $(call TOOL_template,$(tool)))) all: $(TOOLS) install: $(foreach tool,$(TOOLS),$(tool)-install) clean: $(foreach tool,$(TOOLS),$(tool)-clean) common-clean - rm -f ${n64tool_OBJS} ${n64sym_OBJS} ${ed64romconfig_OBJS} + rm -f ${n64tool_OBJS} ${n64metadata_OBJS} ${n64sym_OBJS} ${n64symdump_OBJS} ${ed64romconfig_OBJS} .PHONY: all install clean ifneq ($(V),1) diff --git a/tools/audioconv64/audioconv64.c b/tools/audioconv64/audioconv64.c deleted file mode 100644 index b6fccac75..000000000 --- a/tools/audioconv64/audioconv64.c +++ /dev/null @@ -1,288 +0,0 @@ -/* - audioconv64: convert audio files to the format used by the Libdragon SDK - Written by Giovanni Bajo - - This tool is part of the Libdragon SDK. - - This is free and unencumbered software released into the public domain. - - For more information, please refer to -*/ -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include -#include - -bool flag_verbose = false; -bool flag_debug = false; - -#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ - #define LE32_TO_HOST(i) __builtin_bswap32(i) - #define HOST_TO_LE32(i) __builtin_bswap32(i) - #define LE16_TO_HOST(i) __builtin_bswap16(i) - #define HOST_TO_LE16(i) __builtin_bswap16(i) - - #define BE32_TO_HOST(i) (i) - #define HOST_TO_BE32(i) (i) - #define LE16_TO_HOST(i) (i) - #define HOST_TO_BE16(i) (i) -#else - #define BE32_TO_HOST(i) __builtin_bswap32(i) - #define HOST_TO_BE32(i) __builtin_bswap32(i) - #define BE16_TO_HOST(i) __builtin_bswap16(i) - #define HOST_TO_BE16(i) __builtin_bswap16(i) - - #define LE32_TO_HOST(i) (i) - #define HOST_TO_LE32(i) (i) - #define HOST_TO_LE16(i) (i) - #define LE16_TO_HOST(i) (i) -#endif - -__attribute__((noreturn, format(printf, 1, 2))) -void fatal(const char *str, ...) { - va_list va; - va_start(va, str); - vfprintf(stderr, str, va); - va_end(va); - exit(1); -} - -char* changeext(const char* fn, char *ext); - -/************************************************************************************ - * CONVERTERS - ************************************************************************************/ - -#include "conv_wav64.c" -#include "conv_xm64.c" -#include "conv_ym64.c" - -/************************************************************************************ - * MAIN - ************************************************************************************/ - -void usage(void) { - printf("audioconv64 -- Audio conversion tool for libdragon\n"); - printf("\n"); - printf("Usage:\n"); - printf(" audioconv64 [flags] [[flags] ..]\n"); - printf("\n"); - printf("Supported conversions:\n"); - printf(" * WAV/MP3 => WAV64 (Waveforms)\n"); - printf(" * XM => XM64 (MilkyTracker, OpenMPT)\n"); - printf(" * YM => YM64 (Arkos Tracker II)\n"); - printf("\n"); - printf("Global options:\n"); - printf(" -o / --output Specify output directory\n"); - printf(" -v / --verbose Verbose mode\n"); - printf(" -d / --debug Dump uncompressed files in output directory for debugging\n"); - printf("\n"); - printf("WAV/MP3 options:\n"); - printf(" --wav-mono Force mono output\n"); - printf(" --wav-resample Resample to a different sample rate\n"); - printf(" --wav-compress <0|1|3> Enable compression: 0=none, 1=vadpcm (default), 3=opus\n"); - printf(" --wav-loop Activate playback loop by default\n"); - printf(" --wav-loop-offset Set looping offset (in samples; default: 0)\n"); - printf("\n"); - printf("YM options:\n"); - printf(" --ym-compress Compress output file\n"); - printf("\n"); -} - -char* changeext(const char* fn, char *ext) { - char buf[4096]; - strcpy(buf, fn); - *strrchr(buf, '.') = '\0'; - strcat(buf, ext); - return strdup(buf); -} - -void convert(char *infn, char *outfn1) { - char *ext = strrchr(infn, '.'); - if (!ext) { - fprintf(stderr, "unknown file type: %s\n", infn); - return; - } - - if (strcasecmp(ext, ".wav") == 0 || strcasecmp(ext, ".aiff") == 0 || strcasecmp(ext, ".mp3") == 0) { - char *outfn = changeext(outfn1, ".wav64"); - wav_convert(infn, outfn); - free(outfn); - } else if (strcasecmp(ext, ".xm") == 0) { - char *outfn = changeext(outfn1, ".xm64"); - xm_convert(infn, outfn); - free(outfn); - } else if (strcasecmp(ext, ".ym") == 0) { - char *outfn = changeext(outfn1, ".ym64"); - ym_convert(infn, outfn); - free(outfn); - } else { - fprintf(stderr, "WARNING: ignoring unknown file: %s\n", infn); - } -} - -bool exists(const char *path) { - struct stat st; - return stat(path, &st) == 0; -} - -bool isfile(const char *path) { - struct stat st; - stat(path, &st); - return (st.st_mode & S_IFREG) != 0; -} - -bool isdir(const char *path) { - struct stat st; - stat(path, &st); - return (st.st_mode & S_IFDIR) != 0; -} - -void walkdir(char *inpath, char *outpath, void (*func)(char *, char*)) { - if (isdir(inpath)) { - // We're walking a directory. Make sure there's also a matching - // output directory or create it otherwise. - if (!isdir(outpath)) { - // If there's an obstructing file, exit with an error. - if (isfile(outpath)) { - fprintf(stderr, "ERROR: %s is a file but should be a directory\n", outpath); - return; - } - mkdir(outpath, 0777); - } - DIR* d = opendir(inpath); - struct dirent *de; - while ((de = readdir(d))) { - if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) - continue; - char *inpathsub, *outpathsub; - asprintf(&inpathsub, "%s/%s", inpath, de->d_name); - asprintf(&outpathsub, "%s/%s", outpath, de->d_name); - walkdir(inpathsub, outpathsub, func); - free(inpathsub); - free(outpathsub); - } - closedir(d); - } else if (isfile(inpath)) { - if (isdir(outpath)) { - // We support the format "audioconv64 -o " as special case - char *outpathsub; - char *basename = strrchr(inpath, '/'); - if (!basename) basename = inpath; - asprintf(&outpathsub, "%s/%s", outpath, basename); - - func(inpath, outpathsub); - - free(outpathsub); - } else { - func(inpath, outpath); - } - } else { - fprintf(stderr, "WARNING: ignoring special file: %s\n", inpath); - } -} -int main(int argc, char *argv[]) { - if (argc < 2) { - usage(); - return 1; - } - - char *outdir = "."; - - int i; - for (i=1; i 48000) { - fprintf(stderr, "invalid argument for --wav-resample: %s\n", argv[i]); - return 1; - } - } else if (!strcmp(argv[i], "--ym-compress")) { - if (++i == argc) { - fprintf(stderr, "missing argument for --ym-compress\n"); - return 1; - } - if (!strcmp(argv[i], "true") || !strcmp(argv[i], "1")) - flag_ym_compress = true; - else if (!strcmp(argv[i], "false") || !strcmp(argv[i], "0")) - flag_ym_compress = false; - else { - fprintf(stderr, "invalid boolean argument for --ym-compress: %s\n", argv[i]); - return 1; - } - } else { - fprintf(stderr, "invalid option: %s\n", argv[i]); - return 1; - } - } else { - // Positional argument. It's either a file or a directory. Convert it - if (!exists(argv[i])) { - fprintf(stderr, "ERROR: file %s does not exist\n", argv[i]); - } else { - walkdir(argv[i], outdir, convert); - } - } - } - - return 0; -} diff --git a/tools/audioconv64/audioconv64.cpp b/tools/audioconv64/audioconv64.cpp new file mode 100644 index 000000000..0d023cb41 --- /dev/null +++ b/tools/audioconv64/audioconv64.cpp @@ -0,0 +1,470 @@ +/* + audioconv64: convert audio files to the format used by the Libdragon SDK + Written by Giovanni Bajo + + This tool is part of the Libdragon SDK. + + This is free and unencumbered software released into the public domain. + + For more information, please refer to +*/ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../common/binout.h" +#include "../common/polyfill.h" +#include "../common/assetcomp.h" +#include "audioconv64.h" + +int flag_verbose = 0; +bool flag_debug = false; +static bool had_error = false; + +__attribute__((noreturn, format(printf, 1, 2))) +void fatal(const char *str, ...) { + va_list va; + va_start(va, str); + vfprintf(stderr, str, va); + fprintf(stderr, "\n"); + va_end(va); + exit(1); +} + +/************************************************************************************ + * MAIN + ************************************************************************************/ + +void usage(void) { + printf("audioconv64 -- Audio conversion tool for libdragon\n"); + printf("\n"); + printf("Usage:\n"); + printf(" audioconv64 [flags] [[flags] ..]\n"); + printf("\n"); + printf("Supported conversions:\n"); + printf(" * WAV/MP3 => WAV64 (Waveforms)\n"); + printf(" * XM => XM64 (MilkyTracker, OpenMPT)\n"); + printf(" * YM => YM64 (Arkos Tracker II)\n"); + printf(" * SF2 => SF64 (SoundFont 2)\n"); + printf(" * MID => MID64 (Standard MIDI File)\n"); + printf("\n"); + printf("Global options:\n"); + printf(" -o / --output Specify output directory\n"); + printf(" -v / --verbose Verbose mode\n"); + printf(" -d / --debug Dump uncompressed files in output directory for debugging\n"); + printf(" -h / --help Show this help message\n"); + printf(" --help-compress Show detailed help for compression options\n"); + printf("\n"); + printf("WAV/MP3 options:\n"); + printf(" --wav-mono Force mono output\n"); + printf(" --wav-resample Resample to a different sample rate\n"); + printf(" --wav-compress <0|1|2|3> Enable compression: 0=none, 1=vadpcm (default), 2=ulc, 3=opus\n"); + printf(" --wav-loop Activate playback loop by default\n"); + printf(" --wav-loop-offset Set looping offset (in samples; default: 0)\n"); + printf(" --wav-seek Enable seeking support:\n"); + printf(" - if SEC is a float, add a seekpoint every SEC seconds\n"); + printf(" - if FILE, read a list of seekpoints (one per line):\n"); + printf(" * integer sample offsets, or\n"); + printf(" * timestamps in [hh:]mm:ss[.mmm] format\n"); + printf("\n"); + printf("XM options:\n"); + printf(" --xm-8bit Convert all samples to 8-bit\n"); + printf(" --xm-ext-samples Export samples externally as wav64 files in the specified directory\n"); + printf(" --xm-compress <0|1> Compression level for XM samples (default: 1=vadpcm)\n"); + printf(" --xm-compress-data <0..3> Compression level for XM binary data (default: 1)\n"); + printf("\n"); + printf("YM options:\n"); + printf(" --ym-compress Compress output file\n"); + printf("\n"); + printf("SF2 options:\n"); + printf(" --sf-compress <0|1> Compression for SF samples (default: 1=vadpcm)\n"); + printf("\n"); + printf("MID options:\n"); + printf(" --mid-compress <0..3> Asset compression level for MID64 (default: 1)\n"); + printf("\n"); +} + +void usage_compress(void) +{ + printf("audioconv64 -- Audio conversion tool for libdragon\n"); + printf("\n"); + printf("This help describes the compression options that can be passed to --wav-compress or --xm-compress:\n"); + printf("\n"); + printf(" none (or 0) No compression, store raw samples\n"); + printf(" vadpcm (or 1) Use RSP-optimized VADPCM codec. This is the default\n"); + printf(" ulc (or 2) Use RSP-optimized ULC codec. A simple and fast transform codec.\n"); + printf(" Worse compression than Opus, but better runtime performance.\n"); + printf(" opus (or 3) Use RSP-optimized Opus codec. Slower at runtime, smaller disk size\n"); + printf(" (unsupported for xm64)\n"); + printf("\n"); + printf("It is also possible to specify additional compression flags, separated by commas:\n"); + printf("\n"); + printf(" vadpcm,huffman=true Enable Huffman compression in VADPCM.\n"); + printf(" (default: true for wav64, false for xm64))\n"); + printf(" vadpcm,bits=<2|3|4> Specify how many bits per sample use in VADPCM coding (default: 4)\n"); + printf(" For values less than 4, huffman compression should be enabled.\n"); + printf(" ulc,mode= Select ULC rate-control mode (default: vbr)\n"); + printf(" ulc,quality=<1..100> Set ULC VBR quality (default: 50)\n"); + printf(" ulc,bitrate= Set ULC ABR/CBR bitrate (default: 64)\n"); + printf("\n"); +} + +char* changeext(const char* fn, const char *ext) { + char buf[4096]; + strcpy(buf, fn); + *strrchr(buf, '.') = '\0'; + strcat(buf, ext); + return strdup(buf); +} + +void convert(const char *infn, const char *outfn1) { + const char *ext = strrchr(infn, '.'); + if (!ext) { + fprintf(stderr, "unknown file type: %s\n", infn); + had_error = true; + return; + } + + if (strcasecmp(ext, ".wav") == 0 || strcasecmp(ext, ".aiff") == 0 || strcasecmp(ext, ".mp3") == 0) { + char *outfn = changeext(outfn1, ".wav64"); + if (wav_convert(infn, outfn) != 0) had_error = true; + free(outfn); + } else if (strcasecmp(ext, ".xm") == 0) { + char *outfn = changeext(outfn1, ".xm64"); + if (xm_convert(infn, outfn) != 0) had_error = true; + free(outfn); + } else if (strcasecmp(ext, ".ym") == 0) { + char *outfn = changeext(outfn1, ".ym64"); + if (ym_convert(infn, outfn) != 0) had_error = true; + free(outfn); + } else if (strcasecmp(ext, ".sf2") == 0) { + char *outfn = changeext(outfn1, ".sf64"); + if (sf_convert(infn, outfn) != 0) had_error = true; + free(outfn); + } else if (strcasecmp(ext, ".mid") == 0 || strcasecmp(ext, ".midi") == 0) { + char *outfn = changeext(outfn1, ".mid64"); + if (mid_convert(infn, outfn) != 0) had_error = true; + free(outfn); + } else { + fprintf(stderr, "WARNING: ignoring unknown file: %s\n", infn); + } +} + +bool exists(const char *path) { + struct stat st; + return stat(path, &st) == 0; +} + +bool isfile(const char *path) { + struct stat st; + stat(path, &st); + return (st.st_mode & S_IFREG) != 0; +} + +bool isdir(const char *path) { + struct stat st; + stat(path, &st); + return (st.st_mode & S_IFDIR) != 0; +} + +void walkdir(char *inpath, const char *outpath, void (*func)(const char *, const char*)) { + if (isdir(inpath)) { + // We're walking a directory. Make sure there's also a matching + // output directory or create it otherwise. + if (!isdir(outpath)) { + // If there's an obstructing file, exit with an error. + if (isfile(outpath)) { + fprintf(stderr, "ERROR: %s is a file but should be a directory\n", outpath); + had_error = true; + return; + } + mkdir(outpath, 0777); + } + DIR* d = opendir(inpath); + struct dirent *de; + while ((de = readdir(d))) { + if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) + continue; + char *inpathsub, *outpathsub; + asprintf(&inpathsub, "%s/%s", inpath, de->d_name); + asprintf(&outpathsub, "%s/%s", outpath, de->d_name); + walkdir(inpathsub, outpathsub, func); + free(inpathsub); + free(outpathsub); + } + closedir(d); + } else if (isfile(inpath)) { + if (isdir(outpath)) { + // We support the format "audioconv64 -o " as special case + char *outpathsub; + char *basename = strrchr(inpath, '/'); + if (!basename) basename = inpath; + asprintf(&outpathsub, "%s/%s", outpath, basename); + + func(inpath, outpathsub); + + free(outpathsub); + } else { + func(inpath, outpath); + } + } else { + fprintf(stderr, "WARNING: ignoring special file: %s\n", inpath); + } +} +int main(int argc, char *argv[]) { + winconsole_utf8(); + + if (argc < 2) { + usage(); + return 1; + } + + const char *outdir = "."; + + int i; + for (i=1; i 4) { + fprintf(stderr, "invalid value for compression option 'bits': %s\n", value); + return 1; + } + } else if (!strcmp(key, "mode")) { + if (*flag_compress != 2) { + fprintf(stderr, "compression option 'mode' only allowed for ULC (%s ulc)\n", argv[i-1]); + return 1; + } + if (!strcmp(value, "vbr")) flag_wav_compress_ulc_mode = ULC_MODE_VBR; + else if (!strcmp(value, "abr")) flag_wav_compress_ulc_mode = ULC_MODE_ABR; + else if (!strcmp(value, "cbr")) flag_wav_compress_ulc_mode = ULC_MODE_CBR; + else { + fprintf(stderr, "invalid value for ULC compression option 'mode': %s\n", value); + return 1; + } + } else if (!strcmp(key, "quality")) { + char extra; + if (*flag_compress != 2 || sscanf(value, "%f%c", &flag_wav_compress_ulc_quality, &extra) != 1 || + flag_wav_compress_ulc_quality < 1.0f || flag_wav_compress_ulc_quality > 100.0f) { + fprintf(stderr, "invalid ULC quality (expected 1..100): %s\n", value); + return 1; + } + } else if (!strcmp(key, "bitrate")) { + char extra; + if (*flag_compress != 2 || sscanf(value, "%f%c", &flag_wav_compress_ulc_bitrate, &extra) != 1 || + flag_wav_compress_ulc_bitrate <= 0.0f) { + fprintf(stderr, "invalid ULC bitrate: %s\n", value); + return 1; + } + } else { + fprintf(stderr, "invalid option for %s: %s\n", key, argv[i-1]); + return 1; + } + } + } else if (!strcmp(argv[i], "--wav-resample")) { + if (++i == argc) { + fprintf(stderr, "missing argument for --wav-resample\n"); + return 1; + } + flag_wav_resample = atoi(argv[i]); + if (flag_wav_resample < 1 || flag_wav_resample > 48000) { + fprintf(stderr, "invalid argument for --wav-resample: %s\n", argv[i]); + return 1; + } + } else if (!strcmp(argv[i], "--wav-seek")) { + if (++i == argc) { + fprintf(stderr, "missing argument for --wav-seek\n"); + return 1; + } + const char *param = argv[i]; + char *end = NULL; + double sec = strtod(param, &end); + if (end != param && *end == '\0' && sec > 0.0) { + flag_wav_seek_interval_sec = sec; + } else { + // Defer parsing until after resampling so timestamps can be converted using the final sample rate. + flag_wav_seek_file = param; + } + } else if (!strcmp(argv[i], "--xm-8bit")) { + flag_xm_8bit = true; + } else if (!strcmp(argv[i], "--xm-ext-samples")) { + if (++i == argc) { + fprintf(stderr, "missing argument for --xm-ext-samples\n"); + return 1; + } + flag_xm_extsampledir = argv[i]; + mkdir(flag_xm_extsampledir, 0777); + } else if (!strcmp(argv[i], "--xm-compress-data")) { + if (++i == argc) { + fprintf(stderr, "missing argument for --xm-compress\n"); + return 1; + } + flag_xm_compress_meta = atoi(argv[i]); + if (flag_xm_compress_meta < 0 || flag_xm_compress_meta > MAX_COMPRESSION) { + fprintf(stderr, "invalid argument for --xm-compress: %s\n", argv[i]); + return 1; + } + } else if (!strcmp(argv[i], "--ym-compress")) { + if (++i == argc) { + fprintf(stderr, "missing argument for --ym-compress\n"); + return 1; + } + if (!strcmp(argv[i], "true") || !strcmp(argv[i], "1")) + flag_ym_compress = true; + else if (!strcmp(argv[i], "false") || !strcmp(argv[i], "0")) + flag_ym_compress = false; + else { + fprintf(stderr, "invalid boolean argument for --ym-compress: %s\n", argv[i]); + return 1; + } + } else if (!strcmp(argv[i], "--sf-compress")) { + if (++i == argc) { + fprintf(stderr, "missing argument for --sf-compress\n"); + return 1; + } + if (!strcmp(argv[i], "0") || !strcmp(argv[i], "none")) + flag_sf_compress = 0; + else if (!strcmp(argv[i], "1") || !strcmp(argv[i], "vadpcm")) + flag_sf_compress = 1; + else { + fprintf(stderr, "invalid argument for --sf-compress: %s\n", argv[i]); + return 1; + } + } else if (!strcmp(argv[i], "--mid-compress")) { + if (++i == argc) { + fprintf(stderr, "missing argument for --mid-compress\n"); + return 1; + } + flag_mid_compress = atoi(argv[i]); + if (flag_mid_compress < 0 || flag_mid_compress > MAX_COMPRESSION) { + fprintf(stderr, "invalid argument for --mid-compress: %s\n", argv[i]); + return 1; + } + } else { + fprintf(stderr, "invalid option: %s\n", argv[i]); + return 1; + } + } else { + // Positional argument. It's either a file or a directory. Convert it + if (!exists(argv[i])) { + fprintf(stderr, "ERROR: file %s does not exist\n", argv[i]); + had_error = true; + } else { + walkdir(argv[i], outdir, convert); + } + } + } + + return had_error ? 1 : 0; +} diff --git a/tools/audioconv64/audioconv64.h b/tools/audioconv64/audioconv64.h new file mode 100644 index 000000000..caf722d96 --- /dev/null +++ b/tools/audioconv64/audioconv64.h @@ -0,0 +1,106 @@ +/* + audioconv64: convert audio files to the format used by the Libdragon SDK + Written by Giovanni Bajo + + This tool is part of the Libdragon SDK. + + This is free and unencumbered software released into the public domain. + + For more information, please refer to +*/ +#pragma once + +#include +#include +#include + +// C headers in src/ use _Static_assert; map to C++ static_assert. +#define _Static_assert static_assert + +#include "vadpcm_pack.h" + +#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + #define LE32_TO_HOST(i) __builtin_bswap32(i) + #define HOST_TO_LE32(i) __builtin_bswap32(i) + #define LE16_TO_HOST(i) __builtin_bswap16(i) + #define HOST_TO_LE16(i) __builtin_bswap16(i) + + #define BE32_TO_HOST(i) (i) + #define HOST_TO_BE32(i) (i) + #define LE16_TO_HOST(i) (i) + #define HOST_TO_BE16(i) (i) +#else + #define BE32_TO_HOST(i) __builtin_bswap32(i) + #define HOST_TO_BE32(i) __builtin_bswap32(i) + #define BE16_TO_HOST(i) __builtin_bswap16(i) + #define HOST_TO_BE16(i) __builtin_bswap16(i) + + #define LE32_TO_HOST(i) (i) + #define HOST_TO_LE32(i) (i) + #define HOST_TO_LE16(i) (i) + #define LE16_TO_HOST(i) (i) +#endif + +enum ulc_mode_t { ULC_MODE_VBR, ULC_MODE_ABR, ULC_MODE_CBR }; + +typedef struct { + int16_t *samples; // Samples (always 16-bit signed) + int cnt; // Number of audio frames + int channels; // Number of channels + int bitsPerSample; // Original bits per sample in input file + int sampleRate; + bool looping; + int loopOffset; // Exclusive-start of the sustain loop (samples) + int loopEnd; // Exclusive end of the sustain loop (0 ⇒ cnt) + std::vector skipPoints; // Skip points in the waveform + /** VADPCM: compressed frames to keep in RDRAM for note-on (0 = none). */ + uint8_t attack_frames; + /** VADPCM: preload whole sample into RDRAM (no streaming). */ + bool resident; +} wav_data_t; + +// Global options +extern int flag_verbose; +extern bool flag_debug; + +// WAV options +extern bool flag_wav_looping; +extern int flag_wav_looping_offset; +extern int flag_wav_compress; +extern int flag_wav_compress_vadpcm_huffman; +extern int flag_wav_compress_vadpcm_bits; +extern ulc_mode_t flag_wav_compress_ulc_mode; +extern float flag_wav_compress_ulc_bitrate; +extern float flag_wav_compress_ulc_quality; +extern int flag_wav_resample; +extern double flag_wav_seek_interval_sec; +extern const char *flag_wav_seek_file; +extern bool flag_wav_mono; + +// XM options +extern int flag_xm_compress_meta; +extern int flag_xm_compress_samples; +extern bool flag_xm_8bit; +extern const char *flag_xm_extsampledir; + +// YM options +extern bool flag_ym_compress; + +// SF options +extern int flag_sf_compress; + +// MID options +extern int flag_mid_compress; + +__attribute__((noreturn, format(printf, 1, 2))) +void fatal(const char *str, ...); + +char* changeext(const char* fn, const char *ext); + +int wav_convert(const char *infn, const char *outfn); +int xm_convert(const char *infn, const char *outfn); +int ym_convert(const char *infn, const char *outfn); +int sf_convert(const char *infn, const char *outfn); +int mid_convert(const char *infn, const char *outfn); + +bool wav64_write(const char *infn, const char *outfn, FILE *out, wav_data_t* wav, int format); diff --git a/tools/audioconv64/conv_mid64.cpp b/tools/audioconv64/conv_mid64.cpp new file mode 100644 index 000000000..875cf315f --- /dev/null +++ b/tools/audioconv64/conv_mid64.cpp @@ -0,0 +1,430 @@ +/* + conv_mid64: convert Standard MIDI Files to MID64 format + Written by Giovanni Bajo + + This tool is part of the Libdragon SDK. + + This is free and unencumbered software released into the public domain. + + For more information, please refer to +*/ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include + +#include "../common/binout.h" +#include "../common/utils.h" +#include "../common/assetcomp.h" +#include "audioconv64.h" + +int flag_mid_compress = DEFAULT_COMPRESSION; + +#define MID64_MAGIC "MD64" +#define MID64_VERSION 1 +#define MID64_HEADER_SIZE 32 +#define MID64_OP_SET_TEMPO 0xF0 +#define MID64_OP_GM1_SYSTEM_ON 0xF1 +#define MID64_OP_END 0xFF + +#define MIDI_STATUS_MASK 0xF0 +#define MIDI_CHANNEL_MASK 0x0F +#define MIDI_STATUS_BIT 0x80 +#define MIDI_NOTE_OFF 0x80 +#define MIDI_NOTE_ON 0x90 +#define MIDI_POLY_PRESSURE 0xA0 +#define MIDI_CONTROL_CHANGE 0xB0 +#define MIDI_PROGRAM_CHANGE 0xC0 +#define MIDI_CHANNEL_PRESSURE 0xD0 +#define MIDI_PITCH_BEND 0xE0 +#define MIDI_SYSEX 0xF0 +#define MIDI_SYSEX_ESCAPE 0xF7 +#define MIDI_META 0xFF +#define MIDI_META_EOT 0x2F +#define MIDI_META_TEMPO 0x51 + +enum class midi_event_type { CHANNEL, TEMPO, GM1_SYSTEM_ON }; + +struct midi_event_t { + uint64_t tick; + uint32_t sequence; + uint16_t track; + midi_event_type type; + uint8_t status; + uint8_t data1; + uint8_t data2; + uint32_t tempo_us; +}; + +struct mid_stats_t { + int format; + int tracks; + int ppqn; + int input_events; + int output_events; + int tempo_changes; + int gm1_system_on; + int sysex_stripped; + int meta_stripped; + long input_size; + long events_size; + long total_size; +}; + +static const uint8_t *g_end; + +static uint8_t read_u8(const uint8_t *&p) +{ + if (p >= g_end) fatal("ERROR: truncated MIDI file"); + return *p++; +} + +static uint16_t read_u16(const uint8_t *&p) +{ + uint16_t v = read_u8(p) << 8; + return v | read_u8(p); +} + +static uint32_t read_u32(const uint8_t *&p) +{ + uint32_t v = (uint32_t)read_u8(p) << 24; + v |= (uint32_t)read_u8(p) << 16; + v |= (uint32_t)read_u8(p) << 8; + return v | read_u8(p); +} + +static uint32_t read_vlq(const uint8_t *&p) +{ + uint32_t v = 0; + for (int i = 0; i < 4; i++) { + uint8_t b = read_u8(p); + v = (v << 7) | (b & 0x7f); + if (!(b & 0x80)) return v; + } + fatal("ERROR: MIDI VLQ longer than 4 bytes"); +} + +static void write_vlq(FILE *f, uint32_t v) +{ + uint8_t buf[4]; + int n = 0; + buf[n++] = v & 0x7f; + while (v >>= 7) + buf[n++] = (v & 0x7f) | 0x80; + while (n--) + w8(f, buf[n]); +} + +static int channel_data_len(uint8_t status) +{ + switch (status & MIDI_STATUS_MASK) { + case MIDI_PROGRAM_CHANGE: + case MIDI_CHANNEL_PRESSURE: + return 1; + case MIDI_NOTE_OFF: + case MIDI_NOTE_ON: + case MIDI_POLY_PRESSURE: + case MIDI_CONTROL_CHANGE: + case MIDI_PITCH_BEND: + return 2; + default: + return -1; + } +} + +static void parse_track(const uint8_t *&p, uint32_t len, unsigned track, + std::vector &events, uint64_t &duration, mid_stats_t &st) +{ + const uint8_t *end = p + len; + if (end > g_end) fatal("ERROR: track extends past end of file"); + + uint64_t tick = 0; + uint8_t running = 0; + uint32_t seq = 0; + // Restrict the reader to this track's bytes for the duration of the loop. + const uint8_t *save_end = g_end; + g_end = end; + + while (p < end) { + // Advance absolute time by the event's delta-tick VLQ. + uint32_t delta = read_vlq(p); + tick += delta; + + uint8_t b = read_u8(p); + + // Meta events: keep Set Tempo, strip everything else; EOT ends the track. + if (b == MIDI_META) { + uint8_t type = read_u8(p); + uint32_t mlen = read_vlq(p); + if (p + mlen > end) fatal("ERROR: truncated meta event"); + if (type == MIDI_META_EOT) { + if (mlen != 0) fatal("ERROR: invalid End of Track"); + duration = std::max(duration, tick); + running = 0; + st.meta_stripped++; + break; + } + if (type == MIDI_META_TEMPO) { + if (mlen != 3) fatal("ERROR: invalid Set Tempo length"); + uint32_t tempo = ((uint32_t)p[0] << 16) | ((uint32_t)p[1] << 8) | p[2]; + if (tempo == 0) fatal("ERROR: tempo is zero"); + events.push_back({tick, seq++, (uint16_t)track, midi_event_type::TEMPO, + 0, 0, 0, tempo}); + st.tempo_changes++; + } else { + st.meta_stripped++; + } + p += mlen; + running = 0; // meta cancels running status + continue; + } + + // SysEx: keep GM1 System On (F0 7E dd 09 01 F7); strip the rest. + if (b == MIDI_SYSEX || b == MIDI_SYSEX_ESCAPE) { + uint32_t slen = read_vlq(p); + if (p + slen > end) fatal("ERROR: truncated SysEx"); + bool gm1 = b == MIDI_SYSEX && slen >= 4 + && p[0] == 0x7E && p[2] == 0x09 && p[3] == 0x01; + if (gm1) { + events.push_back({tick, seq++, (uint16_t)track, + midi_event_type::GM1_SYSTEM_ON, 0, 0, 0, 0}); + st.gm1_system_on++; + } else { + st.sysex_stripped++; + } + p += slen; + running = 0; // SysEx cancels running status + continue; + } + + // Channel voice: status byte or reuse previous running status. + uint8_t status; + uint8_t data1; + if (b & MIDI_STATUS_BIT) { + status = b; + if (channel_data_len(status) < 0) + fatal("ERROR: unsupported MIDI status 0x%02x", status); + running = status; + data1 = read_u8(p); + } else { + if (!running) + fatal("ERROR: MIDI data byte without running status"); + status = running; + data1 = b; + } + + // Consume the remaining data byte(s) and validate 7-bit MIDI data. + int dlen = channel_data_len(status); + uint8_t data2 = 0; + if (dlen == 2) + data2 = read_u8(p); + if (data1 & MIDI_STATUS_BIT) + fatal("ERROR: invalid MIDI data byte 0x%02x", data1); + if (dlen == 2 && (data2 & MIDI_STATUS_BIT)) + fatal("ERROR: invalid MIDI data byte 0x%02x", data2); + + // Normalize note-on with velocity 0 into an explicit note-off. + if ((status & MIDI_STATUS_MASK) == MIDI_NOTE_ON && data2 == 0) { + status = MIDI_NOTE_OFF | (status & MIDI_CHANNEL_MASK); + data2 = 0; + } + + events.push_back({tick, seq++, (uint16_t)track, midi_event_type::CHANNEL, + status, data1, data2, 0}); + } + + if (p != end) fatal("ERROR: track length mismatch"); + g_end = save_end; + duration = std::max(duration, tick); +} + +int mid_convert(const char *infn, const char *outfn) +{ + // Slurp the whole SMF into memory; conversion is host-side only. + std::vector data = slurp(infn); + if (data.empty()) fatal("ERROR: cannot open: %s", infn); + + const uint8_t *p = data.data(); + g_end = data.data() + data.size(); + + // Parse and validate the MThd header (format, track count, PPQN). + if (data.size() < 14 || memcmp(p, "MThd", 4) != 0) + fatal("ERROR: not a Standard MIDI File: %s", infn); + p += 4; + uint32_t hdr_len = read_u32(p); + if (hdr_len < 6) fatal("ERROR: invalid MIDI header length"); + uint16_t format = read_u16(p); + uint16_t ntrks = read_u16(p); + uint16_t division = read_u16(p); + p += hdr_len - 6; + + if (format > 1) + fatal("ERROR: MIDI format %u not supported (only 0 and 1)", format); + if (division & 0x8000) + fatal("ERROR: SMPTE timing not supported"); + if (division == 0) + fatal("ERROR: invalid PPQN"); + if (ntrks == 0) + fatal("ERROR: MIDI file has no tracks"); + + mid_stats_t st = {}; + st.format = format; + st.tracks = ntrks; + st.ppqn = division; + st.input_size = (long)data.size(); + + // Parse every MTrk into a shared event list (absolute ticks). + std::vector events; + uint64_t duration = 0; + for (unsigned t = 0; t < ntrks; t++) { + if (p + 8 > g_end) fatal("ERROR: truncated track header"); + if (memcmp(p, "MTrk", 4) != 0) + fatal("ERROR: expected MTrk chunk"); + p += 4; + uint32_t tlen = read_u32(p); + parse_track(p, tlen, t, events, duration, st); + } + + // Merge tracks: sort by tick, then track index, then in-track order. + st.input_events = (int)events.size() + st.meta_stripped + st.sysex_stripped; + std::sort(events.begin(), events.end(), [](const midi_event_t &a, const midi_event_t &b) { + if (a.tick != b.tick) return a.tick < b.tick; + if (a.track != b.track) return a.track < b.track; + return a.sequence < b.sequence; + }); + + // Wall-clock length via the tempo map (same rule as the runtime player: + // a SET_TEMPO applies to the following delta). + uint32_t tempo_us = 500000; + uint64_t cur_us_tick = 0; + uint64_t us_rem = 0; + uint64_t duration_us = 0; + // (us_rem + delta * tempo_us) / division without __int128 (MinGW). + // division ≤ 0x7FFF and tempo_us ≤ 0xFFFFFF, so the tick remainder + // times tempo always fits in 64 bits; only the quotient term can + // overflow on pathological files, in which case we saturate. + auto add_us = [&](uint64_t delta) { + uint64_t q = delta / division; + uint64_t r = delta % division; + if (q && tempo_us > UINT64_MAX / q) + duration_us = UINT64_MAX; + else if (duration_us > UINT64_MAX - q * tempo_us) + duration_us = UINT64_MAX; + else + duration_us += q * tempo_us; + uint64_t num = us_rem + r * tempo_us; + duration_us += num / division; + us_rem = num % division; + }; + for (auto &ev : events) { + add_us(ev.tick - cur_us_tick); + cur_us_tick = ev.tick; + if (ev.type == midi_event_type::TEMPO) + tempo_us = ev.tempo_us; + } + add_us(duration - cur_us_tick); + uint64_t ms64 = duration_us / 1000; + uint32_t duration_ms = ms64 > 0xffffffffu ? 0xffffffffu : (uint32_t)ms64; + + // Build the uncompressed MID64 in a temp file, then wrap with asset compression. + FILE *tmp = tmpfile(); + if (!tmp) fatal("ERROR: cannot create temporary file"); + + wa(tmp, MID64_MAGIC, 4); + w8(tmp, MID64_VERSION); + w8(tmp, 0); // flags + w16(tmp, division); + w32(tmp, MID64_HEADER_SIZE); // events_offset + int events_size_pos = w32_placeholder(tmp); + int num_events_pos = w32_placeholder(tmp); + w32(tmp, duration > 0xffffffffu ? 0xffffffffu : (uint32_t)duration); + w32(tmp, duration_ms); + w32(tmp, 0); // reserved + + // Encode the merged stream: delta VLQ + event, with running status. + uint64_t cur_tick = 0; + uint8_t running = 0; + uint32_t num_events = 0; + long events_start = ftell(tmp); + + for (auto &ev : events) { + uint64_t delta64 = ev.tick - cur_tick; + if (delta64 > 0x0fffffffu) + fatal("ERROR: delta tick too large for MIDI VLQ"); + write_vlq(tmp, (uint32_t)delta64); + cur_tick = ev.tick; + + if (ev.type == midi_event_type::TEMPO) { + w8(tmp, MID64_OP_SET_TEMPO); + w8(tmp, (ev.tempo_us >> 16) & 0xff); + w8(tmp, (ev.tempo_us >> 8) & 0xff); + w8(tmp, ev.tempo_us & 0xff); + running = 0; + } else if (ev.type == midi_event_type::GM1_SYSTEM_ON) { + w8(tmp, MID64_OP_GM1_SYSTEM_ON); + running = 0; + } else { + if (ev.status != running) { + w8(tmp, ev.status); + running = ev.status; + } + w8(tmp, ev.data1); + if (channel_data_len(ev.status) == 2) + w8(tmp, ev.data2); + } + num_events++; + } + + // Trailing END at the max EOT tick (keeps silence after the last note). + uint64_t end_delta = duration - cur_tick; + if (end_delta > 0x0fffffffu) + fatal("ERROR: delta tick too large for MIDI VLQ"); + write_vlq(tmp, (uint32_t)end_delta); + w8(tmp, MID64_OP_END); + num_events++; + + long events_end = ftell(tmp); + uint32_t events_size = (uint32_t)(events_end - events_start); + w32_at(tmp, events_size_pos, events_size); + w32_at(tmp, num_events_pos, num_events); + + std::vector plain = slurp(tmp); + fclose(tmp); + + FILE *out = fopen(outfn, "wb"); + if (!out) fatal("ERROR: cannot create: %s", outfn); + placeholder_clear(); + // Large window: MID64 is always fully preloaded via asset_load(). + int cmp_size = asset_compress_mem(plain.data(), (int)plain.size(), out, + flag_mid_compress, 256 * 1024, NULL); + fclose(out); + if (cmp_size < 0) fatal("ERROR: compression failed: %s", outfn); + + st.output_events = (int)num_events; + st.events_size = events_size; + st.total_size = cmp_size; + + if (flag_verbose) { + fprintf(stderr, "Converting: %s => %s\n", infn, outfn); + fprintf(stderr, " MIDI format: %d\n", st.format); + fprintf(stderr, " Tracks: %d\n", st.tracks); + fprintf(stderr, " PPQN: %d\n", st.ppqn); + fprintf(stderr, " Duration: %u ms (%u ticks)\n", + duration_ms, duration > 0xffffffffu ? 0xffffffffu : (uint32_t)duration); + fprintf(stderr, " Input events: %d\n", st.input_events); + fprintf(stderr, " Output events: %d\n", st.output_events); + fprintf(stderr, " Tempo changes: %d\n", st.tempo_changes); + fprintf(stderr, " GM1 System On: %d\n", st.gm1_system_on); + fprintf(stderr, " SysEx stripped: %d\n", st.sysex_stripped); + fprintf(stderr, " Meta events stripped: %d\n", st.meta_stripped); + fprintf(stderr, " Input MIDI: %.1f KiB\n", st.input_size / 1024.0); + fprintf(stderr, " MID64 uncompressed: %.1f KiB\n", plain.size() / 1024.0); + fprintf(stderr, " MID64 compressed: %.1f KiB (level %d)\n", + st.total_size / 1024.0, flag_mid_compress); + } + + return 0; +} diff --git a/tools/audioconv64/conv_sf64.cpp b/tools/audioconv64/conv_sf64.cpp new file mode 100644 index 000000000..19196e48c --- /dev/null +++ b/tools/audioconv64/conv_sf64.cpp @@ -0,0 +1,407 @@ +/* + conv_sf64: convert SoundFont 2 files to SF64 format + Written by Giovanni Bajo + + This tool is part of the Libdragon SDK. + + This is free and unencumbered software released into the public domain. + + For more information, please refer to +*/ +/* + * SF2 is loaded with a vendored TinySoundFont (MIT). We use its resolved + * preset/region tables (not the public render API), copy them into SF64 + * structures, and embed each unique sample slice as a WAV64. + */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include +#include + +#include "../common/binout.h" +#include "../common/polyfill.h" +#include "../common/utils.h" +#include "../common/assetcomp.h" +#include "../common/crc64.c" +#include "audioconv64.h" +#include "../../src/audio/sf64_internal.h" + +#define TSF_IMPLEMENTATION +#include "tinysoundfont/tsf.h" + +int flag_sf_compress = 1; + +/** CRC-64 over PCM + playback variant params (rate/channels/loop). */ +static uint64_t hash_sample_variant(const int16_t *pcm, int cnt, int rate, int ch, + int loop_start, int loop_end) +{ + uint64_t h = 0; + h = crc64(h, (const unsigned char *)pcm, (uint64_t)cnt * ch * sizeof(int16_t)); + h = crc64(h, (const unsigned char *)&rate, sizeof(rate)); + h = crc64(h, (const unsigned char *)&ch, sizeof(ch)); + h = crc64(h, (const unsigned char *)&loop_start, sizeof(loop_start)); + h = crc64(h, (const unsigned char *)&loop_end, sizeof(loop_end)); + return h; +} + +static int16_t secs_to_timecents(float sec) +{ + if (sec <= 0.0f) return -12000; + float tc = 1200.0f * log2f(sec); + if (tc < -12000.0f) return -12000; + if (tc > 8000.0f) return 8000; + return (int16_t)lroundf(tc); +} + +/** Centibels of attenuation → linear gain in [0,1]. */ +static float centibels_to_gain(int cb) +{ + if (cb <= 0) return 1.0f; + if (cb >= 1440) return 0.0f; + return powf(10.0f, -cb / 200.0f); +} + +/** Hold/decay may still be in timecents when keynum scaling is active. */ +static int16_t env_time_field(float v, float keynum_scale) +{ + if (keynum_scale != 0.0f) + return (int16_t)lroundf(v); + return secs_to_timecents(v); +} + +static void copy_amp_env(sf64_envelope_t *dst, const struct tsf_envelope *src) +{ + dst->delay_timecents = secs_to_timecents(src->delay); + dst->attack_timecents = secs_to_timecents(src->attack); + dst->hold_timecents = env_time_field(src->hold, src->keynumToHold); + dst->decay_timecents = env_time_field(src->decay, src->keynumToDecay); + dst->sustain_gain = src->sustain < 0.0f ? 0.0f : + (src->sustain > 1.0f ? 1.0f : src->sustain); + dst->release_timecents = secs_to_timecents(src->release); + dst->keynum_to_hold = (int16_t)lroundf(src->keynumToHold); + dst->keynum_to_decay = (int16_t)lroundf(src->keynumToDecay); +} + +static bool env_used(const struct tsf_envelope *e) +{ + if (e->delay > 0 || e->attack > 0 || e->release > 0) return true; + if (e->keynumToHold || e->keynumToDecay) return true; + if (e->keynumToHold == 0 && e->hold > 0) return true; + if (e->keynumToDecay == 0 && e->decay > 0) return true; + return e->sustain < 0.999f; +} + +static void note_unsupported(const char *preset, const char *feat, + std::map> &warns) +{ + warns[feat].insert(preset); +} + +static void flush_unsupported(const std::map> &warns) +{ + const size_t max_names = flag_verbose >= 2 ? SIZE_MAX : 10; + for (auto &kv : warns) { + fprintf(stderr, "WARNING: SF2 ignoring unsupported %s (%zu instruments):", + kv.first.c_str(), kv.second.size()); + const char *sep = " "; + size_t n = 0; + for (auto &name : kv.second) { + if (n >= max_names) { + fprintf(stderr, ", ..."); + break; + } + fprintf(stderr, "%s%s", sep, name.c_str()); + sep = ", "; + n++; + } + fprintf(stderr, "\n"); + } +} + +static void write_env(FILE *f, const sf64_envelope_t *e) +{ + w16(f, e->delay_timecents); + w16(f, e->attack_timecents); + w16(f, e->hold_timecents); + w16(f, e->decay_timecents); + wf32(f, e->sustain_gain); + w16(f, e->release_timecents); + w16(f, e->keynum_to_hold); + w16(f, e->keynum_to_decay); +} + +int sf_convert(const char *infn, const char *outfn) +{ + if (flag_wav_compress_vadpcm_huffman < 0) + flag_wav_compress_vadpcm_huffman = 0; + + tsf *font = tsf_load_filename(infn); + if (!font) fatal("ERROR: cannot load SF2: %s\n", infn); + + std::vector presets; + std::vector regions; + std::vector samples; + std::vector names; + std::map sample_by_hash; + std::map> warn_feats; + int64_t raw_pcm_bytes = 0; + int64_t unique_pcm_bytes = 0; + int64_t embedded_wav = 0; + + FILE *out = fopen(outfn, "wb"); + if (!out) fatal("ERROR: cannot create: %s\n", outfn); + placeholder_clear(); + + wa(out, SF64_ID, 4); + w8(out, SF64_VERSION); + w8(out, 0); // flags + w16_placeholderf(out, "num_presets"); + w16_placeholderf(out, "num_regions"); + w16_placeholderf(out, "num_samples"); + w32_placeholderf(out, "metadata_offset"); + w32_placeholderf(out, "metadata_size"); + w32_placeholderf(out, "sample_data_offset"); + + walign(out, 2); + placeholder_set(out, "sample_data_offset"); + + for (int pi = 0; pi < font->presetNum; pi++) { + struct tsf_preset *p = &font->presets[pi]; + sf64_preset_t sp = {}; + sp.bank = p->bank; + sp.program = (uint8_t)p->preset; + sp.first_region = (uint16_t)regions.size(); + sp.num_regions = 0; + + for (int ri = 0; ri < p->regionNum; ri++) { + struct tsf_region *r = &p->regions[ri]; + + if (r->initialFilterFc != 13500 || r->initialFilterQ != 0) + note_unsupported(p->presetName, "filter", warn_feats); + if (r->modEnvToFilterFc) + note_unsupported(p->presetName, "modEnvToFilterFc", warn_feats); + if (env_used(&r->modenv) && !r->modEnvToPitch && !r->modEnvToFilterFc) + note_unsupported(p->presetName, "modulation envelope", warn_feats); + if (r->modLfoToPitch || r->modLfoToFilterFc || r->modLfoToVolume || r->freqModLFO) + note_unsupported(p->presetName, "modulation LFO", warn_feats); + if (r->vibLfoToPitch || r->freqVibLFO) + note_unsupported(p->presetName, "vibrato LFO", warn_feats); + + unsigned int start = r->offset; + unsigned int end = r->end; + if (end <= start) continue; + int cnt = (int)(end - start); + + int loop_mode = SF64_LOOP_NONE; + int loop_start = 0, loop_end = 0; + if (r->loop_mode != TSF_LOOPMODE_NONE && r->loop_start < r->loop_end) { + loop_mode = (r->loop_mode == TSF_LOOPMODE_SUSTAIN) + ? SF64_LOOP_SUSTAIN : SF64_LOOP_CONTINUOUS; + // TSF stores loop_end inclusive; WAV64 wants exclusive. + loop_start = (int)r->loop_start - (int)start; + loop_end = (int)r->loop_end + 1 - (int)start; + if (loop_start < 0) loop_start = 0; + if (loop_end > cnt) loop_end = cnt; + if (loop_end <= loop_start) { + loop_mode = SF64_LOOP_NONE; + loop_start = loop_end = 0; + } + } + + int16_t *pcm = (int16_t*)malloc(cnt * sizeof(int16_t)); + for (int i = 0; i < cnt; i++) { + float s = font->fontSamples[start + i]; + if (s > 1.0f) s = 1.0f; + else if (s < -1.0f) s = -1.0f; + pcm[i] = (int16_t)lroundf(s * 32767.0f); + } + + int loop_start_meta = loop_mode != SF64_LOOP_NONE ? loop_start : 0; + int loop_end_meta = loop_mode != SF64_LOOP_NONE ? loop_end : 0; + uint64_t hash = hash_sample_variant(pcm, cnt, (int)r->sample_rate, 1, + loop_start_meta, loop_end_meta); + raw_pcm_bytes += (int64_t)cnt * 2; + + int sample_index; + auto it = sample_by_hash.find(hash); + if (it != sample_by_hash.end()) { + sample_index = it->second; + free(pcm); + } else { + unique_pcm_bytes += (int64_t)cnt * 2; + wav_data_t wav = {}; + wav.samples = pcm; + wav.cnt = cnt; + wav.channels = 1; + wav.bitsPerSample = 16; + wav.sampleRate = (int)r->sample_rate; + wav.looping = loop_mode != SF64_LOOP_NONE; + wav.loopOffset = loop_start; + wav.loopEnd = loop_mode != SF64_LOOP_NONE ? loop_end : 0; + + walign(out, 2); + uint32_t wav_off = ftell(out); + char *wavfn = NULL; + asprintf(&wavfn, "%s.%d.%d.wav64", outfn, pi, ri); + if (!wav64_write(infn, wavfn, out, &wav, flag_sf_compress)) + fatal("ERROR: failure writing sample for %s\n", infn); + free(wavfn); + uint32_t wav_sz = ftell(out) - wav_off; + embedded_wav += wav_sz; + + if (samples.size() >= 0xffffu) + fatal("ERROR: too many unique samples (max 65535)\n"); + + sf64_sample_t ss = {}; + ss.wav64_offset = wav_off; + ss.wav64_size = wav_sz; + ss.pcm_hash = hash; + ss.sample_start = 0; + // Keep the pre-padding logical length (wav64_write may pad cnt). + ss.sample_end = (uint32_t)cnt; + ss.loop_start = (uint32_t)loop_start_meta; + ss.loop_end = (uint32_t)loop_end_meta; + ss.sample_rate = (uint32_t)r->sample_rate; + ss.channels = 1; + sample_index = (int)samples.size(); + samples.push_back(ss); + sample_by_hash[hash] = sample_index; + free(wav.samples); + } + + if (regions.size() >= 0xffffu) + fatal("ERROR: too many regions (max 65535)\n"); + + sf64_region_t sr = {}; + sr.sample_index = (uint16_t)sample_index; + sr.key_min = r->lokey; + sr.key_max = r->hikey; + sr.velocity_min = r->lovel; + sr.velocity_max = r->hivel; + sr.loop_mode = (uint8_t)loop_mode; + sr.exclusive_group = r->group > 255 ? 255 : (uint8_t)r->group; + sr.root_key = (int8_t)r->pitch_keycenter; + sr.coarse_tune = (int8_t)r->transpose; + sr.fine_tune = (int16_t)r->tune; + sr.pitch_keytrack = (int16_t)r->pitch_keytrack; + // TinySoundFont applies InitialAttenuation with factor 0.01 instead + // of the SF2 0.1 (cB→dB). Multiply by 100 to recover centibels, + // then bake the linear gain once so the N64 never runs powf for it. + int attn_cb = (int)lroundf(r->attenuation * 100.0f); + if (attn_cb < 0) attn_cb = 0; + if (attn_cb > 1440) attn_cb = 1440; + sr.gain = centibels_to_gain(attn_cb); + sr.pan = (int16_t)lroundf(r->pan * 1000.0f); + copy_amp_env(&sr.amp_env, &r->ampenv); + copy_amp_env(&sr.mod_env, &r->modenv); + sr.mod_env_to_pitch = (int16_t)r->modEnvToPitch; + regions.push_back(sr); + sp.num_regions++; + } + + if (sp.num_regions) { + if (presets.size() >= 0xffffu) + fatal("ERROR: too many presets (max 65535)\n"); + names.push_back(p->presetName); + presets.push_back(sp); + } + } + + tsf_close(font); + + // Build uncompressed metadata: presets, regions, samples, then names. + FILE *meta = tmpfile(); + assert(meta); + + uint32_t name_base = (uint32_t)( + presets.size() * sizeof(sf64_preset_t) + + regions.size() * sizeof(sf64_region_t) + + samples.size() * sizeof(sf64_sample_t)); + uint32_t name_off = name_base; + for (size_t i = 0; i < presets.size(); i++) { + presets[i].name_offset = name_off; + name_off += (uint32_t)names[i].size() + 1; + } + + for (auto &p : presets) { + w16(meta, p.bank); + w8(meta, p.program); + w8(meta, p.flags); + w16(meta, p.first_region); + w16(meta, p.num_regions); + w32(meta, p.name_offset); + } + for (auto &r : regions) { + w16(meta, r.sample_index); + w8(meta, r.key_min); + w8(meta, r.key_max); + w8(meta, r.velocity_min); + w8(meta, r.velocity_max); + w8(meta, r.loop_mode); + w8(meta, r.exclusive_group); + w8(meta, r.root_key); + w8(meta, r.coarse_tune); + w16(meta, r.fine_tune); + w16(meta, r.pitch_keytrack); + w16(meta, r.pan); + wf32(meta, r.gain); + write_env(meta, &r.amp_env); + write_env(meta, &r.mod_env); + w16(meta, r.mod_env_to_pitch); + w16(meta, r.reserved_flags); + } + for (auto &s : samples) { + w32(meta, s.wav64_offset); + w32(meta, s.wav64_size); + w64(meta, s.pcm_hash); + w32(meta, s.sample_start); + w32(meta, s.sample_end); + w32(meta, s.loop_start); + w32(meta, s.loop_end); + w32(meta, s.sample_rate); + w8(meta, s.channels); + w8(meta, s.flags); + w16(meta, s.reserved); + } + for (size_t i = 0; i < presets.size(); i++) + wa(meta, names[i].c_str(), names[i].size() + 1); + + std::vector metadata = slurp(meta); + fclose(meta); + + walign(out, 2); + placeholder_set(out, "metadata_offset"); + int meta_cmp = asset_compress_mem(metadata.data(), metadata.size(), out, + DEFAULT_COMPRESSION, 0, NULL); + placeholder_set_offset(out, meta_cmp, "metadata_size"); + + placeholder_set_offset(out, presets.size(), "num_presets"); + placeholder_set_offset(out, regions.size(), "num_regions"); + placeholder_set_offset(out, samples.size(), "num_samples"); + placeholder_clear(); + fclose(out); + + flush_unsupported(warn_feats); + + if (flag_verbose) { + int nwarn = 0; + for (auto &kv : warn_feats) + nwarn += (int)kv.second.size(); + fprintf(stderr, "Converting: %s => %s\n", infn, outfn); + fprintf(stderr, " SF2 presets selected: %zu\n", presets.size()); + fprintf(stderr, " resolved regions: %zu\n", regions.size()); + fprintf(stderr, " unique sample variants: %zu\n", samples.size()); + fprintf(stderr, " embedded WAV64 size: %lld KiB\n", (long long)(embedded_wav / 1024)); + fprintf(stderr, " deduplicated size saved: %lld KiB\n", + (long long)((raw_pcm_bytes - unique_pcm_bytes) / 1024)); + fprintf(stderr, " unsupported generators: %d\n", nwarn); + fprintf(stderr, " stereo voices: 0\n"); + } + + return 0; +} diff --git a/tools/audioconv64/conv_wav64.c b/tools/audioconv64/conv_wav64.c deleted file mode 100644 index 82a7ce36e..000000000 --- a/tools/audioconv64/conv_wav64.c +++ /dev/null @@ -1,471 +0,0 @@ -#include "../../src/audio/wav64_internal.h" - -#define DR_WAV_IMPLEMENTATION -#include "dr_wav.h" - -#define DR_MP3_IMPLEMENTATION -#include "dr_mp3.h" - -#include "vadpcm/vadpcm.h" -#include "vadpcm/encode.c" -#include "vadpcm/error.c" - -#include "../common/binout.c" -#include "../common/binout.h" -#include "../common/polyfill.h" - -#define ENABLE_SINC_BEST_CONVERTER -#define PACKAGE "libsamplerate" -#define VERSION "0.1.9" -#include "libsamplerate/samplerate.h" -#include "libsamplerate/samplerate.c" -#include "libsamplerate/src_sinc.c" -#include "libsamplerate/src_zoh.c" -#include "libsamplerate/src_linear.c" -#undef PACKAGE -#undef VERSION -#undef MIN -#undef MAX - -#include "../../src/audio/libopus.c" - -bool flag_wav_looping = false; -int flag_wav_looping_offset = 0; -int flag_wav_compress = 1; -int flag_wav_resample = 0; -bool flag_wav_mono = false; -const int OPUS_SAMPLE_RATE = 48000; - -typedef struct { - int16_t *samples; - int channels; - int bitsPerSample; - int sampleRate; -} wav_data_t; - -static size_t read_wav(const char *infn, wav_data_t *out) -{ - drwav wav; - if (!drwav_init_file(&wav, infn, NULL)) { - fprintf(stderr, "ERROR: %s: not a valid WAV/RIFF/AIFF file\n", infn); - return 0; - } - - // Decode the samples as 16bit little-endian. This will decode everything including - // compressed formats so that we're able to read any kind of WAV file, though - // it will end up as an uncompressed file. - int16_t* samples = malloc(wav.totalPCMFrameCount * wav.channels * sizeof(int16_t)); - size_t cnt = drwav_read_pcm_frames_s16le(&wav, wav.totalPCMFrameCount, samples); - if (cnt != wav.totalPCMFrameCount) { - fprintf(stderr, "WARNING: %s: %d frames found, but only %zu decoded\n", infn, (int)wav.totalPCMFrameCount, cnt); - } - - out->samples = samples; - out->channels = wav.channels; - out->bitsPerSample = wav.bitsPerSample; - out->sampleRate = wav.sampleRate; - drwav_uninit(&wav); - return cnt; -} - -static size_t read_mp3(const char *infn, wav_data_t *out) -{ - drmp3 mp3; - if (!drmp3_init_file(&mp3, infn, NULL)) { - fprintf(stderr, "ERROR: %s: not a valid MP3 file\n", infn); - return 0; - } - - uint64_t nframes = drmp3_get_pcm_frame_count(&mp3); - int16_t* samples = malloc(nframes * mp3.channels * sizeof(int16_t)); - size_t cnt = drmp3_read_pcm_frames_s16(&mp3, nframes, samples); - if (cnt != nframes) { - fprintf(stderr, "WARNING: %s: %d frames found, but only %zu decoded\n", infn, (int)nframes, cnt); - } - - out->samples = samples; - out->channels = mp3.channels; - out->bitsPerSample = 16; - out->sampleRate = mp3.sampleRate; - drmp3_uninit(&mp3); - return cnt; -} - -int wav_convert(const char *infn, const char *outfn) { - if (flag_verbose) { - const char *compr[4] = { "raw", "vadpcm", "raw", "opus" }; - fprintf(stderr, "Converting: %s => %s (%s)\n", infn, outfn, compr[flag_wav_compress]); - } - - bool failed = false; - wav_data_t wav = {0}; size_t cnt; - - // Read the input file - if (strcasestr(infn, ".mp3")) - cnt = read_mp3(infn, &wav); - else - cnt = read_wav(infn, &wav); - if (cnt == 0) { - return 1; - } - - if (flag_verbose) - fprintf(stderr, " input: %d bits, %d Hz, %d channels\n", wav.bitsPerSample, wav.sampleRate, wav.channels); - - // Check if the user requested conversion to mono - if (flag_wav_mono && wav.channels == 2) { - if (flag_verbose) - fprintf(stderr, " converting to mono\n"); - - // Allocate a new buffer for the mono samples - int16_t *mono_samples = malloc(cnt * sizeof(int16_t)); - - // Convert to mono - int16_t *sptr = wav.samples; - int16_t *dptr = mono_samples; - for (int i=0;i> 8) | ((v & 0x00FF) << 8); - *sptr = v; - // Write the sample as 16bit or 8bit. Since *sptr is 16-bit big-endian, - // the 8bit representation is just the first byte (MSB). Notice - // that WAV64 8bit is signed anyway. - fwrite(sptr, 1, nbits == 8 ? 1 : 2, out); - sptr++; - } - } break; - - case 1: { // vadpcm - // We need cnt to be a multiple of kVADPCMFrameSampleCount (16) because - // VADPCM are compressed using 16-sample frames. - // In addition to that, our RSP decompressor at the moment only supports - // multiples of 32 (for DMA alignment issues), so pad it to that. - const int VADPCM_ALIGN = kVADPCMFrameSampleCount*2; - if (cnt % VADPCM_ALIGN) { - int newcnt = (cnt + VADPCM_ALIGN - 1) / VADPCM_ALIGN * VADPCM_ALIGN; - wav.samples = realloc(wav.samples, newcnt * wav.channels * sizeof(int16_t)); - memset(wav.samples + cnt, 0, (newcnt - cnt) * wav.channels * sizeof(int16_t)); - cnt = newcnt; - } - - enum { kPREDICTORS = 4 }; - - assert(cnt % kVADPCMFrameSampleCount == 0); - int nframes = cnt / kVADPCMFrameSampleCount; - void *scratch = malloc(vadpcm_encode_scratch_size(nframes)); - struct vadpcm_vector *codebook = alloca(kPREDICTORS * kVADPCMEncodeOrder * wav.channels * sizeof(struct vadpcm_vector)); - struct vadpcm_params parms = { .predictor_count = kPREDICTORS }; - void *dest = malloc(nframes * kVADPCMFrameByteSize * wav.channels); - - if (flag_verbose) - fprintf(stderr, " compressing into VADPCM format (%d frames)\n", nframes); - - int16_t *schan = malloc(cnt * sizeof(int16_t)); - uint8_t *destchan = dest; - for (int i=0; i 960 samples - // 32 Khz => 640 samples - const int FRAMES_PER_SECOND = 50; - int frame_size = wav.sampleRate / FRAMES_PER_SECOND; - int err = OPUS_OK; - - OpusCustomMode *custom_mode = opus_custom_mode_create( - wav.sampleRate, frame_size, &err); - if (err != OPUS_OK) { - fprintf(stderr, "ERROR: %s: cannot create opus custom mode: %s\n", infn, opus_strerror(err)); - failed = true; goto end; - } - - OpusCustomEncoder *enc = opus_custom_encoder_create( - custom_mode, wav.channels, &err); - if (err != OPUS_OK) { - opus_custom_mode_destroy(custom_mode); - fprintf(stderr, "ERROR: %s: cannot create opus encoder: %s\n", infn, opus_strerror(err)); - failed = true; goto end; - } - - // Automatic bitrate calculation for "good quality". This is the same - // algorithm libopus selects when setting OPUS_AUTO bitrate. - int bitrate_bps = 60*FRAMES_PER_SECOND + wavOriginalSampleRate * wav.channels; - if (flag_verbose) - fprintf(stderr, " opus bitrate: %d bps\n", bitrate_bps); - - // Write extended header - w32(out, frame_size); - uint32_t max_cmp_size_pos = w32_placeholder(out); // max compressed frame size - w32(out, bitrate_bps); - w32_at(out, wstart_offset, ftell(out)); - - // Configure opus encoder. We use VBR as it provides the best - // compression/quality balance and we don't have specific constraints - // there. We select the maximum algorithmic complexity to get the best quality. - opus_custom_encoder_ctl(enc, OPUS_SET_BITRATE(bitrate_bps)); - opus_custom_encoder_ctl(enc, OPUS_SET_BANDWIDTH(OPUS_AUTO)); - opus_custom_encoder_ctl(enc, OPUS_SET_VBR(1)); - opus_custom_encoder_ctl(enc, OPUS_SET_VBR_CONSTRAINT(0)); - opus_custom_encoder_ctl(enc, OPUS_SET_COMPLEXITY(10)); - opus_custom_encoder_ctl(enc, OPUS_SET_INBAND_FEC(0)); - opus_custom_encoder_ctl(enc, OPUS_SET_FORCE_CHANNELS(OPUS_AUTO)); - opus_custom_encoder_ctl(enc, OPUS_SET_DTX(0)); - opus_custom_encoder_ctl(enc, OPUS_SET_PACKET_LOSS_PERC(0)); - opus_custom_encoder_ctl(enc, OPUS_SET_LSB_DEPTH(16)); - - // Pad input samples with zeros, rounding to frame size - int newcnt = (cnt + frame_size - 1) / frame_size * frame_size; - wav.samples = realloc(wav.samples, newcnt * wav.channels * sizeof(int16_t)); - memset(wav.samples + cnt, 0, (newcnt - cnt) * wav.channels * sizeof(int16_t)); - - int max_nb = 0; - int out_max_size = bitrate_bps/8; // overestimation - uint8_t *out_buffer = malloc(out_max_size); - for (int i=0; i max_nb) - max_nb = nb; - walign(out, 2); // make sure frames are 2-byte aligned - } - - w32_at(out, max_cmp_size_pos, max_nb); // write maxixum compressed frame size - - free(out_buffer); - opus_custom_encoder_destroy(enc); - - if (flag_debug) { - fclose(out); - - char* wav2fn = changeext(outfn, ".opus.wav"); - if (flag_verbose) - fprintf(stderr, " writing uncompressed file %s\n", wav2fn); - - out = fopen(outfn, "rb"); - fseek(out, 36, SEEK_SET); - OpusCustomDecoder *dec = opus_custom_decoder_create( - custom_mode, wav.channels, &err); - if (err != OPUS_OK) { - opus_custom_mode_destroy(custom_mode); - fprintf(stderr, "ERROR: %s: cannot create opus decoder: %s\n", infn, opus_strerror(err)); - free(wav2fn); - failed = true; goto end; - } - - // Decode the whole file to check for errors - int16_t *out_samples = malloc(newcnt * wav.channels * sizeof(int16_t)); - int outcnt = 0; - for (int i=0; i + + This tool is part of the Libdragon SDK. + + This is free and unencumbered software released into the public domain. + + For more information, please refer to +*/ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include +#include "../common/binout.h" +#include "../common/polyfill.h" +#include "audioconv64.h" +#include "../../src/audio/wav64_internal.h" + +#define DR_WAV_IMPLEMENTATION +#include "../common/dr_wav.h" + +#define DR_MP3_IMPLEMENTATION +#include "../common/dr_mp3.h" + +#include "libvadpcm.h" +#include "libsamplerate.h" +#include "libopus.h" +#include "libulc.h" + +#include "huff_vadpcm.c" + +// Shared parsing helpers for --wav-seek (same syntax as videoconv64 --seek) +#include "../common/seekfile.cpp" + +bool flag_wav_looping = false; +int flag_wav_looping_offset = 0; +int flag_wav_compress = 1; +int flag_wav_compress_vadpcm_huffman = -1; +int flag_wav_compress_vadpcm_bits = 4; +ulc_mode_t flag_wav_compress_ulc_mode = ULC_MODE_VBR; +float flag_wav_compress_ulc_bitrate = 64.0f; +float flag_wav_compress_ulc_quality = 50.0f; +int flag_wav_resample = 0; +double flag_wav_seek_interval_sec = 0.0; +const char *flag_wav_seek_file = NULL; +bool flag_wav_mono = false; +const int OPUS_SAMPLE_RATE = 48000; +const int ULC_BLOCK_SIZE = 1024; + +static bool read_wav(const char *infn, wav_data_t *out) +{ + drwav wav; + if (!drwav_init_file_with_metadata(&wav, infn, 0, NULL)) { + fprintf(stderr, "ERROR: %s: not a valid WAV/RIFF/AIFF file\n", infn); + return false; + } + + // Decode the samples as 16bit little-endian. This will decode everything including + // compressed formats so that we're able to read any kind of WAV file, though + // it will end up as an uncompressed file. + int16_t* samples = (int16_t*)malloc(wav.totalPCMFrameCount * wav.channels * sizeof(int16_t)); + out->cnt = drwav_read_pcm_frames_s16le(&wav, wav.totalPCMFrameCount, samples); + if (out->cnt != wav.totalPCMFrameCount) { + fprintf(stderr, "WARNING: %s: %d frames found, but only %d decoded\n", infn, (int)wav.totalPCMFrameCount, out->cnt); + } + + out->samples = samples; + out->channels = wav.channels; + out->bitsPerSample = wav.bitsPerSample; + out->sampleRate = wav.sampleRate; + + // Check if we find smpl metadata, and if so, extract the loop points. + // Keep samples past the loop end as a release tail. + for (int i=0; isampleLoopCount > 0) { + // If we have multiple loops, we just take the first one. + drwav_smpl_loop* loop = &smpl->pLoops[0]; + out->looping = true; + out->loopOffset = loop->firstSampleOffset; + out->loopEnd = loop->lastSampleOffset + 1; + if (out->loopEnd > out->cnt) + out->loopEnd = out->cnt; + + switch (loop->type) { + case 0: // standard forward loop + if (flag_verbose) + fprintf(stderr, " found forward loop [start=%d end=%d cnt=%d release=%d]\n", + loop->firstSampleOffset, loop->lastSampleOffset, out->cnt, + out->cnt - out->loopEnd); + break; + case 1: { // ping-pong loop + if (flag_verbose) + fprintf(stderr, " found ping-pong loop [start=%d end=%d cnt=%d]\n", loop->firstSampleOffset, + loop->lastSampleOffset, out->cnt); + // Unroll the ping-pong into a terminal forward loop; any + // release past the original loop end is dropped. + int last_offset = loop->lastSampleOffset; + int first_offset = loop->firstSampleOffset; + int loop_len = last_offset - first_offset + 1; + int keep = out->loopEnd; + int16_t* new_samples = (int16_t*)malloc((keep + loop_len) * out->channels * sizeof(int16_t)); + memcpy(new_samples, samples, keep * out->channels * sizeof(int16_t)); + for (int i=0; isamples = new_samples; + out->cnt = keep + loop_len; + out->loopOffset = keep; + out->loopEnd = out->cnt; + } break; + default: + fprintf(stderr, "WARNING: %s: loop type %d not supported\n", infn, loop->type); + break; + } + } + } + + // If we find cue points, interpret them as "seek points" (skip points) used during compression. + // This is more semantically correct than abusing SMPL loops for markers. + if (wav.pMetadata[i].type == drwav_metadata_type_cue) { + drwav_cue *cue = &wav.pMetadata[i].data.cue; + if (cue->cuePointCount > 0 && cue->pCuePoints) { + for (drwav_uint32 j = 0; j < cue->cuePointCount; j++) { + drwav_uint32 off = cue->pCuePoints[j].sampleOffset; + if (off > 0) out->skipPoints.push_back((int)off); + } + } + } + } + + drwav_uninit(&wav); + return true; +} + +static size_t read_mp3(const char *infn, wav_data_t *out) +{ + drmp3 mp3; + if (!drmp3_init_file(&mp3, infn, NULL)) { + fprintf(stderr, "ERROR: %s: not a valid MP3 file\n", infn); + return false; + } + + uint64_t nframes = drmp3_get_pcm_frame_count(&mp3); + int16_t* samples = (int16_t*)malloc(nframes * mp3.channels * sizeof(int16_t)); + out->cnt = drmp3_read_pcm_frames_s16(&mp3, nframes, samples); + if (out->cnt != nframes) { + fprintf(stderr, "WARNING: %s: %d frames found, but only %d decoded\n", infn, (int)nframes, out->cnt); + } + + out->samples = samples; + out->channels = mp3.channels; + out->bitsPerSample = 16; + out->sampleRate = mp3.sampleRate; + drmp3_uninit(&mp3); + return true; +} + +static int64_t now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static void resample_progress_print(int64_t bytes_done, int64_t bytes_total, int64_t *last_print_ms, double *last_pct) { + if (!flag_verbose) return; + int64_t now = now_ms(); + // Print at most once every 2 seconds, but always print the final 100%. + const bool is_done = (bytes_total > 0 && bytes_done >= bytes_total); + if (!is_done && *last_print_ms && (now - *last_print_ms) < 1000) return; + double pct = (bytes_total > 0) ? (double)bytes_done * 100.0 / (double)bytes_total : 0.0; + if (pct > 100.0) pct = 100.0; + *last_print_ms = now; + *last_pct = pct; + fprintf(stderr, "Resampling: %lld bytes (%.1f%%)\n", (long long)bytes_done, pct); +} + +/** + * Insert @p ncopy samples of the loop prefix at @p loopEnd, shifting the + * release tail forward. Both loop bounds advance by @p ncopy so the loop + * content is a rotation of the original (used to align loopStart). + * + * The copy runs a little past the new loop end, over the first samples of the + * shifted tail. The rotation leaves those out of phase with the loop, and the + * compressed frame that straddles the loop end would have to encode the jump + * together with the last samples of the loop, on a single scale factor: enough + * to flatten them, and the loop is what plays over and over. Rewriting up to + * the frame boundary also covers the taps the resampler reads past the loop. + */ +static void wav_rotate_loop_at_end(wav_data_t *wav, int ncopy) +{ + const int FRAME = kVADPCMFrameSampleCount; + int ch = wav->channels; + int loop_end = wav->loopEnd ? wav->loopEnd : wav->cnt; + int loop_len = loop_end - wav->loopOffset; + int tail = wav->cnt - loop_end; + assert(ncopy > 0 && loop_len > 0 && tail >= 0); + + int end = loop_end + ncopy; + int over = (end + 3 + FRAME-1) / FRAME * FRAME - end; + if (over > tail) over = tail; + + wav->samples = (int16_t*)realloc(wav->samples, (wav->cnt + ncopy) * ch * sizeof(int16_t)); + memmove(&wav->samples[end * ch], + &wav->samples[loop_end * ch], + tail * ch * sizeof(int16_t)); + for (int i = 0; i < ncopy + over; i++) { + int src = wav->loopOffset + (i % loop_len); + for (int c = 0; c < ch; c++) + wav->samples[(loop_end + i) * ch + c] = wav->samples[src * ch + c]; + } + wav->cnt += ncopy; + wav->loopOffset += ncopy; + wav->loopEnd = end; +} + +/** + * @brief Write a WAV64 file, optionally compressing it. + * + * @param infn Input file name (used only for diagnostics) + * @param outfn Output file name (used only to create debug dump files if requested) + * @param out Output file handle + * @param wav Input WAV data (always pre-converted to 16 bit) + * @param format Compression format. 0=none, 1=VADPCM, 3=opus. + * @return true if the file was written successfully + * @return false if any error occurred + * + * After the call: + * wav->samples might have been reallocated to a different buffer, and the original one freed + * + * Consider the function might have to change the wav->cnt / loopOffset / loopEnd + * values to make them compatible with the compression format (eg: padding, realigning). + */ +bool wav64_write(const char *infn, const char *outfn, FILE *out, wav_data_t* wav, int format) +{ + bool failed = false; + int basepos = ftell(out); + + // Terminal loop if none was set explicitly. + if (wav->looping && wav->loopEnd == 0) + wav->loopEnd = wav->cnt; + int loop_end = wav->looping ? wav->loopEnd : 0; + int loop_len = wav->looping ? loop_end - wav->loopOffset : 0; + if (loop_len < 0 || loop_end > wav->cnt) { + fprintf(stderr, "WARNING: %s: invalid loop %d..%d (size: %d)\n", + infn, wav->loopOffset, loop_end, wav->cnt); + loop_len = 0; + loop_end = 0; + wav->looping = false; + wav->loopEnd = 0; + } + + switch (format) { + case 0: + if (loop_len&1 && wav->bitsPerSample ==8) { + // Odd loop lengths are not supported for 8-bit waveforms because they would + // change the 2-byte phase between ROM and RDRAM addresses during loop unrolling. + // We shorten the loop by 1 sample which shouldn't matter. + wav->loopOffset += 1; + loop_len -= 1; + } + break; + + case 1: { // vadpcm + // Align loopStart to 32 samples (even VADPCM frames) by rotating the + // skipped prefix into the loop end — not past sample_end, so a release + // tail stays after the loop. + enum { VADCPM_ALIGN = 32 }; + if (wav->looping && (wav->loopOffset % VADCPM_ALIGN) != 0) { + wav_rotate_loop_at_end(wav, VADCPM_ALIGN - (wav->loopOffset % VADCPM_ALIGN)); + loop_end = wav->loopEnd; + loop_len = loop_end - wav->loopOffset; + } + + wav->bitsPerSample = 16; // VADPCM always uses 16-bit samples + } break; + + case 2: { // ulc + wav->bitsPerSample = 16; // ULC always uses 16-bit source samples + + // Keep the logical waveform length on an 8-byte boundary so that full-file + // loops always append decoded blocks at an aligned RDRAM address. ULC pads + // the encoded stream to whole 1024-frame blocks independently. + const int frame_bytes = wav->channels * sizeof(int16_t); + int frame_align = 1; + while ((frame_align * frame_bytes) & 7) { + frame_align++; + } + + if (wav->looping && (wav->loopOffset % ULC_BLOCK_SIZE) != 0) { + wav_rotate_loop_at_end(wav, ULC_BLOCK_SIZE - (wav->loopOffset % ULC_BLOCK_SIZE)); + loop_end = wav->loopEnd; + loop_len = loop_end - wav->loopOffset; + } + + wav->cnt -= wav->cnt % frame_align; + if (wav->looping && wav->loopEnd > wav->cnt) + wav->loopEnd = wav->cnt; + loop_end = wav->looping ? wav->loopEnd : 0; + loop_len = wav->looping ? loop_end - wav->loopOffset : 0; + } break; + + case 3: // opus: + wav->bitsPerSample = 16; // Opus always uses 16-bit source samples + break; + } + + // Recompute after alignment. On disk, 0 means terminal (loop_end == len). + loop_end = wav->looping ? (wav->loopEnd ? wav->loopEnd : wav->cnt) : 0; + loop_len = wav->looping ? loop_end - wav->loopOffset : 0; + int loop_end_disk = (wav->looping && loop_end < wav->cnt) ? loop_end : 0; + + fwrite("WV64", 1, 4, out); + w8(out, 10); // version + w8(out, format); // format + w8(out, wav->channels); // channels + w8(out, wav->bitsPerSample); // bits + w32(out, wav->sampleRate); // frequency + w32(out, wav->cnt); // len + w32(out, loop_len); // loop_len + w32(out, loop_end_disk); // loop_end (0 means len) + w32_placeholderf(out, "%s/samples", outfn); // offset where samples begin + w32_placeholderf(out, "%s/state_size", outfn); // size of per-mixer-channel state to allocate at runtime + + switch (format) { + case 0: { // no compression + // Uncompressed waveforms need to no state (0 bytes). + placeholder_set_offset(out, 0, "%s/state_size", outfn); + + // Start of the samples data + placeholder_set_offset(out, ftell(out)-basepos, "%s/samples", outfn); + int16_t *sptr = wav->samples; + for (int i=0;icnt*wav->channels;i++) { + // Byteswap *sptr + int16_t v = *sptr; + v = ((v & 0xFF00) >> 8) | ((v & 0x00FF) << 8); + *sptr = v; + // Write the sample as 16bit or 8bit. Since *sptr is 16-bit big-endian, + // the 8bit representation is just the first byte (MSB). Notice + // that WAV64 8bit is signed anyway. + fwrite(sptr, 1, wav->bitsPerSample == 8 ? 1 : 2, out); + sptr++; + } + } break; + + case 1: { // vadpcm + // Sub-nibble residuals are packed natively in the bitstream, which + // leaves nothing for Huffman to exploit: it works on nibbles and both + // schemes squeeze the same redundancy. + if (flag_wav_compress_vadpcm_bits < 4 && flag_wav_compress_vadpcm_huffman) { + if (flag_verbose) + fprintf(stderr, " %d-bit residuals are packed natively: disabling huffman\n", + flag_wav_compress_vadpcm_bits); + flag_wav_compress_vadpcm_huffman = 0; + } + + // The state is 16+4+4 bytes per channel (see wav64_state_vadpcm_t), but the runtime code requires to + // always allocate both channels even for mono files. + placeholder_set_offset(out, 48, "%s/state_size", outfn); + + // We need cnt to be a multiple of kVADPCMFrameSampleCount (16) because + // VADPCM are compressed using 16-sample frames. + // In addition to that, our RSP decompressor at the moment only supports + // multiples of 32 (for DMA alignment issues), so pad it to that. + const int VADPCM_ALIGN = kVADPCMFrameSampleCount*2; + int prepad_cnt = wav->cnt; + if (wav->cnt % VADPCM_ALIGN) { + int newcnt = (wav->cnt + VADPCM_ALIGN - 1) / VADPCM_ALIGN * VADPCM_ALIGN; + wav->samples = (int16_t*)realloc(wav->samples, newcnt * wav->channels * sizeof(int16_t)); + // The padding shares a frame with real samples, and a frame carries + // a single scale factor: padding with silence would spend the whole + // residual range on the jump to zero and flatten the real samples + // next to it. Continue the waveform instead, with the samples that + // really play next (the loop start) or by holding the last one. + for (int i = wav->cnt; i < newcnt; i++) { + int src = loop_len > 0 + ? wav->loopOffset + (i - wav->cnt) % loop_len + : wav->cnt - 1; + for (int c = 0; c < wav->channels; c++) + wav->samples[i * wav->channels + c] = wav->samples[src * wav->channels + c]; + } + wav->cnt = newcnt; + } + + enum { kPREDICTORS = 4 }; + + assert(wav->cnt % kVADPCMFrameSampleCount == 0); + int nframes = wav->cnt / kVADPCMFrameSampleCount; + struct vadpcm_vector *codebook = (struct vadpcm_vector *)alloca(kPREDICTORS * kVADPCMEncodeOrder * wav->channels * sizeof(struct vadpcm_vector)); + struct vadpcm_params parms = { + .predictor_count = kPREDICTORS, + // Match previous encoder behavior (always dithered residuals). + .dither = kVADPCMDitherRectangular, + .min_residual = -(1 << (flag_wav_compress_vadpcm_bits-1)), + .max_residual = (1 << (flag_wav_compress_vadpcm_bits-1)) - 1 + }; + uint8_t *dest = (uint8_t*)malloc(nframes * kVADPCMFrameByteSize * wav->channels); + + if (flag_verbose) + fprintf(stderr, " compressing into VADPCM format (%d frames)\n", nframes); + + std::vector skip_points = wav->skipPoints; + if (wav->looping) skip_points.push_back(wav->loopOffset); + std::sort(skip_points.begin(), skip_points.end()); + + // Abort if skip points are out of bound + for (int i=0; i= wav->cnt) { + fprintf(stderr, "ERROR: %s: invalid skip point: %d\n", infn, skip_points[i]); + failed = true; + break; + } + } + + std::vector skip_bitpos(skip_points.size(), 0); + std::vector> skip_state(skip_points.size()); + // First three samples decoded at the loop start, stored after each + // channel's codebook so the mixer can Hermite across the loop point. + std::array, 2> loop_head = {}; + int loop_start_aligned = -1; + if (wav->looping) { + loop_start_aligned = (wav->loopOffset + kVADPCMFrameSampleCount - 1) + / kVADPCMFrameSampleCount * kVADPCMFrameSampleCount; + } + + int16_t *schan = (int16_t*)malloc(wav->cnt * sizeof(int16_t)); + for (int i=0; ichannels; i++) { + uint8_t *destchan = (uint8_t*)malloc(nframes * kVADPCMFrameByteSize); + for (int j=0; jcnt; j++) + schan[j] = wav->samples[i + j*wav->channels]; + vadpcm_error err = vadpcm_encode(&parms, codebook + kPREDICTORS * kVADPCMEncodeOrder * i, nframes, destchan, schan, NULL); + if (err != kVADPCMErrNone) { + fprintf(stderr, "ERROR: %s: VADPCM encoding failed: %s\n", infn, vadpcm_error_name(err)); + failed = true; + free(destchan); + break; + } + if (!skip_points.empty()) { + // Compute decoder states at all skip points in a single forward pass. + // We need the state at the *beginning* of the target VADPCM frame N, + // which corresponds to the state after decoding frames [0..N). + struct vadpcm_vector st = {0}; + int cur_frame = 0; + for (int j=0; j<(int)skip_points.size(); j++) { + const int target_frame = skip_points[j] / kVADPCMFrameSampleCount; + const int run_frames = target_frame - cur_frame; + if (run_frames > 0) { + vadpcm_error err = vadpcm_decode(kPREDICTORS, kVADPCMEncodeOrder, + codebook + kPREDICTORS * kVADPCMEncodeOrder * i, + &st, (size_t)run_frames, schan, + destchan + (size_t)cur_frame * kVADPCMFrameByteSize); + assert(err == kVADPCMErrNone); + } + + skip_state[j][i] = st; + // The loop taps are the first three samples of the frame the + // loop resumes on: decode that one frame on a copy of the + // state so the forward pass is not disturbed. + if (skip_points[j] == loop_start_aligned && target_frame < nframes) { + struct vadpcm_vector st_head = st; + int16_t head[kVADPCMFrameSampleCount]; + vadpcm_error herr = vadpcm_decode(kPREDICTORS, kVADPCMEncodeOrder, + codebook + kPREDICTORS * kVADPCMEncodeOrder * i, + &st_head, 1, head, + destchan + (size_t)target_frame * kVADPCMFrameByteSize); + assert(herr == kVADPCMErrNone); + loop_head[i][0] = head[0]; + loop_head[i][1] = head[1]; + loop_head[i][2] = head[2]; + } + cur_frame = target_frame; + } + } + + // Block-planar layout: for each block of B frames, all L then all R + // (mono unchanged). One ring fill maps to one file block. + const int B = 128; + for (int j=0; jchannels == 1) { + dstj = j; + } else { + int block = j / B; + int off = j % B; + int nblocks = (nframes + B - 1) / B; + int bs = (block == nblocks - 1) ? (nframes - block * B) : B; + dstj = block * B * wav->channels + i * bs + off; + } + memcpy(dest + dstj * kVADPCMFrameByteSize, destchan + j * kVADPCMFrameByteSize, kVADPCMFrameByteSize); + } + free(destchan); + } + free(schan); + if (failed) { + free(dest); + break; + } + + if (!skip_points.empty() && flag_verbose) + fprintf(stderr, " state generated for %zu seek points\n", skip_points.size()); + + const int dest_size = nframes * kVADPCMFrameByteSize * wav->channels; + const int maxcompbuflen = dest_size; + uint8_t *compbuf = (uint8_t*)malloc(maxcompbuflen); + uint8_t *ctxbuf = (uint8_t*)calloc(HUFF_CONTEXT_LEN, 1); + int compbuflen = 0; + if (flag_wav_compress_vadpcm_huffman) { + compbuflen = huffv_compress(dest, dest_size, compbuf, maxcompbuflen, ctxbuf, HUFF_CONTEXT_LEN); + + if (flag_verbose) + fprintf(stderr, " huffman compressed %d bytes into %d bytes (ratio: %.1f%%)\n", + nframes * kVADPCMFrameByteSize * wav->channels, compbuflen, + 100.0f * compbuflen / (nframes * kVADPCMFrameByteSize * wav->channels)); + + // Try decompressing now, just to double check there are no bugs + std::vector scratch(dest_size); + HuffLookup tbl[HUFF_CONTEXTS]; + huffv_decompress_init(ctxbuf, HUFF_CONTEXT_LEN, tbl); + std::vector bitpos_stats; + if (!skip_points.empty()) + bitpos_stats.resize(dest_size / kVADPCMFrameByteSize + 1); + + int bitpos = huffv_decompress(compbuf, compbuflen, tbl, &scratch[0], dest_size, + bitpos_stats.empty() ? NULL : bitpos_stats.data()); + assert((bitpos+7)/8 == compbuflen); + assert(memcmp(&scratch[0], dest, dest_size) == 0); + + // Compute bit offset for each skip point (O(1) lookup from full decode stats). + // Bitpos is the start of channel-0's frame F in the block-planar stream, + // so seeking can resume Huffman decoding from that point. + const int B = 128; + for (int i=0; i<(int)skip_points.size(); i++) { + int F = skip_points[i] / kVADPCMFrameSampleCount; + int idx; + if (wav->channels == 1) { + idx = F; + } else { + int block = F / B; + int off = F % B; + idx = block * B * wav->channels + off; // channel 0 within block + } + assert(idx >= 0); + assert(idx < (int)bitpos_stats.size()); + skip_bitpos[i] = bitpos_stats[idx]; + } + } + + uint8_t flags = 0; + if (flag_wav_compress_vadpcm_huffman) flags |= (1<<0); + if (wav->resident) flags |= (1<<1); // VADPCM_FLAG_RESIDENT + + // Per channel: 8 predictor vectors (128 bytes) + 3 loop taps + pad. + const int CODEBOOK_STRIDE = kPREDICTORS * kVADPCMEncodeOrder * 16 + 8; + const int codebook_bytes = CODEBOOK_STRIDE * wav->channels; + struct vadpcm_vector state = {0}; + w8(out, kPREDICTORS); + w8(out, kVADPCMEncodeOrder); + w16(out, flags); + w16(out, skip_points.size()); + w8(out, flag_wav_compress_vadpcm_bits); + w8(out, wav->attack_frames); + w32(out, 0); // huff_tbl_ptr + w32(out, skip_points.size() > 0 ? codebook_bytes : 0); // skip_points_ptr + w32(out, skip_points.size() > 0 ? codebook_bytes + (int)skip_points.size()*8 : 0); // skip_states_ptr + fwrite(ctxbuf, 1, HUFF_CONTEXT_LEN, out); // Huffman context + w32(out, 0); // padding + for (int ch=0; chchannels; ch++) { + struct vadpcm_vector *cb = codebook + kPREDICTORS * kVADPCMEncodeOrder * ch; + for (int i=0; ichannels;k++) { + for (int j=0; j<8; j++) + w16(out, skip_state[i][k].v[j]); + } + } + + // Start of samples data + placeholder_set_offset(out, ftell(out)-basepos, "%s/samples", outfn); + if (flag_wav_compress_vadpcm_huffman) { + fwrite(compbuf, 1, compbuflen, out); + } else if (flag_wav_compress_vadpcm_bits < 4) { + const int total_frames = nframes * wav->channels; + uint8_t *packed = (uint8_t*)malloc(total_frames * vadpcm_frame_bytes(flag_wav_compress_vadpcm_bits)); + int packed_size = vadpcm_pack_frames(packed, dest, total_frames, flag_wav_compress_vadpcm_bits); + if (flag_verbose) + fprintf(stderr, " packed %d bytes into %d bytes (ratio: %.1f%%)\n", + dest_size, packed_size, 100.0f * packed_size / dest_size); + fwrite(packed, 1, packed_size, out); + free(packed); + } else { + fwrite(dest, 1, nframes * kVADPCMFrameByteSize * wav->channels, out); + } + + if (flag_debug) { + char* wav2fn = changeext(outfn, ".vadpcm.wav"); + if (flag_verbose) + fprintf(stderr, " writing uncompressed file %s\n", wav2fn); + + int16_t *out_samples = (int16_t *)malloc(wav->cnt * wav->channels * sizeof(int16_t)); + int16_t *out_channel = (int16_t *)malloc(wav->cnt * sizeof(int16_t)); + const int B = 128; + for (int i=0;ichannels;i++) { + uint8_t *in_channel = (uint8_t*)malloc(nframes * kVADPCMFrameByteSize); + for (int j=0;jchannels == 1) { + srcj = j; + } else { + int block = j / B; + int off = j % B; + int nblocks = (nframes + B - 1) / B; + int bs = (block == nblocks - 1) ? (nframes - block * B) : B; + srcj = block * B * wav->channels + i * bs + off; + } + memcpy(in_channel + j * kVADPCMFrameByteSize, dest + srcj * kVADPCMFrameByteSize, kVADPCMFrameByteSize); + } + + memset(&state, 0, sizeof(state)); + vadpcm_decode(kPREDICTORS, kVADPCMEncodeOrder, + codebook + kPREDICTORS * kVADPCMEncodeOrder * i, + &state, nframes, out_channel, in_channel); + for (int j=0;jcnt;j++) + out_samples[i + j*wav->channels] = out_channel[j]; + free(in_channel); + } + free(out_channel); + + drwav_data_format fmt = { + .container = drwav_container_riff, + .format = DR_WAVE_FORMAT_PCM, + .channels = wav->channels, + .sampleRate = wav->sampleRate, + .bitsPerSample = 16, + }; + drwav wav2; + if (!drwav_init_file_write(&wav2, wav2fn, &fmt, NULL)) { + fprintf(stderr, "ERROR: %s: cannot create WAV file\n", outfn); + failed = true; + } else { + drwav_write_pcm_frames(&wav2, prepad_cnt, out_samples); + drwav_uninit(&wav2); + } + + free(out_samples); + } + + free(dest); + free(compbuf); + free(ctxbuf); + } break; + + case 2: { // ulc + const int blocks_len = (wav->cnt + ULC_BLOCK_SIZE - 1) / ULC_BLOCK_SIZE + 2; + struct ULC_EncoderState_t enc = {}; + enc.RateHz = wav->sampleRate; + enc.nChan = wav->channels; + enc.BlockSize = ULC_BLOCK_SIZE; + if (ULC_EncoderState_Init(&enc) <= 0) { + fprintf(stderr, "ERROR: %s: cannot initialize ULC encoder\n", infn); + failed = true; + break; + } + + std::vector block(ULC_BLOCK_SIZE * wav->channels, 0.0f); + auto load_block = [&](int block_idx) { + std::fill(block.begin(), block.end(), 0.0f); + const int first = block_idx * ULC_BLOCK_SIZE; + for (int ch = 0; ch < wav->channels; ch++) + for (int i = 0; i < ULC_BLOCK_SIZE && first + i < wav->cnt; i++) + block[i * wav->channels + ch] = wav->samples[(first + i) * wav->channels + ch] / 32768.0f; + }; + + float avg_complexity = 0.0f; + if (flag_wav_compress_ulc_mode == ULC_MODE_ABR) { + double complexity_sum = 0.0; + for (int i = 0; i < blocks_len; i++) { + load_block(i); + ULC_EncodeBlock_VBR(&enc, block.data(), NULL, flag_wav_compress_ulc_quality); + complexity_sum += enc.BlockComplexity; + } + avg_complexity = (float)(complexity_sum / blocks_len); + if (avg_complexity <= 0.0f) avg_complexity = ULC_COEF_EPS; + ULC_EncoderState_Destroy(&enc); + enc = {}; + enc.RateHz = wav->sampleRate; + enc.nChan = wav->channels; + enc.BlockSize = ULC_BLOCK_SIZE; + if (ULC_EncoderState_Init(&enc) <= 0) { + fprintf(stderr, "ERROR: %s: cannot initialize ULC ABR encoder\n", infn); + failed = true; + break; + } + } + + w16(out, ULC_BLOCK_SIZE); + w16_placeholderf(out, "%s/ulc_max_block_size", outfn); + w32(out, blocks_len); + w32_placeholderf(out, "%s/ulc_bitrate", outfn); + w32_placeholderf(out, "%s/ulc_seek_table", outfn); // reserved: seek table offset + + // Collect the first-block-relative offset of every eighth block. The table + // itself is written after the compressed stream. + const int seek_interval_blocks = 8; + const int seek_table_len = (blocks_len + seek_interval_blocks - 1) / seek_interval_blocks; + const int samples_start = ftell(out); + placeholder_set_offset(out, samples_start-basepos, "%s/samples", outfn); + std::vector seek_offsets; + seek_offsets.reserve(seek_table_len); + + // Fixed 32-bit target state, alignment slack, two temporary blocks per + // channel, and the persistent per-channel lap state. The two banks retain + // coefficients for both queued preroll blocks. Normal mono transforms + // overwrite coefficients directly in the output samplebuffer; normal + // stereo reuses the first bank as planar mid/side staging. + const int decoder_state_size = 24 /*sizeof(ulc_state_t)*/ + 63 + + sizeof(int16_t) * 2 * wav->channels * ULC_BLOCK_SIZE + + sizeof(int16_t) * wav->channels * (ULC_BLOCK_SIZE / 2); + placeholder_set_offset(out, decoder_state_size, "%s/state_size", outfn); + + uint64_t total_bytes = 0; + int max_block_size = 0; + std::vector> debug_blocks; + if (flag_debug) + debug_blocks.reserve(blocks_len); + for (int i = 0; i < blocks_len; i++) { + if (i % seek_interval_blocks == 0) + seek_offsets.push_back(ftell(out)-samples_start); + + load_block(i); + int size_bits = 0; + const void *encoded; + switch (flag_wav_compress_ulc_mode) { + case ULC_MODE_VBR: + encoded = ULC_EncodeBlock_VBR(&enc, block.data(), &size_bits, flag_wav_compress_ulc_quality); + break; + case ULC_MODE_ABR: + encoded = ULC_EncodeBlock_ABR(&enc, block.data(), &size_bits, flag_wav_compress_ulc_bitrate, avg_complexity); + break; + default: + encoded = ULC_EncodeBlock_CBR(&enc, block.data(), &size_bits, flag_wav_compress_ulc_bitrate); + break; + } + const int size_bytes = (size_bits + 7) / 8; + fwrite(encoded, 1, size_bytes, out); + if (flag_debug) + debug_blocks.emplace_back((const uint8_t *)encoded, (const uint8_t *)encoded + size_bytes); + total_bytes += size_bytes; + max_block_size = std::max(max_block_size, size_bytes); + } + assert((int)seek_offsets.size() == seek_table_len); + placeholder_set_offset(out, ftell(out)-samples_start, "%s/ulc_seek_table", outfn); + for (uint32_t offset : seek_offsets) + w32(out, offset); + + const int actual_bitrate = (int)llround(total_bytes * 8.0 * wav->sampleRate / ((double)blocks_len * ULC_BLOCK_SIZE)); + placeholder_set_offset(out, max_block_size, "%s/ulc_max_block_size", outfn); + placeholder_set_offset(out, actual_bitrate, "%s/ulc_bitrate", outfn); + if (flag_verbose) + fprintf(stderr, " ULC: %d blocks, %.2f kbps (%s)\n", blocks_len, actual_bitrate / 1000.0, + flag_wav_compress_ulc_mode == ULC_MODE_ABR ? "ABR" : + flag_wav_compress_ulc_mode == ULC_MODE_CBR ? "CBR" : "VBR"); + + if (flag_debug) { + char* wav2fn = changeext(outfn, ".ulc.wav"); + if (flag_verbose) + fprintf(stderr, " writing uncompressed file %s\n", wav2fn); + + struct ULC_DecoderState_t dec = {}; + dec.nChan = wav->channels; + dec.BlockSize = ULC_BLOCK_SIZE; + if (ULC_DecoderState_Init(&dec) <= 0) { + fprintf(stderr, "ERROR: %s: cannot initialize ULC decoder\n", infn); + free(wav2fn); + failed = true; + } else { + int out_len = ULC_BLOCK_SIZE * blocks_len; + int out_pos = 0; + std::vector out_samples(out_len * wav->channels); + std::vector decode_buffer(ULC_BLOCK_SIZE * wav->channels); + + for (int i = 0; i < blocks_len; i++) { + int bits = ULC_DecodeBlock(&dec, decode_buffer.data(), debug_blocks[i].data()); + if (bits <= 0) { + fprintf(stderr, "ERROR: %s: ULC decoding failed at block %d\n", infn, i); + failed = true; + break; + } + + for (int j = 0; j < ULC_BLOCK_SIZE * wav->channels; j++) { + float v = decode_buffer[j] * 32768.0f; + if (v > 32767.0f) { + v = 32767.0f; + } + if (v < -32768.0f) { + v = -32768.0f; + } + out_samples[out_pos++] = v; + } + } + + if (!failed) { + drwav_data_format fmt = { + .container = drwav_container_riff, + .format = DR_WAVE_FORMAT_PCM, + .channels = wav->channels, + .sampleRate = wav->sampleRate, + .bitsPerSample = 16, + }; + drwav wav2; + if (!drwav_init_file_write(&wav2, wav2fn, &fmt, NULL)) { + fprintf(stderr, "ERROR: %s: cannot create WAV file\n", outfn); + failed = true; + } else { + drwav_write_pcm_frames(&wav2, out_len, out_samples.data()); + drwav_uninit(&wav2); + } + } + + ULC_DecoderState_Destroy(&dec); + free(wav2fn); + } + } + ULC_EncoderState_Destroy(&enc); + } break; + + case 3: { // opus + // Number of preroll frames to decode/discard after seeking, to allow + // the decoder to warm up and start producing valid output. + const uint16_t PREROLL_FRAMES = 2; + + // Frame size: for now this is hardcoded to frames of 20ms, which is the + // maximum support by celt and also the best for quality. + // 48 Khz => 960 samples + // 32 Khz => 640 samples + const int FRAMES_PER_SECOND = 50; + int frame_size = wav->sampleRate / FRAMES_PER_SECOND; + int err = OPUS_OK; + + OpusCustomMode *custom_mode = opus_custom_mode_create( + wav->sampleRate, frame_size, &err); + if (err != OPUS_OK) { + fprintf(stderr, "ERROR: %s: cannot create opus custom mode: %s\n", infn, opus_strerror(err)); + failed = true; goto end; + } + + OpusCustomEncoder *enc = opus_custom_encoder_create( + custom_mode, wav->channels, &err); + if (err != OPUS_OK) { + opus_custom_mode_destroy(custom_mode); + fprintf(stderr, "ERROR: %s: cannot create opus encoder: %s\n", infn, opus_strerror(err)); + failed = true; goto end; + } + + // Automatic bitrate calculation for "good quality". This is the same + // algorithm libopus selects when setting OPUS_AUTO bitrate. + int bitrate_bps = 60*FRAMES_PER_SECOND + flag_wav_resample * wav->channels; + if (flag_verbose) + fprintf(stderr, " opus bitrate: %d bps\n", bitrate_bps); + + // Collect seek points (cue points) from the input file. No extra CLI options. + std::vector seek_points = wav->skipPoints; + if (wav->looping) seek_points.push_back(wav->loopOffset); + std::sort(seek_points.begin(), seek_points.end()); + + // Write extended header + w32(out, frame_size); + w32_placeholderf(out, "%s/max_cmp_frame_size", outfn); // max compressed frame size + w32(out, bitrate_bps); + w32(out, 0); // custom mode pointer at runtime + w16(out, PREROLL_FRAMES); + w16(out, (uint16_t)seek_points.size()); + w32(out, 0); // reserved / padding + + // Write seek table with placeholders for file offsets (relative to samples start) + std::unordered_set missing_frame_offsets; + for (size_t si = 0; si < seek_points.size(); si++) { + const int sp = seek_points[si]; + const int frame_idx = sp / frame_size; + const int intra_skip = sp - frame_idx * frame_size; + int pre_idx = frame_idx - (int)PREROLL_FRAMES; + if (pre_idx < 0) pre_idx = 0; + + w32(out, sp); + w32_placeholderf(out, "%s/frame_offset/%d", outfn, pre_idx); // file_offset_preroll (patched later) + w16(out, intra_skip); + w16(out, 0); // padding + missing_frame_offsets.insert(pre_idx); + } + + // Start of samples data + long samples_start = ftell(out); + placeholder_set_offset(out, samples_start-basepos, "%s/samples", outfn); + + // Ask the size of the decoder state to the opus library. This is computed on x86-64 + // so it could be larger than on the N64, but it's a good approximation. + // Add 16 because OpusDecoder has a 16-byte internal alingment, so we add + // some margin. The value is asserted at runtime anyway. + placeholder_set_offset(out, 16+opus_custom_decoder_get_size(custom_mode, wav->channels), "%s/state_size", outfn); + + // Configure opus encoder. We use VBR as it provides the best + // compression/quality balance and we don't have specific constraints + // there. We select the maximum algorithmic complexity to get the best quality. + opus_custom_encoder_ctl(enc, OPUS_SET_BITRATE(bitrate_bps)); + opus_custom_encoder_ctl(enc, OPUS_SET_BANDWIDTH(OPUS_AUTO)); + opus_custom_encoder_ctl(enc, OPUS_SET_VBR(1)); + opus_custom_encoder_ctl(enc, OPUS_SET_VBR_CONSTRAINT(0)); + opus_custom_encoder_ctl(enc, OPUS_SET_COMPLEXITY(10)); + opus_custom_encoder_ctl(enc, OPUS_SET_INBAND_FEC(0)); + opus_custom_encoder_ctl(enc, OPUS_SET_FORCE_CHANNELS(OPUS_AUTO)); + opus_custom_encoder_ctl(enc, OPUS_SET_DTX(0)); + opus_custom_encoder_ctl(enc, OPUS_SET_PACKET_LOSS_PERC(0)); + opus_custom_encoder_ctl(enc, OPUS_SET_LSB_DEPTH(16)); + + // Pad input samples with zeros, rounding to frame size + int newcnt = (wav->cnt + frame_size - 1) / frame_size * frame_size; + wav->samples = (int16_t*)realloc(wav->samples, newcnt * wav->channels * sizeof(int16_t)); + memset(wav->samples + wav->cnt, 0, (newcnt - wav->cnt) * wav->channels * sizeof(int16_t)); + + int max_nb = 0; + int out_max_size = bitrate_bps/8; // overestimation + uint8_t *out_buffer = (uint8_t*)malloc(out_max_size); + + // Compress frames and write them to the output file. While doing so, + // we patch the file offsets of the seek points to allow seeking to + // the correct frame. + int frame_idx = 0; + for (int i=0; isamples + i*wav->channels, frame_size, out_buffer, out_max_size); + if (nb < 0) { + fprintf(stderr, "ERROR: %s: opus encoding failed: %s\n", infn, opus_strerror(nb)); + failed = true; + break; + } + + w16(out, nb); + fwrite(out_buffer, 1, nb, out); + if (nb > max_nb) + max_nb = nb; + walign(out, 2); // make sure frames are 2-byte aligned + frame_idx++; + } + + // Save the maximum compressed frame size to the placeholder. + placeholder_set_offset(out, max_nb, "%s/max_cmp_frame_size", outfn); + + free(out_buffer); + opus_custom_encoder_destroy(enc); + + if (flag_debug) { + fclose(out); + + char* wav2fn = changeext(outfn, ".opus.wav"); + if (flag_verbose) + fprintf(stderr, " writing uncompressed file %s\n", wav2fn); + + out = fopen(outfn, "rb"); + fseek(out, samples_start, SEEK_SET); + OpusCustomDecoder *dec = opus_custom_decoder_create( + custom_mode, wav->channels, &err); + if (err != OPUS_OK) { + opus_custom_mode_destroy(custom_mode); + fprintf(stderr, "ERROR: %s: cannot create opus decoder: %s\n", infn, opus_strerror(err)); + free(wav2fn); + failed = true; goto end; + } + + // Decode the whole file to check for errors + int16_t *out_samples = (int16_t*)malloc(newcnt * wav->channels * sizeof(int16_t)); + int outcnt = 0; + for (int i=0; i in_samples(nb); + fread(&in_samples[0], 1, nb, out); + if (nb & 1) fgetc(out); // align to 2-byte boundary + + int ret = opus_custom_decode(dec, &in_samples[0], nb, out_samples + outcnt*wav->channels, frame_size); + if (ret < 0) { + fprintf(stderr, "ERROR: %s: opus decoding failed: %s\n", infn, opus_strerror(ret)); + failed = true; + break; + } + outcnt += frame_size; + } + + // Save decoded samples into WAV file + if (!failed) { + drwav_data_format fmt = { + .container = drwav_container_riff, + .format = DR_WAVE_FORMAT_PCM, + .channels = wav->channels, + .sampleRate = wav->sampleRate, + .bitsPerSample = 16, + }; + drwav wav2; + if (!drwav_init_file_write(&wav2, wav2fn, &fmt, NULL)) { + fprintf(stderr, "ERROR: %s: cannot create WAV file\n", outfn); + failed = true; + } else { + drwav_write_pcm_frames(&wav2, outcnt, out_samples); + drwav_uninit(&wav2); + } + } + + free(wav2fn); + } + + opus_custom_mode_destroy(custom_mode); + } break; + } + +end: + return !failed; +} + + + +int wav_convert(const char *infn, const char *outfn) { + if (flag_verbose) { + const char *compr[5] = { "raw", "vadpcm", "raw", "opus", "ulc" }; + fprintf(stderr, "Converting: %s => %s (%s)\n", infn, outfn, compr[flag_wav_compress]); + } + + // For wav64 conversion, activate huffman by default + if (flag_wav_compress_vadpcm_huffman < 0) + flag_wav_compress_vadpcm_huffman = 1; + + bool failed = false; + wav_data_t wav = {0}; + + // Read the input file + bool loaded; + if (strcasestr(infn, ".mp3")) + loaded = read_mp3(infn, &wav); + else + loaded = read_wav(infn, &wav); + if (!loaded) { + return 1; + } + + int uncompressedSize; + if (flag_verbose) + fprintf(stderr, " input: %d bits, %d Hz, %d channels\n", wav.bitsPerSample, wav.sampleRate, wav.channels); + uncompressedSize = (int64_t)wav.cnt * wav.channels * wav.bitsPerSample / 8; + + // Apply command line flags if not provided by WAV itself + if (flag_wav_looping_offset > 0 && wav.loopOffset == 0) + wav.loopOffset = flag_wav_looping_offset; + if (flag_wav_looping && !wav.looping) + wav.looping = true; + if (wav.looping && wav.loopEnd == 0) + wav.loopEnd = wav.cnt; + + // Check if the user requested conversion to mono + if (flag_wav_mono && wav.channels == 2) { + if (flag_verbose) + fprintf(stderr, " converting to mono\n"); + + // Allocate a new buffer for the mono samples + int16_t *mono_samples = (int16_t*)malloc(wav.cnt * sizeof(int16_t)); + + // Convert to mono + int16_t *sptr = wav.samples; + int16_t *dptr = mono_samples; + for (int i=0;i 0.0) { + std::vector points; + + // From explicit list (file-based --wav-seek) + if (flag_wav_seek_file) { + points = load_seek_frames_file(flag_wav_seek_file, (double)wav.sampleRate); + } + + // From periodic interval (seconds) + if (flag_wav_seek_interval_sec > 0.0) { + int step = (int)llround(flag_wav_seek_interval_sec * (double)wav.sampleRate); + if (step <= 0) step = 1; + for (int sp = step; sp < wav.cnt; sp += step) + points.push_back(sp); + } + + // Append, then normalize (remove <=0 and out-of-range, sort+unique). + for (int sp : points) { + if (sp > 0 && sp < wav.cnt) + wav.skipPoints.push_back(sp); + } + } + + int wavResampleTo = flag_wav_resample; + + // When compressing with opus, we need to resample to 32 Khz. Whatever value + // was selected by the user, we force it to 32 Khz. + if (flag_wav_compress == 3) { + if (flag_verbose) + fprintf(stderr, " opus only supports %d kHz, forcing resample\n", OPUS_SAMPLE_RATE/1000); + + // For Opus, input files must always be 48 Khz (OPUS_SAMPLE_RATE). + // We will check the real flag_wav_resample later as a way to tune the + // bitrate. + wavResampleTo = OPUS_SAMPLE_RATE; + if (!flag_wav_resample) + flag_wav_resample = wav.sampleRate; + } + + // Do sample rate conversion if requested + if (wavResampleTo && wav.sampleRate != wavResampleTo) { + if (flag_verbose) + fprintf(stderr, " resampling to %d Hz\n", wavResampleTo); + + // Convert input samples to float + float *fsamples_in = (float*)malloc(wav.cnt * wav.channels * sizeof(float)); + src_short_to_float_array(wav.samples, fsamples_in, wav.cnt * wav.channels); + + // Allocate output buffer, estimating the size based on the ratio. + // We add some margin because we are not sure of rounding errors. + int newcnt = (int64_t)wav.cnt * wavResampleTo / wav.sampleRate + 16; + float *fsamples_out = (float*)malloc(newcnt * wav.channels * sizeof(float)); + + // Don't use best quality for files longer than 15 seconds. It is + // extremely slow and it's not worth the time. + int converter = SRC_SINC_BEST_QUALITY; + if (wav.cnt > 15 * wav.sampleRate) { + if (flag_verbose) + fprintf(stderr, " using medium quality resampling for long files\n"); + converter = SRC_SINC_MEDIUM_QUALITY; + } + + // Do the conversion in chunks so we can report progress. + int err = 0; + SRC_STATE *st = src_new(converter, wav.channels, &err); + if (!st || err != 0) { + fprintf(stderr, "ERROR: %s: resampling init failed: %s\n", infn, src_strerror(err)); + free(fsamples_in); + free(fsamples_out); + free(wav.samples); + return 1; + } + + const int64_t total_in_frames = wav.cnt; + const int64_t total_in_bytes = (int64_t)wav.cnt * wav.channels * sizeof(int16_t); + int64_t in_pos = 0; + int64_t out_pos = 0; + int64_t last_print_ms = 0; + double last_pct = -1.0; + + while (in_pos < total_in_frames) { + const int64_t remaining_in = total_in_frames - in_pos; + const int64_t chunk_in = remaining_in > 4096 ? 4096 : remaining_in; + + SRC_DATA d{}; + d.data_in = fsamples_in + in_pos * wav.channels; + d.input_frames = (long)chunk_in; + d.data_out = fsamples_out + out_pos * wav.channels; + d.output_frames = (long)(newcnt - out_pos); + d.src_ratio = (double)wavResampleTo / (double)wav.sampleRate; + d.end_of_input = (in_pos + chunk_in == total_in_frames) ? 1 : 0; + + err = src_process(st, &d); + if (err != 0) break; + + in_pos += d.input_frames_used; + out_pos += d.output_frames_gen; + + resample_progress_print(in_pos * wav.channels * (int64_t)sizeof(int16_t), total_in_bytes, &last_print_ms, &last_pct); + if (d.input_frames_used == 0 && d.output_frames_gen == 0) break; + } + + // Flush. + for (;;) { + SRC_DATA d{}; + d.data_in = NULL; + d.input_frames = 0; + d.data_out = fsamples_out + out_pos * wav.channels; + d.output_frames = (long)(newcnt - out_pos); + d.src_ratio = (double)wavResampleTo / (double)wav.sampleRate; + d.end_of_input = 1; + err = src_process(st, &d); + if (err != 0) break; + out_pos += d.output_frames_gen; + if (d.output_frames_gen == 0) break; + } + + src_delete(st); + if (err == 0) resample_progress_print(total_in_bytes, total_in_bytes, &last_print_ms, &last_pct); + + if (err != 0) { + fprintf(stderr, "ERROR: %s: resampling failed: %s\n", infn, src_strerror(err)); + free(fsamples_in); + free(fsamples_out); + free(wav.samples); + return 1; + } + + // Extract the number of samples generated, and convert back to 16-bit + wav.cnt = (int)out_pos; + wav.samples = (int16_t*)realloc(wav.samples, wav.cnt * wav.channels * sizeof(int16_t)); + src_float_to_short_array(fsamples_out, wav.samples, wav.cnt * wav.channels); + + free(fsamples_in); + free(fsamples_out); + + // Update loop/seek points to the new sample rate + wav.loopOffset = (int)((int64_t)wav.loopOffset * wavResampleTo / wav.sampleRate); + if (wav.loopEnd) + wav.loopEnd = (int)((int64_t)wav.loopEnd * wavResampleTo / wav.sampleRate); + for (size_t i = 0; i < wav.skipPoints.size(); i++) { + wav.skipPoints[i] = (int)((int64_t)wav.skipPoints[i] * wavResampleTo / wav.sampleRate); + } + + // Update wav.sampleRate as it will be used later + wav.sampleRate = wavResampleTo; + } + + // Normalize seek points array + std::sort(wav.skipPoints.begin(), wav.skipPoints.end()); + wav.skipPoints.erase(std::unique(wav.skipPoints.begin(), wav.skipPoints.end()), wav.skipPoints.end()); + + FILE *out = fopen(outfn, "wb"); + placeholder_clear(); + if (!out) { + fprintf(stderr, "ERROR: %s: cannot create file\n", outfn); + free(wav.samples); + return 1; + } + + failed = !wav64_write(infn, outfn, out, &wav, flag_wav_compress); + + // Show a message with the final compression ratio between original file and output file + if (flag_verbose) + fprintf(stderr, " uncompressed: %ld bytes, compressed: %ld bytes (ratio: %.1f%%)\n", + (long)uncompressedSize, (long)ftell(out), (long)ftell(out) * 100.0 / uncompressedSize); + + fclose(out); + free(wav.samples); + if (failed) { + remove(outfn); + return 1; + } + return 0; +} diff --git a/tools/audioconv64/conv_xm64.c b/tools/audioconv64/conv_xm64.c deleted file mode 100644 index 83e2f5a54..000000000 --- a/tools/audioconv64/conv_xm64.c +++ /dev/null @@ -1,239 +0,0 @@ -/* - * We convert XM files to XM64 format. XM64 is a dump of the internal datastructure - * of libxm, similar to the official "libxmize" but portable across different - * architectures (and endian friendly). - * - * On top of this, XM64 has also several benefits and pre-processing: - * - * * Samples with ping-pong loops are unrolled to forward loops, as the RSP - * player does not support ping-pong looping. - * * Patterns are recompressed using a custom RLE algorithm. This helps reducing - * ROM size while still requiring negligible CPU time each time a new pattern - * is loaded (only the current pattern is kept in RAM at any given time). - * The decompression also requires no additional memory (RAM). - * * The module is analyzed to calculate the minimum amount of RAM to be allocated - * for each channel for streaming samples for ROMs. Each channel has a buffer - * that must contain enough samples for playing one "tick", so the exact - * size depends on the playing speed, sample pitch, etc. across the whole - * module. - */ - -#include "mixer.h" - -// Loops made by an odd number of bytes and shorter than this length are -// duplicated to prevent frequency changes during playback. See below for more -// information. -#define XM64_SHORT_ODD_LOOP_LENGTH 1024 - -// Bring libxm in -#include "../../src/audio/libxm/play.c" -#include "../../src/audio/libxm/context.c" -#include "../../src/audio/libxm/load.c" - -int xm_convert(const char *infn, const char *outfn) { - if (flag_verbose) - fprintf(stderr, "Converting: %s => %s\n", infn, outfn); - - FILE *xm = fopen(infn, "rb"); - if (!xm) fatal("cannot open: %s\n", infn); - - fseek(xm, 0, SEEK_END); - int fsize = ftell(xm); - fseek(xm, 0, SEEK_SET); - - char *xmdata = malloc(fsize); - fread(xmdata, 1, fsize, xm); - - size_t mem_ctx, mem_pat, mem_sam; - xm_get_memory_needed_for_context(xmdata, fsize, &mem_ctx, &mem_pat, &mem_sam); - - // Load the XM into a XM context. The specified playback frequency is - // arbitrary, and it doesn't affect the calculations being done of the buffer - // sizes (as those depend on the instrument notes, not the output frequency). - xm_context_t* ctx; - xm_create_context_safe(&ctx, xmdata, fsize, 48000); - if (!ctx) fatal("cannot read XM file: invalid format?"); - free(xmdata); - - // Pre-process all waveforms: - // 1) Ping-pong loops will be unrolled as regular forward - // 2) Repeat initial data after loop end for MIXER_LOOP_OVERREAD bytes - // to speed up decoding in RSP. - for (int i=0;imodule.num_instruments;i++) { - xm_instrument_t *ins = &ctx->module.instruments[i]; - - for (int j=0;jnum_samples;j++) { - xm_sample_t *s = &ins->samples[j]; - int bps = s->bits / 8; - - uint32_t length = s->length * bps; - uint32_t loop_length = s->loop_length * bps; - uint32_t loop_end = s->loop_end * bps; - - uint8_t *sout, *out; - switch (s->loop_type) { - default: - fatal("invalid loop type: %d\n", s->loop_type); - case XM_NO_LOOP: - sout = malloc(length + MIXER_LOOP_OVERREAD); - memcpy(sout, s->data8, length); - memset(sout+length, 0, MIXER_LOOP_OVERREAD); - break; - case XM_FORWARD_LOOP: - // Special case for odd-sized loops of 8-bit samples. We cannot - // properly handle these at runtime because they cannot be DMA'd - // as they change the 2-byte phase between ROM and RAM. - // xm64.c will decrease the loop length by 1 byte to playback them, - // but this can affect the period in case of short loops: - // for instance, a 13-bytes loop shortened 12-bytes change the - // period by 7%, which can be several notes of difference at - // high frequencies. - // So for short loops (<1024 bytes), we just duplicate the loop - // itself to make it of even size. For longer loops, the period - // error made by xm64 when shortening is < 0.1%, which isn't - // audible. - if (bps == 1 && loop_length%2 == 1 && loop_length < XM64_SHORT_ODD_LOOP_LENGTH) { - sout = malloc(loop_end + loop_length + MIXER_LOOP_OVERREAD); - length = loop_end+loop_length; - // Copy waveform until loop end - memcpy(sout, s->data8, loop_end); - // Duplicate loop - memmove(sout + loop_end, s->data8 + loop_end - loop_length, loop_length); - // Add overread - memmove(sout + loop_end + loop_length, s->data8 + loop_end - loop_length, MIXER_LOOP_OVERREAD); - loop_end += loop_length; - loop_length *= 2; - } else { - sout = malloc(loop_end + MIXER_LOOP_OVERREAD); - length = loop_end; - // Copy waveform until loop end - memcpy(sout, s->data8, loop_end); - // Add overread - memmove(sout + loop_end, s->data8 + loop_end - loop_length, MIXER_LOOP_OVERREAD); - } - break; - case XM_PING_PONG_LOOP: - length = loop_end + loop_length; - sout = malloc(length + MIXER_LOOP_OVERREAD); - out = sout; - - memcpy(out, s->data8, loop_end); - out += loop_end; - for (int x=0;xdata8[(loop_end-x-1) ^ (bps>>1)]; - memmove(out, s->data8 + loop_end - loop_length, MIXER_LOOP_OVERREAD); - - loop_end += loop_length; - loop_length *= 2; - s->loop_type = XM_FORWARD_LOOP; - break; - } - - // If the sample length changed, update the memory - // required for the context. - if (length != s->length*bps) - { - #define ALIGN8(n) ((((n) + 7) >> 3) << 3) - ctx->ctx_size -= ALIGN8(s->length*bps); - ctx->ctx_size_all_samples -= ALIGN8(s->length*bps); - ctx->ctx_size += ALIGN8(length); - ctx->ctx_size_all_samples += ALIGN8(length); - } - s->length = length / bps; - s->loop_length = loop_length / bps; - s->loop_end = loop_end / bps; - s->data8 = (int8_t*)sout; - } - } - - // Calculate the optimal sample buffer size for each channel. - // To do this, go through the whole song once doing a "dry run" playback; - // for every tick, check which waveforms are currently played and at what - // frequency, calculate the sample buffer size required at that tick, - // and keep the maximum. - int ch_buf[32] = {0}; - - while (xm_get_loop_count(ctx) == 0) { - xm_tick(ctx); - - // Number of samples that will be generated for this tick. - int nsamples = ceilf(ctx->remaining_samples_in_tick); - for(int i = 0; i < ctx->module.num_channels; ++i) { - xm_channel_context_t *ch = &ctx->channels[i]; - - if (ch->instrument && ch->sample) { - // Number of samples for this waveform at this playback frequency - // (capped at the waveform length) - int n = ceilf(ch->step * nsamples); - if (n > ch->sample->length) { - n = ch->sample->length; - } - - // Convert samples to bytes - if (ch->sample->bits == 16) - n *= 2; - - // Take overread buffer into account - n += MIXER_LOOP_OVERREAD; - - // Keep the maximum - if (ch_buf[i] < n) - ch_buf[i] = n; - } - } - ctx->remaining_samples_in_tick -= nsamples; - } - - int sam_size = 0; - for (int i=0;imodule.num_channels;i++) { - // Add a 5% of margin, just in case there is a bug somewhere. We're still - // pretty tight on RAM so let's not exaggerate. - ch_buf[i] = ch_buf[i] * 1.05; - - // Round up to 8 bytes, which is the required alignment for a sample buffer. - ch_buf[i] = ((ch_buf[i] + 7) / 8) * 8; - - // Save the size in the context structure. It will be used at playback - // time to allocate the correct amount of sample buffers. - ctx->ctx_size_stream_sample_buf[i] = ch_buf[i]; - sam_size += ch_buf[i]; - } - - FILE *out = fopen(outfn, "wb"); - if (!out) fatal("cannot create: %s", outfn); - xm_context_save(ctx, out); - int romsize = ftell(out); - fclose(out); - - // Dump some statistics for the conversion - if (flag_verbose) { - fprintf(stderr, " * ROM size: %u KiB (samples:%zu)\n", - romsize / 1024, mem_sam / 1024); - fprintf(stderr, " * RAM size: %zu KiB (ctx:%zu, patterns:%u, samples:%u)\n", - (mem_ctx+sam_size+ctx->ctx_size_stream_pattern_buf)/1024, - mem_ctx / 1024, - ctx->ctx_size_stream_pattern_buf / 1024, - sam_size / 1024 - ); - fprintf(stderr, " * Samples RAM per channel: ["); - for (int i=0;imodule.num_channels;i++) { - if (i!=0) fprintf(stderr, ", "); - fprintf(stderr, "%d", ch_buf[i]); - } - fprintf(stderr, "]\n"); - } - - - // Try reloading the just-created file. This is just a safety net to catch - // serialization bugs and other mistakes immediately at conversion time. - // It's not required if we can't trust our own code, but it's not a big - // deal anyway, so better safe than sorry. - xm_context_t *ctx2; - out = fopen(outfn, "rb"); - if (!out) fatal("cannot open: %s", outfn); - int ret = xm_context_load(&ctx2, out, 48000); - if (ret != 0) fatal("internal error: loading just created module: %s (ret:%d)", outfn, ret); - fclose(out); - - return 0; -} diff --git a/tools/audioconv64/conv_xm64.cpp b/tools/audioconv64/conv_xm64.cpp new file mode 100644 index 000000000..fbcac9a8e --- /dev/null +++ b/tools/audioconv64/conv_xm64.cpp @@ -0,0 +1,992 @@ +/* + conv_xm64: convert XM files to XM64 format + Written by Giovanni Bajo + + This tool is part of the Libdragon SDK. + + This is free and unencumbered software released into the public domain. + + For more information, please refer to +*/ +/* + * We convert XM files to XM64 format. XM64 is a dump of the internal datastructure + * of libxm, similar to the official "libxmize" but portable across different + * architectures (and endian friendly). + * + * On top of this, XM64 has also several benefits and pre-processing: + * + * * Samples with ping-pong loops are unrolled to forward loops, as the RSP + * player does not support ping-pong looping. + * * Patterns are recompressed using a custom RLE algorithm. This helps reducing + * ROM size while still requiring negligible CPU time each time a new pattern + * is loaded (only the current pattern is kept in RAM at any given time). + * The decompression also requires no additional memory (RAM). + * * The module is analyzed to calculate the minimum amount of RAM to be allocated + * for each channel for streaming samples for ROMs. Each channel has a buffer + * that must contain enough samples for playing one "tick", so the exact + * size depends on the playing speed, sample pitch, etc. across the whole + * module. + */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../common/binout.h" +#include "../common/nanotime.h" +#include "../common/polyfill.h" +#include "../common/utils.h" +#include "../common/assetcomp.h" +#include "audioconv64.h" +#include "mixer.h" +#include "samplebuffer.h" +#include "libxm.h" +#include "../common/crc32.c" + +int flag_xm_compress_meta = DEFAULT_COMPRESSION; +int flag_xm_compress_samples = DEFAULT_COMPRESSION; +bool flag_xm_8bit = false; +const char *flag_xm_extsampledir = NULL; + +static std::map> sample_skip_points; +/** Per-sample VADPCM attack window (frames), keyed by sample CRC32. */ +static std::map sample_attack_by_hash; +/** Samples marked for full RDRAM preload (keyed by CRC32). */ +static std::map sample_resident_by_hash; + +// Loops made by an odd number of bytes and shorter than this length are +// duplicated to prevent frequency changes during playback. See below for more +// information. +#define XM64_SHORT_ODD_LOOP_LENGTH 1024 + +static uint32_t xm_sample_crc32(xm_sample_t *s) +{ + if (s->bits == 8) + return crc32((uint8_t*)s->data8, s->length); + else + return crc32((uint8_t*)s->data16, s->length * 2); +} + +static void xm_save_wave64(xm_sample_t *s, FILE *out, const char *outfn) +{ + int16_t *samples16 = (int16_t*)malloc(s->length * sizeof(int16_t)); + if (s->bits == 8) { + for (int k=0;klength;k++) + samples16[k] = (s->data8[k] << 8) | (uint8_t)s->data8[k]; + } else { + memcpy(samples16, s->data16, s->length * sizeof(int16_t)); + } + + wav_data_t wav = { + .samples = samples16, + .cnt = s->length, + .channels = 1, + .bitsPerSample = s->bits, + .sampleRate = 44100, + .looping = s->loop_type != 0, + .loopOffset = s->loop_start, + .loopEnd = s->loop_type != 0 ? (int)s->loop_end : 0, + }; + for (auto pos : sample_skip_points[s]) + wav.skipPoints.push_back(pos); + wav.attack_frames = sample_attack_by_hash[xm_sample_crc32(s)]; + wav.resident = sample_resident_by_hash[xm_sample_crc32(s)]; + // Resident samples already live in RDRAM: no attack window. + if (wav.resident) + wav.attack_frames = 0; + + if (!wav64_write("xm", outfn, out, &wav, flag_xm_compress_samples)) + fatal("ERROR: failure while writing %s\n", outfn); + + free(wav.samples); + + if (wav.looping) { + // Adjust loop information as they might have changed during compression + int lend = wav.loopEnd ? wav.loopEnd : wav.cnt; + s->loop_start = wav.loopOffset; + s->loop_length = lend - wav.loopOffset; + s->loop_end = lend; + s->length = wav.cnt; + } +} + +static void xm_save_wave_internally(xm_context_t* ctx, FILE* meta, FILE* out, const char *outfn, int totsamples) +{ + // Use a sample structure to do internal de-duplication of samples + struct sample_checksum { + uint32_t hash; + uint32_t pos; + xm_sample_t *s; + }; + std::vector wave_sums(totsamples+1); + + for (int i=0;imodule.num_instruments;i++) { + xm_instrument_t *ins = &ctx->module.instruments[i]; + for (int j=0;jnum_samples;j++) { + xm_sample_t *s = &ins->samples[j]; + assert(s->bits == 8 || s->bits == 16); + + // Check if the sample has been already seen in this module + uint32_t hash = xm_sample_crc32(s); + int k = 0; + while (wave_sums[k].pos > 0 && wave_sums[k].hash != hash) { + k++; + } + if (wave_sums[k].pos == 0) { + walign(out, 2); + wave_sums[k].hash = hash; + wave_sums[k].pos = ftell(out); + wave_sums[k].s = s; + + char *wavfn = NULL; asprintf(&wavfn, "%s.%d.%d.wav64", outfn, i, j); // used only for --debug + xm_save_wave64(s, out, wavfn); + free(wavfn); + } else { + // Sample already seen. Make sure to update the sample + // with information that might change during compression + if (s->loop_type != 0) { + s->length = wave_sums[k].s->length; + s->loop_start = wave_sums[k].s->loop_start; + s->loop_length = wave_sums[k].s->loop_length; + s->loop_end = wave_sums[k].s->loop_end; + s->loop_type = wave_sums[k].s->loop_type; + } + } + + placeholder_set_offset(meta, wave_sums[k].pos, "sample_%d_%d", i, j); + } + } +} + +static void xm_save_wave_externally(xm_context_t* ctx, FILE *meta, FILE* out, const char *outfn, int totsamples) +{ + for (int i=0;imodule.num_instruments;i++) { + xm_instrument_t *ins = &ctx->module.instruments[i]; + for (int j=0;jnum_samples;j++) { + xm_sample_t *s = &ins->samples[j]; + assert(s->bits == 8 || s->bits == 16); + + // Rewrite the file atomically. Notice that the file might already + // exist since it's a shared archive of wavs, but we want to rewrite + // it anyway using our own settings (eg: compression). This gives a + // better developer experience as changes in Makefile are effective even + // with "make -B" or in general if any input file is changed. + uint32_t hash = xm_sample_crc32(s); + char *filename = NULL; + asprintf(&filename, "%s/%08x.wav64", flag_xm_extsampledir, hash); + + // Create a temporary unique file name embedding a random number + char *tmpname = NULL; + asprintf(&tmpname, "%s/%x.%" PRIx64 ".tmp", flag_xm_extsampledir, getpid(), nanotime()); + + int fd = open(tmpname, O_WRONLY | O_CREAT | O_EXCL, 0644); + if (fd < 0) { + fprintf(stderr, "FATAL: cannot create %s: %s\n", filename, strerror(errno)); + exit(1); + } + + // Write the file + FILE *wavf = fdopen(fd, "wb"); + if (!wavf) fatal("ERROR: cannot create %s: %s\n", filename, strerror(errno)); + char *wavfn=NULL; asprintf(&wavfn, "%s.%d.%d.wav64", outfn, i, j); // used only for --debug + + xm_save_wave64(s, wavf, wavfn); + + fclose(wavf); + free(wavfn); + rename(tmpname, filename); + + // Save the WAV hash in the XM64 file as "offset" of the waveform + placeholder_set_offset(meta, hash, "sample_%d_%d", i, j); + free(filename); + free(tmpname); + } + } +} + + +static void xm_context_save(xm_context_t* ctx, FILE* xm64, const char *outfn) { + // Version log: + // 5: first public version + // 6: added overread for non-looping samples. The size of optimal + // stream sample buffer size must change, hance the version bump. + // 7: switch to wav64 for samples, and add support for external samples + // 8: patterns are compressed with asset library + // 9: metadata compressed with asset library + // 10: added sample position memory to xm_channel_context_t + // 11: change sample_position to double + // 12: extra sample-buffer headroom for syncless mixer in-flight rounds + // 13: sample-buffer sizing as a rate, scaled at playback by the queue depth + const uint8_t version = 13; + wa(xm64, "XM64", 4); + w8(xm64, version); + w32_placeholderf(xm64, "metadata_offset"); + w32_placeholderf(xm64, "metadata_size"); + + // Write metadata into a temporary file + FILE *meta = tmpfile(); + assert(meta && "Temporary file creation failed"); + + int totsamples = 0; + for (int i=0;imodule.num_instruments;i++) { + xm_instrument_t *ins = &ctx->module.instruments[i]; + totsamples += ins->num_samples; + } + + // Write the samples (either internally or externally). We do this before + // writing the metadata as the process might cause change some samples + // (eg for compression requirements). + if (flag_xm_extsampledir) + xm_save_wave_externally(ctx, meta, xm64, outfn, totsamples); + else + xm_save_wave_internally(ctx, meta, xm64, outfn, totsamples); + + w32(meta, ctx->ctx_size); + w32(meta, ctx->ctx_size_all_patterns); + w32(meta, ctx->ctx_size_all_samples); + w32_placeholderf(meta, "ctx_size_stream_pattern_buf"); + for (int i=0; i<32; i++) w32(meta, ctx->ctx_stream_buf_rate[i]); + for (int i=0; i<32; i++) w32(meta, ctx->ctx_stream_buf_base[i]); + for (int i=0; i<32; i++) w32(meta, ctx->ctx_stream_buf_min[i]); + for (int i=0; i<32; i++) w32(meta, ctx->ctx_stream_buf_cap[i]); + + w16(meta, ctx->module.tempo); + w16(meta, ctx->module.bpm); + +#if XM_STRINGS + wa(meta, ctx->module.name, sizeof(ctx->module.name)); + wa(meta, ctx->module.trackername, sizeof(ctx->module.trackername)); +#else + char name[MODULE_NAME_LENGTH+1] = {0}; char trackername[TRACKER_NAME_LENGTH+1] = {0}; + wa(meta, name, sizeof(name)); wa(meta, trackername, sizeof(trackername)); +#endif + w16(meta, ctx->module.length); + w16(meta, ctx->module.restart_position); + w16(meta, ctx->module.num_channels); + w16(meta, ctx->module.num_patterns); + w16(meta, ctx->module.num_instruments); + w32(meta, ctx->module.frequency_type); + wa(meta, ctx->module.pattern_table, sizeof(ctx->module.pattern_table)); + + for (int i=0;imodule.num_patterns;i++) { + w16(meta, ctx->module.patterns[i].num_rows); + w32_placeholderf(meta, "pattern_%d", i); + w16_placeholderf(meta, "pattern_size_%d", i); + } + + for (int i=0;imodule.num_instruments;i++) { + xm_instrument_t *ins = &ctx->module.instruments[i]; +#if XM_STRINGS + wa(meta, ins->name, sizeof(ins->name)); +#else + char name[INSTRUMENT_NAME_LENGTH + 1] = {0}; + wa(meta, name); +#endif + wa(meta, ins->sample_of_notes, sizeof(ins->sample_of_notes)); + + w8(meta, ins->volume_envelope.num_points); + for (int j=0;jvolume_envelope.num_points;j++) { + w16(meta, ins->volume_envelope.points[j].frame); + w16(meta, ins->volume_envelope.points[j].value); + } + w8(meta, ins->volume_envelope.sustain_point); + w8(meta, ins->volume_envelope.loop_start_point); + w8(meta, ins->volume_envelope.loop_end_point); + w8(meta, ins->volume_envelope.enabled); + w8(meta, ins->volume_envelope.sustain_enabled); + w8(meta, ins->volume_envelope.loop_enabled); + + w8(meta, ins->panning_envelope.num_points); + for (int j=0;jpanning_envelope.num_points;j++) { + w16(meta, ins->panning_envelope.points[j].frame); + w16(meta, ins->panning_envelope.points[j].value); + } + w8(meta, ins->panning_envelope.sustain_point); + w8(meta, ins->panning_envelope.loop_start_point); + w8(meta, ins->panning_envelope.loop_end_point); + w8(meta, ins->panning_envelope.enabled); + w8(meta, ins->panning_envelope.sustain_enabled); + w8(meta, ins->panning_envelope.loop_enabled); + + w32(meta, ins->vibrato_type); + w8(meta, ins->vibrato_sweep); + w8(meta, ins->vibrato_depth); + w8(meta, ins->vibrato_rate); + w16(meta, ins->volume_fadeout); + w64(meta, ins->latest_trigger); + + w16(meta, ins->num_samples); + for (int j=0;jnum_samples;j++) { + xm_sample_t *s = &ins->samples[j]; + // NOTE: use original bitsize here (even if VADPCM is always 16-bit) + // This is useful at least for 0x9 command (set sample offset) that + // requires to know the original bitsize of the sample. + // The WAV64 will be marked as 16-bit with VADPCM instead, so that + // playback will be correct. + w8(meta, s->bits); + w32(meta, s->length); + w32(meta, s->loop_start); + w32(meta, s->loop_length); + w32(meta, s->loop_end); + wf32(meta, s->volume); + w8(meta, s->finetune); + w32(meta, s->loop_type); + wf32(meta, s->panning); + w8(meta, s->relative_note); + w32_placeholderf(meta, "sample_%d_%d", i, j); + } + } + w8(meta, (flag_xm_extsampledir != NULL)); + + // Write the patterns (potentially compressed) + int max_inplace_margin = 0; + for (int i=0;imodule.num_patterns;i++) { + walign(xm64, 2); + int pos = ftell(xm64); + + xm_pattern_t *p = &ctx->module.patterns[i]; + + int pat_size = p->num_rows*ctx->module.num_channels*5; + std::vector cur_pat(pat_size); + uint8_t *pp = &cur_pat[0]; + + xm_pattern_slot_t *s = &p->slots[0]; + for (int k=0;kmodule.num_channels;k++) { + for (int j=0;jnum_rows;j++) { + *pp++ = s->note; + *pp++ = s->instrument; + *pp++ = s->volume_column; + *pp++ = s->effect_type; + *pp++ = s->effect_param; + s++; + } + } + + int inplace_margin; + asset_compress_mem(&cur_pat[0], pat_size, xm64, flag_xm_compress_meta, 0, &inplace_margin); + if (inplace_margin > max_inplace_margin) + max_inplace_margin = inplace_margin; + placeholder_set_offset(meta, pos, "pattern_%d", i); + placeholder_set_offset(meta, ftell(xm64) - pos, "pattern_size_%d", i); + } + + // Add the necessary maximum margin to the size of the pattern buffer + // See the code in asset_buf_size() for more information. + ctx->ctx_size_stream_pattern_buf += max_inplace_margin; + ctx->ctx_size_stream_pattern_buf += 4; // max alignment for 32-bit loads (for shrinkler) + ctx->ctx_size_stream_pattern_buf += 8; // margin for OOB writes of decompressors + ctx->ctx_size_stream_pattern_buf = (ctx->ctx_size_stream_pattern_buf + 15) / 16 * 16; + placeholder_set_offset(meta, ctx->ctx_size_stream_pattern_buf, "ctx_size_stream_pattern_buf"); + + // Now we have completed the file. Read back the metadata and compress them + // to the xm64 output file. + std::vector metadata = slurp(meta); + fclose(meta); + walign(xm64, 2); + placeholder_set(xm64, "metadata_offset"); + placeholder_set_offset(xm64, metadata.size(), "metadata_size"); + + asset_compress_mem(metadata.data(), metadata.size(), xm64, flag_xm_compress_meta, 0, NULL); +} + +static void xm_remove_empty_samples(xm_context_t *ctx) +{ + // Some instruments may have empty samples (length=0). Remove them and + // remap the sample numbers in the instrument. + for (int i=0;imodule.num_instruments;i++) { + xm_instrument_t *ins = &ctx->module.instruments[i]; + std::vector sample_remap(ins->num_samples); + + int j = 0; + for (int k=0;knum_samples;k++) { + if (ins->samples[k].length > 0) { + sample_remap[k] = j; + if (j != k) ins->samples[j] = ins->samples[k]; + j++; + } else { + sample_remap[k] = -1; + } + } + + // Update sample_of_notes + for (int k=0;ksample_of_notes[k] < ins->num_samples) + ins->sample_of_notes[k] = sample_remap[ins->sample_of_notes[k]]; + } + + // Update total count of samples + ins->num_samples = j; + } +} + + + +/** Bytes needed to LOOP_CACHE a loop of @p L samples in the channel buffer. */ +static int xm_loop_pin_bytes(int L) +{ + if (flag_xm_compress_samples > 0) { + int fbytes = vadpcm_frame_bytes(flag_wav_compress_vadpcm_bits); + int nframes = (L + 15) / 16; + nframes += (MIXER_LOOP_OVERREAD + fbytes - 1) / fbytes + 1; + return nframes * fbytes; + } + return L * 2 + MIXER_LOOP_OVERREAD + 2; +} + +/** RDRAM cost of making @p s fully resident (VADPCM body or PCM). */ +static int xm_sample_resident_bytes(xm_sample_t *s) +{ + if (flag_xm_compress_samples > 0) + return (int)(((s->length + 15) / 16) * vadpcm_frame_bytes(flag_wav_compress_vadpcm_bits)); + return (int)(s->length * (s->bits / 8)); +} + +/** + * Per-channel samplebuffer sizing. + * + * The ring has to span everything the RSP may still have to read, which is + * what the mixer lets the CPU enqueue before waiting for it: that span is + * only known at playback time, so it is kept as a rate here and turned into + * bytes by #xm64player_play. + */ +struct ch_sizing_t { + double rate; ///< Stream bytes consumed per second of playback, at the top pitch + int base; ///< Bytes that do not scale with the queue (prefetch, overread) + int cap; ///< Bytes of the longest sample: nothing on the channel needs more + int pin; ///< Floor to hold a pinned loop, 0 if none +}; + +/** Reference queue depth for the pin budget: two AI buffers at 44100 Hz, + * which is what the mixer lets the CPU run ahead (MIXER_POLL_LOOKAHEAD). */ +#define XM_REF_INFLIGHT_SAMPLES 3528 +#define XM_REF_OUTPUT_RATE 44100 + +/** + * Bytes this sizing asks for when the CPU can run @p inflight output samples + * ahead, mixing at @p out_rate. The stream is consumed in wall-clock time, so + * the output rate of the game is what turns that queue into a duration. + */ +static int xm_sizing_bytes(const ch_sizing_t &s, int inflight, int out_rate) +{ + if (s.rate <= 0 && !s.pin) + return 0; + int n = s.base + (int)ceil(s.rate * inflight / out_rate); + if (s.cap && n > s.cap) n = s.cap; + if (n < s.pin) n = s.pin; + return (n + 7) / 8 * 8; +} + +/** + * Dry-run the whole module (every pattern order) to collect sizing inputs: + * per-channel streaming rates (@p ch_sz), max pitch of each looping sample + * on each channel (@p ch_loop_freq), note-ons from offset 0 and their same-tick + * burst size, 9xx skip points, and which instrument samples are used. + */ +static void xm_dry_run(xm_context_t *ctx, ch_sizing_t ch_sz[32], + std::map ch_loop_freq[32], + std::map ¬eons_from0, + std::map &cold_burst, + bool **used_samples) +{ + int num_orders = xm_get_module_length(ctx); + bool played_orders[PATTERN_ORDER_TABLE_LENGTH] = {0}; + + while (1) { + do { + xm_tick(ctx); + + uint8_t pat_idx; + xm_get_position(ctx, &pat_idx, NULL, NULL, NULL); + played_orders[pat_idx] = true; + + int nsamples = ceilf(ctx->remaining_samples_in_tick); + xm_sample_t *cold[32]; + int ncold = 0; + for (int i = 0; i < ctx->module.num_channels; ++i) { + xm_channel_context_t *ch = &ctx->channels[i]; + if (!ch->instrument || !ch->sample) + continue; + + int ins_idx = ch->instrument - ctx->module.instruments; + int smp_idx = ch->sample - ch->instrument->samples; + bool *used_samp_inst = used_samples[ins_idx]; + if (smp_idx >= 0 && smp_idx < ch->instrument->num_samples) + used_samp_inst[smp_idx] = true; + + // Bytes of stream per output sample at this pitch, and the + // bytes on top of that which the queue depth does not change: + // what #samplebuffer_prefetch keeps ready, the RSP overread + // past a loop, and the framing slack of a block codec. + double bps; // bytes per source sample + int ub, extra; // samplebuffer unit, fixed bytes + if (flag_xm_compress_samples > 0) { + int fbytes = vadpcm_frame_bytes(flag_wav_compress_vadpcm_bits); + ub = fbytes; + bps = fbytes / 16.0; + int nframes = (MIXER_LOOP_OVERREAD + fbytes - 1) / fbytes + 2; + if (ch->sample->loop_type) + nframes += 2; + extra = nframes * fbytes; + } else { + ub = ch->sample->bits == 16 ? 2 : 1; + bps = ub; + extra = MIXER_LOOP_OVERREAD; + } + extra += SAMPLEBUFFER_MARGIN_UNITS * ub; + + ch_sizing_t &sz = ch_sz[i]; + double rate = ch->step * bps * ctx->rate; + if (sz.rate < rate) sz.rate = rate; + if (sz.base < extra) sz.base = extra; + int cap = (int)ceil(ch->sample->length * bps) + extra; + if (sz.cap < cap) sz.cap = cap; + + if (ch->sample->loop_type && ch->sample->loop_length > 0) { + float &f = ch_loop_freq[i][ch->sample]; + if (ch->frequency > f) + f = ch->frequency; + } + + bool key_on = ch->current->note > 0 && ch->current->note < 97; + if (key_on && ch->current->effect_type == 0x9) + sample_skip_points[ch->sample].insert(ch->sample_position); + // Tick 0 of the row, no 9xx (or 9xx=0): cold PI from sample start. + if (key_on && ctx->current_tick == 0 + && (ch->current->effect_type != 0x9 || ch->current->effect_param == 0)) + cold[ncold++] = ch->sample; + } + for (int k = 0; k < ncold; k++) { + noteons_from0[cold[k]]++; + if (cold_burst[cold[k]] < ncold) + cold_burst[cold[k]] = ncold; + } + ctx->remaining_samples_in_tick -= nsamples; + } while (xm_get_loop_count(ctx) == 0); + + // Force playback of every non-empty pattern order (multi sub-song XMs). + bool fully_played = true; + for (int i = 0; i < num_orders; i++) { + if (!played_orders[i]) { + if (flag_verbose) + fprintf(stderr, " * found potential sub-song starting at pattern index: %d\n", i); + xm_seek(ctx, i, 0, 0); + fully_played = false; + break; + } + } + if (fully_played) + break; + } +} + +/** + * Size each channel's samplebuffer for streaming, then optionally grow it to + * LOOP_CACHE a loop (or mark the sample resident). + * + * A loop is worth pinning on a channel when the extra RAM is repaid by about + * one second of avoided PI traffic at that pitch: + * pin_sz - stream_sz <= freq · bps + * Affordable pin candidates (budget ≤ streaming footprint) are ranked by + * (channels × pitch / loop_len / pin_extra). When a candidate's turn comes, + * prefer one resident copy of the body if that is cheaper than growing the + * channel buffers — never against a pin that would not fit the budget. + * + * Writes ctx->ctx_stream_buf_*[] and sample_resident_by_hash. + * @return total samplebuffer bytes; *@p res_bytes is resident RDRAM. + */ +static int xm_size_loop_buffers(xm_context_t *ctx, ch_sizing_t ch_sz[32], + std::map ch_loop_freq[32], int *res_bytes_out) +{ + const float PIN_LOOP_HORIZON_SEC = 1.0f; + + // Baseline streaming size per channel, at a reference queue depth: the + // real one is a playback-time choice, but the pin decisions below only + // need the two costs to be comparable. Cap the pin budget at this total + // so pin RAM cannot exceed what streaming already spends. + int stream_sz[32] = {0}; + int stream_total = 0; + for (int i = 0; i < ctx->module.num_channels; i++) { + ch_sz[i].rate *= 1.05; + stream_sz[i] = xm_sizing_bytes(ch_sz[i], XM_REF_INFLIGHT_SAMPLES, XM_REF_OUTPUT_RATE); + stream_total += stream_sz[i]; + } + + float bps = flag_xm_compress_samples > 0 + ? vadpcm_frame_bytes(flag_wav_compress_vadpcm_bits) / 16.0f : 2.0f; + + // Aggregate identical looping samples (same CRC) across channels: one + // body may play on several channels, so pin/resident decisions must be + // per unique waveform, not per (channel, sample) pair. + struct loop_info { + xm_sample_t *s; + std::vector chs; + float freq_ch[32]; + int psz, pin_extra; + float score; + }; + std::map by_hash; + for (int i = 0; i < ctx->module.num_channels; i++) { + for (auto const &kv : ch_loop_freq[i]) { + uint32_t h = xm_sample_crc32(kv.first); + auto &info = by_hash[h]; + if (!info.s) { + info.s = kv.first; + memset(info.freq_ch, 0, sizeof(info.freq_ch)); + } + info.chs.push_back(i); + if (kv.second > info.freq_ch[i]) + info.freq_ch[i] = kv.second; + } + } + + // Keep only loops worth pinning: on at least one channel, the extra RAM + // to hold the full loop is repaid by ~1s of PI traffic at that pitch. + // Score favors short hot loops shared by many channels (PI win / cost). + std::vector> pin_cands; + for (auto &kv : by_hash) { + loop_info &info = kv.second; + xm_sample_t *s = info.s; + int L = s->loop_length; + if (L <= 0) continue; + info.psz = xm_loop_pin_bytes(L); + info.pin_extra = 0; + bool worth = false; + float max_freq = 0; + for (int ch : info.chs) { + int extra = info.psz > stream_sz[ch] ? info.psz - stream_sz[ch] : 0; + info.pin_extra += extra; + float freq = info.freq_ch[ch]; + if (freq > max_freq) max_freq = freq; + if (extra > 0 && (double)extra <= (double)PIN_LOOP_HORIZON_SEC * (double)freq * (double)bps) + worth = true; + } + if (!worth || info.pin_extra <= 0) + continue; + info.score = (float)info.chs.size() * max_freq / (float)L / (float)info.pin_extra; + pin_cands.push_back({ info.score, kv.first }); + } + std::sort(pin_cands.begin(), pin_cands.end(), + [](const std::pair &a, const std::pair &b) { + return a.first > b.first; + }); + + // Spend the pin budget in score order. For each affordable candidate, + // prefer a single resident copy of the body when cheaper than growing + // every channel's samplebuffer; otherwise grow those buffers (LOOP_CACHE). + // Candidates that do not fit the remaining budget are left streaming. + int grown[32]; + memcpy(grown, stream_sz, sizeof(grown)); + int pin_budget = stream_total; + int pin_spent = 0; + int res_n = 0, res_bytes = 0; + for (auto const &pc : pin_cands) { + loop_info &info = by_hash[pc.second]; + int cost = 0; + for (int ch : info.chs) + if (info.psz > grown[ch]) + cost += info.psz - grown[ch]; + if (cost <= 0 || cost > pin_budget - pin_spent) + continue; + int rcost = xm_sample_resident_bytes(info.s); + if (rcost <= cost) { + sample_resident_by_hash[pc.second] = true; + res_n++; + res_bytes += rcost; + if (flag_verbose) + fprintf(stderr, " * resident loop=%d len=%d: %d B once < %d B pin (%d ch)\n", + info.s->loop_length, (int)info.s->length, rcost, cost, (int)info.chs.size()); + continue; + } + pin_spent += cost; + for (int ch : info.chs) + if (info.psz > grown[ch]) + grown[ch] = info.psz; + if (flag_verbose) + fprintf(stderr, " * pin loop=%d: +%d B across %d ch (score=%.3g)\n", + info.s->loop_length, cost, (int)info.chs.size(), info.score); + } + + // Commit the sizing into the XM context. A channel grown to hold a pinned + // loop carries that as a floor; the rest is recomputed at playback time + // from the queue depth in use. + int sam_size = 0; + for (int i = 0; i < ctx->module.num_channels; i++) { + if (grown[i] > stream_sz[i]) + ch_sz[i].pin = ((grown[i] + 7) / 8) * 8; + ctx->ctx_stream_buf_rate[i] = (uint32_t)ceil(ch_sz[i].rate); + ctx->ctx_stream_buf_base[i] = ch_sz[i].base; + ctx->ctx_stream_buf_min[i] = ch_sz[i].pin; + ctx->ctx_stream_buf_cap[i] = ch_sz[i].cap; + sam_size += xm_sizing_bytes(ch_sz[i], XM_REF_INFLIGHT_SAMPLES, XM_REF_OUTPUT_RATE); + } + if (flag_verbose) { + if (res_n) + fprintf(stderr, " * resident: %d samples → %d KiB\n", res_n, (res_bytes + 1023) / 1024); + fprintf(stderr, " * loop pin: +%d KiB (budget %d KiB)\n", + (pin_spent + 1023) / 1024, (pin_budget + 1023) / 1024); + } + *res_bytes_out = res_bytes; + return sam_size; +} + +/** + * Prefetch the first SAMPLEBUFFER_MARGIN VADPCM frames of selected samples + * into RDRAM (attack cache). A note that starts at sample offset 0 must DMA + * those frames from ROM before it can play; when several such note-ons land + * on the same tick they queue on the PI bus. Keep a small head of the worst + * offenders resident so those note-ons hit RDRAM instead. + * + * Budget is 1/8 of the samplebuffer RAM (@p sam_size), spent on samples with + * the highest (note-on count × same-tick burst) score. Results go into + * sample_attack_by_hash. + */ +static void xm_select_attack_cache( + const std::map ¬eons_from0, + const std::map &cold_burst, + int sam_size) +{ + const int ATTACK_FRAMES = 128; // matches SAMPLEBUFFER_MARGIN_UNITS + int fbytes = flag_xm_compress_samples > 0 + ? vadpcm_frame_bytes(flag_wav_compress_vadpcm_bits) : 0; + if (!fbytes) + return; + + int attack_budget = sam_size / 8; + struct att_cand { int score; xm_sample_t *s; }; + std::vector cands; + for (auto const &kv : noteons_from0) + cands.push_back({ kv.second * cold_burst.at(kv.first), kv.first }); + std::sort(cands.begin(), cands.end(), [](att_cand a, att_cand b) { + return a.score > b.score; + }); + + int attack_n = 0, attack_bytes = 0; + int budget = attack_budget; + int cost = ATTACK_FRAMES * fbytes; + for (auto const &c : cands) { + if (budget < cost) break; + uint32_t hash = xm_sample_crc32(c.s); + if (sample_attack_by_hash[hash] >= ATTACK_FRAMES) continue; + sample_attack_by_hash[hash] = ATTACK_FRAMES; + budget -= cost; + attack_n++; + attack_bytes += cost; + } + if (flag_verbose) + fprintf(stderr, " * attack cache: %d/%d samples → %d KiB (budget %d KiB = 1/8 of %d KiB samplebufs)\n", + attack_n, (int)noteons_from0.size(), + (attack_bytes + 1023) / 1024, (attack_budget + 1023) / 1024, + sam_size / 1024); +} + +int xm_convert(const char *infn, const char *outfn) { + if (flag_verbose) + fprintf(stderr, "Converting: %s => %s\n", infn, outfn); + + // For xm64 conversions, deactivate huffman by default + if (flag_wav_compress_vadpcm_huffman < 0) + flag_wav_compress_vadpcm_huffman = 0; + + FILE *xm = fopen(infn, "rb"); + if (!xm) fatal("cannot open: %s\n", infn); + + fseek(xm, 0, SEEK_END); + int fsize = ftell(xm); + fseek(xm, 0, SEEK_SET); + + char *xmdata = (char*)malloc(fsize); + fread(xmdata, 1, fsize, xm); + + size_t mem_ctx, mem_pat, mem_sam; + xm_get_memory_needed_for_context(xmdata, fsize, &mem_ctx, &mem_pat, &mem_sam); + + // Load the XM into a XM context. The specified playback frequency is + // arbitrary, and it doesn't affect the calculations being done of the buffer + // sizes (as those depend on the instrument notes, not the output frequency). + xm_context_t* ctx; + xm_create_context_safe(&ctx, xmdata, fsize, 48000); + if (!ctx) fatal("cannot read XM file: invalid format?"); + free(xmdata); + + // Remove 0-length samples + xm_remove_empty_samples(ctx); + + // Pre-process all waveforms: + // 1) Ping-pong loops will be unrolled as regular forward + // 2) Repeat initial data after loop end for MIXER_LOOP_OVERREAD bytes + // to speed up decoding in RSP. + for (int i=0;imodule.num_instruments;i++) { + xm_instrument_t *ins = &ctx->module.instruments[i]; + + for (int j=0;jnum_samples;j++) { + xm_sample_t *s = &ins->samples[j]; + int bps = s->bits / 8; + + if (flag_xm_8bit && bps == 2) { + // Convert 16-bit samples to 8-bit + int8_t *data8 = (int8_t*)malloc(s->length); + for (int k=0;klength;k++) + data8[k] = s->data16[k] >> 8; + memcpy(s->data8, data8, s->length); + free(data8); + s->bits = 8; + bps = 1; + } + + uint32_t length = s->length * bps; + uint32_t loop_length = s->loop_length * bps; + uint32_t loop_end = s->loop_end * bps; + + uint8_t *sout, *out; + switch (s->loop_type) { + default: + fatal("invalid loop type: %d\n", s->loop_type); + case XM_NO_LOOP: + sout = (uint8_t*)malloc(length); + memcpy(sout, s->data8, length); + break; + case XM_FORWARD_LOOP: + // Special case for odd-sized loops of 8-bit samples. We cannot + // properly handle these at runtime because they cannot be DMA'd + // as they change the 2-byte phase between ROM and RAM. + // xm64.c will decrease the loop length by 1 byte to playback them, + // but this can affect the period in case of short loops: + // for instance, a 13-bytes loop shortened 12-bytes change the + // period by 7%, which can be several notes of difference at + // high frequencies. + // So for short loops (<1024 bytes), we just duplicate the loop + // itself to make it of even size. For longer loops, the period + // error made by xm64 when shortening is < 0.1%, which isn't + // audible. + if (bps == 1 && loop_length%2 == 1 && loop_length < XM64_SHORT_ODD_LOOP_LENGTH) { + sout = (uint8_t*)malloc(loop_end + loop_length); + length = loop_end+loop_length; + // Copy waveform until loop end + memcpy(sout, s->data8, loop_end); + // Duplicate loop + memmove(sout + loop_end, s->data8 + loop_end - loop_length, loop_length); + loop_end += loop_length; + loop_length *= 2; + } else { + sout = (uint8_t*)malloc(loop_end); + length = loop_end; + // Copy waveform until loop end + memcpy(sout, s->data8, loop_end); + } + break; + case XM_PING_PONG_LOOP: + length = loop_end + loop_length; + sout = (uint8_t*)malloc(length); + out = sout; + + memcpy(out, s->data8, loop_end); + out += loop_end; + for (int x=0;xdata8[(loop_end-x-1) ^ (bps>>1)]; + + loop_end += loop_length; + loop_length *= 2; + s->loop_type = XM_FORWARD_LOOP; + break; + } + + // If the sample length changed, update the memory + // required for the context. + if (length != s->length*bps) + { + #define ALIGN8(n) ((((n) + 7) >> 3) << 3) + ctx->ctx_size -= ALIGN8(s->length*bps); + ctx->ctx_size_all_samples -= ALIGN8(s->length*bps); + ctx->ctx_size += ALIGN8(length); + ctx->ctx_size_all_samples += ALIGN8(length); + } + s->length = length / bps; + s->loop_length = loop_length / bps; + s->loop_end = loop_end / bps; + s->data8 = (int8_t*)sout; + } + } + + // Analyze playback, then size streaming / pin / attack RAM. + ch_sizing_t ch_sz[32] = {}; + std::map ch_loop_freq[32]; + std::map sample_noteons_from0; + std::map sample_cold_burst; + + bool** used_samples = (bool**)calloc(ctx->module.num_instruments, sizeof(bool*)); + for (int i=0; imodule.num_instruments; i++) + if (ctx->module.instruments[i].num_samples > 0) + used_samples[i] = (bool*)calloc(ctx->module.instruments[i].num_samples, sizeof(bool)); + + sample_skip_points.clear(); + sample_attack_by_hash.clear(); + sample_resident_by_hash.clear(); + + xm_dry_run(ctx, ch_sz, ch_loop_freq, sample_noteons_from0, sample_cold_burst, used_samples); + + int res_bytes = 0; + int sam_size = xm_size_loop_buffers(ctx, ch_sz, ch_loop_freq, &res_bytes); + xm_select_attack_cache(sample_noteons_from0, sample_cold_burst, sam_size); + + // Free unused samples, to save ROM space. We only remove the last unused samples + // to avoid renumbering the samples, which would require updating all the pattern + // data. This is OK most of the times since 99% of XM files only has 1 sample per + // instrument anyway. + for (int i=0; imodule.num_instruments; i++) { + xm_instrument_t *ins = &ctx->module.instruments[i]; + while (ins->num_samples > 0 && !used_samples[i][ins->num_samples-1]) { + if (flag_verbose) fprintf(stderr, " * removing unused sample %d from instrument %d\n", ins->num_samples-1, i+1); + free(ins->samples[ins->num_samples-1].data8); + ins->samples[ins->num_samples-1].data8 = NULL; + memset(&ins->samples[ins->num_samples-1], 0, sizeof(xm_sample_t)); + ins->num_samples--; + } + free(used_samples[i]); + } + free(used_samples); + + FILE *out = fopen(outfn, "wb"); + if (!out) fatal("cannot create: %s", outfn); + placeholder_clear(); + xm_context_save(ctx, out, outfn); + int romsize = ftell(out); + fclose(out); + + // Dump some statistics for the conversion + if (flag_verbose) { + fprintf(stderr, " * ROM size: %u KiB (samples:%zu)\n", + romsize / 1024, mem_sam / 1024); + fprintf(stderr, " * RAM size: %zu KiB (ctx:%zu, patterns:%u, samples:%u, resident:%u)\n", + (mem_ctx+sam_size+res_bytes+ctx->ctx_size_stream_pattern_buf)/1024, + mem_ctx / 1024, + ctx->ctx_size_stream_pattern_buf / 1024, + sam_size / 1024, + res_bytes / 1024 + ); + fprintf(stderr, " * Samples RAM per channel (at %d queued samples): [", XM_REF_INFLIGHT_SAMPLES); + for (int i=0;imodule.num_channels;i++) { + if (i!=0) fprintf(stderr, ", "); + fprintf(stderr, "%d", xm_sizing_bytes(ch_sz[i], XM_REF_INFLIGHT_SAMPLES, XM_REF_OUTPUT_RATE)); + } + fprintf(stderr, "]\n"); + // Only the queued part follows the audio buffer chain of the game, so + // break the sizing down: it is what an app trades away by polling in + // smaller steps. + for (int i=0;imodule.num_channels;i++) + fprintf(stderr, " ch%-2d %6d B = %d fixed + %.2f B/sample x %d%s%s\n", + i, xm_sizing_bytes(ch_sz[i], XM_REF_INFLIGHT_SAMPLES, XM_REF_OUTPUT_RATE), + ch_sz[i].base, ch_sz[i].rate / XM_REF_OUTPUT_RATE, XM_REF_INFLIGHT_SAMPLES, + ch_sz[i].pin ? " [loop pin]" : "", + ch_sz[i].cap && ch_sz[i].base + ch_sz[i].rate * XM_REF_INFLIGHT_SAMPLES / XM_REF_OUTPUT_RATE > ch_sz[i].cap + ? " [capped by sample length]" : ""); + } + + return 0; +} diff --git a/tools/audioconv64/conv_ym64.c b/tools/audioconv64/conv_ym64.cpp similarity index 93% rename from tools/audioconv64/conv_ym64.c rename to tools/audioconv64/conv_ym64.cpp index 8969b6f4e..906dba5a8 100644 --- a/tools/audioconv64/conv_ym64.c +++ b/tools/audioconv64/conv_ym64.cpp @@ -22,18 +22,11 @@ * */ -#ifndef assertf -#define assertf(x, ...) assert(x) -#endif -#define memalign(a, b) malloc(b) - -#include "../../src/compress/lzh5_internal.h" // LZH5 decompression -#include "../../src/compress/lzh5.c" -#include "../../src/compress/ringbuf.c" -#include "../common/lzh5_compress.h" // LZH5 compression -#include "../common/lzh5_compress.c" -#include +#include +#include +#include "liblzh5.h" +#include "audioconv64.h" bool flag_ym_compress = false; @@ -49,7 +42,7 @@ typedef struct __attribute__((packed)) { uint8_t filename_len; } lhaheader; -_Static_assert(sizeof(lhaheader) == 22, "invalid lhaheader size"); +static_assert(sizeof(lhaheader) == 22, "invalid lhaheader size"); typedef struct __attribute__((packed)) { uint32_t nvbl; @@ -61,11 +54,11 @@ typedef struct __attribute__((packed)) { uint16_t sizeext; } ym5header; -_Static_assert(sizeof(ym5header) == 22, "invalid ym5header size"); +static_assert(sizeof(ym5header) == 22, "invalid ym5header size"); static FILE *ym_f; static bool ym_compressed; -static uint8_t alignas(8) ym_decoder[DECOMPRESS_LZH5_STATE_SIZE]; +static uint8_t __attribute__((aligned(8))) ym_decoder[DECOMPRESS_LZH5_STATE_SIZE + DECOMPRESS_LZH5_DEFAULT_WINDOW_SIZE]; static void ymread(void *buf, int sz) { if (ym_compressed) { @@ -187,14 +180,14 @@ int ym_convert(const char *infn, const char *outfn) { int nframes = csize / 14; // Read the whole file. If YM3b, read also the looping frame number. - uint8_t *data = malloc(csize); + uint8_t *data = (uint8_t*)malloc(csize); ymread(data, csize); if (head[3] == 'b') ymread(&loop, 4); fclose(ym_f); // De-interleave the data. Notice that YM3! stores data for 14 // registers, while YM5! has room for 16 registers (to handle digidrums). - uint8_t *outdata = malloc(csize/14*16); + uint8_t *outdata = (uint8_t*)malloc(csize/14*16); memset(outdata, 0, csize/14*16); for (int i=0;i + + This is a tool part of the Libdragon SDK. + + This is free and unencumbered software released into the public domain. + + For more information, please refer to +*/ +#include +#include +#include +#include +#include +#include + +#define HUFF_TABLE_SIZE 16 +#define HUFF_CONTEXTS 3 +#define HUFF_CONTEXT_LEN (HUFF_CONTEXTS * 24) + +// Huffman tree node +typedef struct HuffNode { + int symbol; + int frequency; + int order; // used to stable-sort of nodes with the same frequency (for determinism) + struct HuffNode *left, *right; +} HuffNode; + +typedef struct { + int value; + int length; +} HuffCode; + +typedef struct { + int freq[HUFF_CONTEXTS][HUFF_TABLE_SIZE]; +} HuffFreq; + +typedef struct { + HuffNode *root; + HuffCode codes[HUFF_TABLE_SIZE]; +} HuffCtx; + +int compare_freq(const void *a, const void *b) { + HuffNode *node_a = *(HuffNode**)a; + HuffNode *node_b = *(HuffNode**)b; + if (node_a->frequency != node_b->frequency) + return node_a->frequency - node_b->frequency; + return node_a->order - node_b->order; +} + +HuffNode* build_huffman_tree(int frequencies[], int size) { + HuffNode* heap[16]; + int hsize = 0; + int next_order = 0; + for (int i = 0; i < size; i++) { + if (frequencies[i] == 0) { + continue; + } + heap[hsize] = (HuffNode*)malloc(sizeof(HuffNode)); + heap[hsize]->symbol = i; + heap[hsize]->frequency = frequencies[i]; + heap[hsize]->order = next_order++; + heap[hsize]->left = heap[hsize]->right = NULL; + hsize++; + } + qsort(heap, hsize, sizeof(HuffNode*), compare_freq); + + while (hsize > 1) { + HuffNode* left = heap[0]; + HuffNode* right = heap[1]; + HuffNode* parent = (HuffNode*)malloc(sizeof(HuffNode)); + parent->symbol = -1; + parent->frequency = left->frequency + right->frequency; + parent->order = next_order++; + parent->left = left; + parent->right = right; + memmove(&heap[0], &heap[2], (hsize - 2) * sizeof(HuffNode*)); + heap[hsize - 2] = parent; + hsize--; + qsort(heap, hsize, sizeof(HuffNode*), compare_freq); + } + return heap[0]; +} + +void free_huffman_tree(HuffNode* root) { + if (root->left) free_huffman_tree(root->left); + if (root->right) free_huffman_tree(root->right); + free(root); +} + +void generate_huffman_codes(HuffNode* root, int code, int length, HuffCode codes[]) { + if (root->symbol != -1) { + codes[root->symbol].value = code; + codes[root->symbol].length = length; + } else { + generate_huffman_codes(root->left, (code << 1) | 0, length + 1, codes); + generate_huffman_codes(root->right, (code << 1) | 1, length + 1, codes); + } +} + +void calculate_frequencies(uint8_t *input_data, int data_len, HuffFreq *freq) { + assert(data_len % 9 == 0); + + memset(freq, 0, sizeof(HuffFreq)); + while (data_len > 0) { + for (int i = 0; i < 18; i+=2) { + int sym0 = input_data[i/2] >> 4; + int sym1 = input_data[i/2] & 15; + int c0 = (i+0 < 2) ? i+0 : 2; + int c1 = (i+1 < 2) ? i+1 : 2; + freq->freq[c0][sym0]++; + freq->freq[c1][sym1]++; + } + input_data += 9; + data_len -= 9; + } + + // Now normalize the frequencies so that they sum to 1 + for (int i = 0; i < HUFF_CONTEXTS; i++) { + int sum = 0; + for (int j = 0; j < HUFF_TABLE_SIZE; j++) { + sum += freq->freq[i][j]; + } + assert(sum > 0); + if (sum == 0) { + continue; + } + for (int j = 0; j < HUFF_TABLE_SIZE; j++) { + int nfreq = ((int64_t)freq->freq[i][j] * 255 + sum - 1) / sum; + assert(nfreq >= 0); + if (nfreq == 0) assert(freq->freq[i][j] == 0); + freq->freq[i][j] = nfreq; + } + } +} + +int huffv_compress(uint8_t *input_data, int data_len, uint8_t *output, int output_len, uint8_t *outctx, int outctxlen) { + assert(data_len > 0); + + HuffFreq freq; + calculate_frequencies(input_data, data_len, &freq); + + HuffCtx ctx[HUFF_CONTEXTS] = {{0}}; + for (int i = 0; i < HUFF_CONTEXTS; i++) { + while (1) { + // Initialize codes with invalid length so we can detect if we can't encode this context + for (int j = 0; j < HUFF_TABLE_SIZE; j++) { + ctx[i].codes[j].length = -1; + } + + ctx[i].root = build_huffman_tree(freq.freq[i], HUFF_TABLE_SIZE); + generate_huffman_codes(ctx[i].root, 0, 0, ctx[i].codes); + + // Check if all codes are 8 bits or less, otherwise we can't encode this context + // We limit the maximum code length to 8 bits so that the decompressor + // can use a compact table to decode. + bool valid = true; + for (int j = 0; j < HUFF_TABLE_SIZE; j++) { + if (ctx[i].codes[j].length > 8) { + valid = false; + break; + } + } + if (valid) break; + free_huffman_tree(ctx[i].root); + + // Scale the frequencies and retry. This is what bzip2 does. It's not + // the best solution but it's simple and works. + for (int j = 0; j < HUFF_TABLE_SIZE; j++) { + if (freq.freq[i][j] > 1) + freq.freq[i][j] /= 2; + } + } + } + + int written = 0; + + // Serialize the compression contexts + for (int i = 0; i < HUFF_CONTEXTS; i++) { + // Write lengths as 4-bit + for (int j=0; j<16; j+=2) { + int l = 0; + assert(ctx[i].codes[j+0].length < 16); + assert(ctx[i].codes[j+1].length < 16); + // Negative lengths (unused symbols) are encoded + // as 0xF. Notice that 0 length is a valid length + // (only one symbol in the context). + int l0 = ctx[i].codes[j+0].length < 0 ? 0xF : ctx[i].codes[j+0].length; + int l1 = ctx[i].codes[j+1].length < 0 ? 0xF : ctx[i].codes[j+1].length; + l |= l0 << 4; + l |= l1 << 0; + assert(outctxlen-- > 0); + *outctx++ = l; + } + + // Write values as 8 bits + for (int j=0; j<16; j++) { + assert(ctx[i].codes[j].value < 256); + assert(outctxlen-- > 0); + *outctx++ = ctx[i].codes[j].value; + } + } + assert(outctxlen == 0); + + uint32_t buffer = 0; + int bit_pos = 0; + + assert(data_len % 9 == 0); + while (data_len > 0) { + for (int i = 0; i < 18; i += 2) { + int sym0 = input_data[i/2] >> 4; + int sym1 = input_data[i/2] & 15; + int c0 = (i+0 < 2) ? i+0 : 2; + int c1 = (i+1 < 2) ? i+1 : 2; + + // 0-lengths happen when a context only has a single symbol, so + // there's no need to encode it. + assert(ctx[c0].codes[sym0].length >= 0); + assert(ctx[c1].codes[sym1].length >= 0); + + buffer = (buffer << ctx[c0].codes[sym0].length) | ctx[c0].codes[sym0].value; + bit_pos += ctx[c0].codes[sym0].length; + buffer = (buffer << ctx[c1].codes[sym1].length) | ctx[c1].codes[sym1].value; + bit_pos += ctx[c1].codes[sym1].length; + + while (bit_pos >= 8) { + assert(written < output_len); + output[written++] = buffer >> (bit_pos - 8); + bit_pos -= 8; + } + } + + input_data += 9; + data_len -= 9; + } + + if (bit_pos > 0) { + assert(written < output_len); + assert(bit_pos < 8); + output[written++] = buffer << (8 - bit_pos); + } + + for (int i=0; ilengths[sym/2] >> (4 * (~sym&1)) & 0xF; + // The value 0xF indicates an unused symbol; skip it. + if (len == 0xF) + continue; + assert(len <= 8); + // Ensure that the code value fits in the specified number of bits. + // (i.e., it must be less than 1 << len) + assert(sctx->values[sym] < (1 << len)); + + // Calculate the number of bits to shift so that the code is left-aligned in 8 bits. + int shift = 8 - len; + int code = sctx->values[sym] << shift; + // Prepare the value to store in the lookup table: + // - high nibble: the symbol (0..15) + // - low nibble: the code length in bits (1..8) + uint8_t table_value = (sym << 4) | (len & 0xF); + + // Fill every table entry that starts with the computed code. + // There will be 1 << (8 - len) possible 8-bit values with this prefix. + for (int k = 0; k < (1 << shift); k++) { + assert(lookup[i].codes[code + k] == 0); // ensure each entry is written only once. + lookup[i].codes[code + k] = table_value; + } + } + // Verify that all 256 entries have been filled. + for (int j = 0; j < 256; j++) { + assert(lookup[i].codes[j] != 0); + } + } +} + +//===================================================================== +// Function: huffv_decompress +// Description: +// Decompresses data using the provided lookup tables. +// It reads symbols from the compressed bit stream using the lookup tables, +// reconstructs the original nibble stream (each symbol is 4 bits), and then +// combines every two nibbles into one output byte. +// This function returns the number of bits consumed from the input buffer. +// Parameters: +// - compressed: pointer to the compressed data buffer (bit stream) +// - compressed_len: length (in bytes) of the compressed buffer +// - lookup: an array of 3 prebuilt HuffLookup tables (one per context) +// - output: pointer to the buffer where decompressed data is written +// - output_len: expected size (in bytes) of the decompressed data; must be a multiple of 9, +// as each original block consists of 9 bytes (18 4-bit symbols) +// - bitpos_stats: if not NULL, it must point to an array of (output_len/9 + 1) ints. +// The function will write the current bit position (bits consumed) at each block boundary: +// bitpos_stats[0] = 0 +// bitpos_stats[k] = bitpos after decoding k blocks (k in 1..output_len/9) +// Returns: +// The number of bits used/consumed from the compressed input buffer. +// This value may be less than compressed_len * 8 if the output buffer is too short +// to contain all the decompressed data. Also the last byte might always +// be only partially consumed. +//===================================================================== +int huffv_decompress(uint8_t *compressed, int compressed_len, + const HuffLookup lookup[HUFF_CONTEXTS], + uint8_t *output, int output_len, + int *bitpos_stats) +{ + // The output length must be a multiple of 9 bytes (each block decodes into 9 bytes). + assert(output_len % 9 == 0); + int num_blocks = output_len / 9; + int out_index = 0; + + // Bit stream reading state. + uint32_t bit_buffer = 0; + int bit_count = 0; + int comp_index = 0; // Current index in the compressed input buffer + + if (bitpos_stats) { + // At 0 decoded blocks, 0 bits have been consumed. + bitpos_stats[0] = 0; + } + + // Process each block. + for (int b = 0; b < num_blocks; b++) { + uint8_t nibbles[18]; // Temporary array to store 18 decoded 4-bit symbols. + + // Each block consists of 18 symbols: + // - The first symbol uses context 0, + // - The second uses context 1, + // - The remaining 16 symbols use context 2. + for (int n = 0; n < 18; n++) { + int context = (n < 2) ? n : 2; + + // Ensure there are at least 8 bits available in the buffer. + while (bit_count < 8) { + bit_buffer <<= 8; + if (comp_index < compressed_len) + bit_buffer |= compressed[comp_index++]; + else + comp_index++; // just for statistics + bit_count += 8; + } + // Extract the top 8 bits (without removing them yet) to use as index. + uint8_t index = (bit_buffer >> (bit_count - 8)) & 0xFF; + // Look up the corresponding symbol and the code length. + uint8_t code_val = lookup[context].codes[index]; + int symbol = code_val >> 4; + int code_len = code_val & 0x0F; + assert(code_len <= 8); + nibbles[n] = (uint8_t)symbol; + // Remove the used bits from the bit buffer. + bit_count -= code_len; + } + // Combine every two nibbles into one byte (the first nibble is the high half, + // the second nibble is the low half) to reconstruct the original 9 bytes. + for (int i = 0; i < 9; i++) { + uint8_t byte = (nibbles[2 * i] << 4) | (nibbles[2 * i + 1] & 0x0F); + output[out_index++] = byte; + } + + if (bitpos_stats) { + // bits consumed so far = bits read from input - bits still in the internal buffer + bitpos_stats[b + 1] = comp_index * 8 - bit_count; + } + } + // Calculate the total number of bits consumed. + // This is computed as the total number of bits read from the input buffer: + // (comp_index * 8) minus the number of bits remaining in bit_buffer. + int bits_consumed = comp_index * 8 - bit_count; + return bits_consumed; +} diff --git a/tools/audioconv64/liblzh5.c b/tools/audioconv64/liblzh5.c new file mode 100644 index 000000000..19b9a4441 --- /dev/null +++ b/tools/audioconv64/liblzh5.c @@ -0,0 +1,9 @@ +#include +#define assertf(x, ...) assert(x) +#ifndef assertf +#define assertf(x, ...) assert(x) +#endif +#define memalign(a, b) malloc(b) + +#include "../../src/compress/lzh5.c" +#include "lzh5_compress.c" diff --git a/tools/audioconv64/liblzh5.h b/tools/audioconv64/liblzh5.h new file mode 100644 index 000000000..8c9d66117 --- /dev/null +++ b/tools/audioconv64/liblzh5.h @@ -0,0 +1,10 @@ +#ifdef __cplusplus +extern "C" { +#endif + +#include "../../src/compress/lzh5_internal.h" // LZH5 decompression +#include "lzh5_compress.h" // LZH5 compression + +#ifdef __cplusplus +} +#endif diff --git a/tools/audioconv64/libopus.c b/tools/audioconv64/libopus.c index 40f20752e..cdd1e6776 100644 --- a/tools/audioconv64/libopus.c +++ b/tools/audioconv64/libopus.c @@ -1,42 +1 @@ - -#pragma GCC diagnostic push - -// Compiling libopus seems to trigger these warnings that we can safely ignore. -#pragma GCC diagnostic ignored "-Wtautological-pointer-compare" -#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" - -// Activate support for CELT custom modes. We are going to always use the -// custom mode API even for standard modes (like 48 Khz), so we can safely -// define CUSTOM_MODES_ONLY. -#define CUSTOM_MODES -#define CUSTOM_MODES_ONLY - -// Some config macros, since we don't run configure -#define USE_ALLOCA -#define HAVE_LRINTF -#define HAVE_LRINT - -// Bring opus in -#include "opus/opus_custom.h" -#include "opus/celt_encoder.c" -#include "opus/celt_decoder.c" -#include "opus/bands.c" -#include "opus/celt.c" -#include "opus/cwrs.c" -#include "opus/celt_lpc.c" -#include "opus/laplace.c" -#include "opus/entcode.c" -#include "opus/entenc.c" -#include "opus/entdec.c" -#include "opus/pitch.c" -#include "opus/rate.c" -#include "opus/mathops.c" -#include "opus/modes.c" -#include "opus/mdct.c" -#include "opus/kiss_fft.c" -#include "opus/vq.c" -#include "opus/quant_bands.c" - -#pragma GCC diagnostic pop -#undef MIN -#undef MAX +#include "../../src/audio/libopus.c" diff --git a/tools/audioconv64/libopus.h b/tools/audioconv64/libopus.h new file mode 100644 index 000000000..0eac367d9 --- /dev/null +++ b/tools/audioconv64/libopus.h @@ -0,0 +1 @@ +#include "../../src/audio/opus/opus_custom.h" diff --git a/tools/audioconv64/libsamplerate.c b/tools/audioconv64/libsamplerate.c new file mode 100644 index 000000000..c5c5d679b --- /dev/null +++ b/tools/audioconv64/libsamplerate.c @@ -0,0 +1,12 @@ +#ifndef __MINGW32__ +// Enable usage of visibility attributes for GCC and Clang, except when using MinGW +#define HAVE_VISIBILITY +#endif + +#include "libsamplerate.h" + +#include "libsamplerate/samplerate.c" +#include "libsamplerate/src_sinc.c" +#include "libsamplerate/src_zoh.c" +#include "libsamplerate/src_linear.c" + diff --git a/tools/audioconv64/libsamplerate.h b/tools/audioconv64/libsamplerate.h new file mode 100644 index 000000000..8637a341b --- /dev/null +++ b/tools/audioconv64/libsamplerate.h @@ -0,0 +1,7 @@ +#include + +#define ENABLE_SINC_BEST_CONVERTER +#define ENABLE_SINC_MEDIUM_CONVERTER +#define PACKAGE "libsamplerate" +#define VERSION "0.1.9" +#include "libsamplerate/samplerate.h" diff --git a/tools/audioconv64/libulc.c b/tools/audioconv64/libulc.c new file mode 100644 index 000000000..e5a4ae136 --- /dev/null +++ b/tools/audioconv64/libulc.c @@ -0,0 +1,22 @@ +#ifndef __MINGW32__ +// Enable usage of visibility attributes for GCC and Clang, except when using MinGW +#define HAVE_VISIBILITY +#endif + +#include "libulc.h" + +#include "libulc/libfourier/Fourier_ApplyAmplitude.c" +#include "libulc/libfourier/Fourier_ApplyWindow.c" +#include "libulc/libfourier/Fourier_CenterFFT.c" +#include "libulc/libfourier/Fourier_DCT2.c" +#include "libulc/libfourier/Fourier_DCT4.c" +#include "libulc/libfourier/Fourier_IMDCT.c" +#include "libulc/libfourier/Fourier_MDCT.c" + +#include "libulc/ulcDecoder.c" +#include "libulc/ulcEncoder.c" +#include "libulc/ulcEncoder_BlockTransform.c" +#include "libulc/ulcEncoder_Encode.c" +#include "libulc/ulcEncoder_NoiseFill.c" +#include "libulc/ulcEncoder_Psyopt.c" +#include "libulc/ulcEncoder_WindowControl.c" diff --git a/tools/audioconv64/libulc.h b/tools/audioconv64/libulc.h new file mode 100644 index 000000000..a7fc5c8de --- /dev/null +++ b/tools/audioconv64/libulc.h @@ -0,0 +1,10 @@ +#ifdef __cplusplus +extern "C" { +#endif + +#include "libulc/ulcEncoder.h" +#include "libulc/ulcDecoder.h" + +#ifdef __cplusplus +} +#endif diff --git a/tools/audioconv64/libulc/FormatSpecs.md b/tools/audioconv64/libulc/FormatSpecs.md new file mode 100644 index 000000000..f91e49324 --- /dev/null +++ b/tools/audioconv64/libulc/FormatSpecs.md @@ -0,0 +1,157 @@ +### Overview +*** + +Each block is coded sequentially, with each channel also coded sequentially, resulting in the following structure: + + Block { + Header + Chan0 { + SubBlock0 + SubBlock1 + SubBlock2 + ... + } + Chan1 { + SubBlock0 + SubBlock1 + SubBlock2 + ... + } + } + +The encoding tools align each block (that is, including all channels) to byte boundaries, but this is not strictly necessary; any alignment will do. + +These blocks code MDCT coefficients that have been pre-normalized. So to decode, one must simply apply an unnormalized IMDCT to return the decoded PCM stream. + +Each block always starts with a window control code before any coefficients are coded (this is considered the header). This controls the window lengths and shapes, and is explained in the `Block header` section. + +NB: The encoder is expected to handle all scaling, such that the inverse transform needs no scaling whatsoever (not even MDCT normalization). The result is that all coefficients are in the range ```|x| <= 4/Pi``` (where 4/Pi is the p=1 limit of the MDCT matrix as N approaches infinity). + +### Block header +*** + +Each block starts with a nybble that encodes overlap scaling, with the high bit being a decimation (window switch) toggle. + +| Nybble sequence | Window 0 | Window 1 | Window 2 | Window 3 | +| ------------------ | -------- | -------- | -------- | -------- | +| ```0h..7h``` | N* | - | - | - | +| ```8h..Fh,0010b``` | N/2* | N/2 | - | - | +| ```8h..Fh,0011b``` | N/2 | N/2* | - | - | +| ```8h..Fh,0100b``` | N/4* | N/4 | N/2 | - | +| ```8h..Fh,0101b``` | N/4 | N/4* | N/2 | - | +| ```8h..Fh,0110b``` | N/2 | N/4* | N/4 | - | +| ```8h..Fh,0111b``` | N/2 | N/4 | N/4* | - | +| ```8h..Fh,1000b``` | N/8* | N/8 | N/4 | N/2 | +| ```8h..Fh,1001b``` | N/8 | N/8* | N/4 | N/2 | +| ```8h..Fh,1010b``` | N/4 | N/8* | N/8 | N/2 | +| ```8h..Fh,1011b``` | N/4 | N/8 | N/8* | N/2 | +| ```8h..Fh,1100b``` | N/2 | N/8* | N/8 | N/4 | +| ```8h..Fh,1101b``` | N/2 | N/8 | N/8* | N/4 | +| ```8h..Fh,1110b``` | N/2 | N/4 | N/8* | N/8 | +| ```8h..Fh,1111b``` | N/2 | N/4 | N/8 | N/8* | + +Overlap scaling (bit0..2 of the first nybble) is applied to the \[sub]block denoted with an asterisk, and results in an overlap of ```50% * 2^-Scale``` (that is, ```SubBlockSize * 2^-Scale``` samples of overlap) for that \[sub]block blending with the last. + +Note that the transient subblock index can be easily obtained using a population count (POPCNT) on bit1..3 of the second nybble (minus 1 for 0-based indexing). + +### Block syntax +*** + +| Nybble sequence | Explanation | Effect | +| ----------------------- | ------------------- | ----------------------------------------------- | +| ```-7h..-2h,+2h..+7h``` | Normal coefficient | Insert ```Coef[n++] = Nybble*Abs[Nybble] * Quantizer``` | +| ```0h,0h..Fh``` | Zeros run (short) | Insert zeros | +| ```1h,Yh,Xh``` | Zeros run (long) | Insert zeros | +| ```8h,Zh,Yh,Xh``` | Noise-fill | Insert noise | +| ```Fh,0h..Dh``` | Quantizer change | Set ```Quantizer = 2^-(5+X)``` | +| ```Fh,Eh,0h..Ch``` | Quantizer change | Set ```Quantizer = 2^-(5+14+X)``` | +| ```Fh,Eh,Dh``` | *Unallocated* | N/A | +| ```Fh,Eh,Eh``` | *Unallocated* | N/A | +| ```Fh,Eh,Fh``` | Stop | Stop reading coefficients; fill rest with zeros | +| ```Fh,Fh,Zh,Yh,Xh``` | Stop (noise) | Stop reading coefficients; fill rest with noise | + +#### ```-7h..-2h, +2h..+7h```: Normal coefficient + +This is a quantized coefficient. To unpack, take the signed squared value (ie. ```x*ABS(x)```), scale by the current quantizer, and move to the next coefficient. + +If we've received as many coefficients as there are in a transform block (eg. 2048 coefficients), this channel's coefficients are finished, and we can move on to inverse transforming. + +Note that this range excludes the coefficients +/-1. This is by design, as in testing, it was found that this coefficient level was generally used less than 0.05% of the time, no matter the bitrate. + +#### ```0h,0h..Fh```: Zeros run (short) + +This inserts coefficients with a value of 0. As these are fairly common after quantization (especially without noise fill), we use a compact format to code runs of them. + +To unpack: ```n = 1 + [second nybble]```. This allows 1..16 zeros to be coded with two nybbles. + +#### ```1h,Yh,Xh```: Zeros run (long) + +Further refinement on short zeros runs. This in particular is more common at lower bitrates, where quantization becomes too aggressive and/or we must cull large swathes of frequency bands in order to meet the target bitrate. + +Because a short run can already code up to 32 zeros at once in the 4 nybbles this command uses, a long run codes 33..288 zeros at once. + +To unpack: ```n = (Y<<4 | X) + 33``` + +#### ```8h,Zh,Yh,Xh```: Noise-fill + +Because removing a larger number of coefficients can result in holes in the spectrum, this command allows filling the holes with noise data. + +To unpack: ```n = (Z<<5 | Y<<1 | (X&1)) + 16``` and ```Level = ((X>>1) + 1)^2 * Quantizer/4```. Then store ```n``` coefficients generated by randomly cycling +/-```Level```. + +#### ```Fh,0h..Dh```, ```Fh,Eh,0h..Ch```: Quantizer change + +This modifies the current quantizer. + +When the third nybble is between ```0h..Dh```, this signals a normal quantizer change; at average audio levels, these are the most common quantization scalers. + +```Quantizer = 2^-(5 + [third nybble])``` + +(Note the bias of 5) + +When the nybble is ```Eh```, however, this signals that the quantizer needs to be smaller still, and so is followed by an extra nybble to afford a so-called 'extended-precision quantizer', allowing a maximum quantization scale of 2^-31. + +```Quantizer = 2^-(5 + 14 + [fourth nybble])``` + +This can be considered to result in a scientific notation containing a sign bit, 3 bits of significand, and 5 bits of exponent, with the minimum possible coefficient value being ±1.0×2^-31. + +Each channel begins with a nybble specifying the initial quantizer, akin to a silent ```Fh``` at the start (extended-precision quantizers are also allowed for this first quantizer band). + +#### ```Fh,Eh,Fh```, ```Fh,Fh,Zh,Yh,Xh```: Stop + +```Fh,Eh,Fh``` signals that the remaining coefficients for this channel are all zeros. This completes this channel's data. + +```Fh,Fh,Zh,Yh,Xh``` signals that the remaining coefficients should be filled with exponentially-decaying noise. + +A channel's block may begin with ```[Fh,]Eh,Fh```; this means that the channel is silent. + +A channel's block cannot begin with ```[Fh],Fh,Zh,Yh,Xh```; no quantizer has been set at this point, rendering the expression meaningless. + +##### Tail-end noise-fill + +Unpack Amplitude and Decay as follows: + + Amplitude = (Z+1)^2 * Quantizer/16 + Decay = 1 - 2^-19*(Y<<4 | X)^2 + +For each of the remaining coefficients in the block, noise is used in place of encoded coefficients as follows: + + Coef[n++] = Amplitude * [randomly generated +/-1] + Amplitude = Amplitude * Decay + +The quantizer is scaled by 1/16, as this was found to give consistently good results with very minimal overload/saturation and underload/collapse. + +### Inverse transform process +*** + +Inverse transform takes the above-decoded coefficients and applies the IMDCT to give the output stream. + +#### IMDCT + +The IMDCT used in this codec follows the normal IMDCT formula found on any maths book, but without any scaling factor in front (as it is moved to the MDCT in the encoder) and with a shifted basis that results in phase inversion (as this results in programming optimizations), ie. + + y[n] = Sum[X[k]*Cos[(n+1/2 + N/2 + N*2)(k+1/2)*Pi/N], {k,0,N-1}] + = -Sum[X[k]*Cos[(n+1/2 + N/2 )(k+1/2)*Pi/N], {k,0,N-1}] + +The windowing function used is a sine window, with the overlap amount based on the block size scaled by the nybble at the start of the block (see the `Overview` section). To use a different window, the MDCT and IMDCT functions of the source code must be modified to accomodate such (this is not too difficult, and only involves loading the sine/cosine values with appropriate data). Note that using a sine window allows reuse of the DCT coefficients table, whereas a different window cannot reuse these coefficients and so needs double the storage space. + +When using window switching, it is important to note that a subblock's overlap may be larger than allowed by the last subblock. When this happens, the number of overlap samples must be clipped to the size of the previous, smaller subblock. This unfortunately results in an additional block delay for decoding (on top of the MDCT delay), as the encoder must have knowledge about the next \[sub]block to account for this. diff --git a/tools/audioconv64/libulc/LICENSE b/tools/audioconv64/libulc/LICENSE new file mode 100644 index 000000000..fdddb29aa --- /dev/null +++ b/tools/audioconv64/libulc/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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 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. + +For more information, please refer to diff --git a/tools/audioconv64/libulc/README.md b/tools/audioconv64/libulc/README.md new file mode 100644 index 000000000..49e2a1400 --- /dev/null +++ b/tools/audioconv64/libulc/README.md @@ -0,0 +1,64 @@ +# ulc-codec +ulc-codec (Ultra-Low-Complexity Codec) is intended to be a low-bitrate MDCT audio codec, providing ultra-low-complexity decoding. + +## Getting started + +### Prerequisites +None (so far). Perhaps GCC if building from source. + +### Installing +Run ```make all``` to build the file-based encoding and decoding tools (```ulcencode``` and ```ulcdecode```). + +You could also ```make encodetool``` or ```make decodetool```. + +## Usage +The encoding/decoding tools work with WAV files for simplicity, and to avoid external dependencies. + +Additionally, the core encoding/decoding routines can theoretically work with any data they are fed, allowing for easier integration with non-file-based blocks of audio in the future. + +### Encoding +```ulcencodetool Input.wav Output.ulc RateKbps[,AvgComplexity]|-Quality [-blocksize:2048]``` + +This will take ```Input.wav``` and encode it into the output file ```Output.ulc```, at a coding rate of ```RateKbps``` (with ```AvgComplexity``` being passed, this uses ABR mode); alternatively, passing a negative value between -1 and -100 will encode in VBR mode (```-1``` corresponds to Quality=1, ```-100``` corresponds to Quality=100). ```-blocksize:X``` sets the size of each block (ie. the number of coefficients per block). The input file must be 8-bit, 16-bit, 24-bit, or 32-bit float. + +Encoding in any mode will display the actual average bitrate, maximum bitrate, and an 'average complexity' parameter. The latter doesn't have much real meaning (perhaps 'how difficult the file is to encode', or 'Quality parameter needed to achieve full transparency'), but can be passed to the encoder in ABR mode to achieve a desired average bitrate. + +### Decoding +```ulcdecodetool Input.ulc Output.wav [-format:PCM16]``` + +This will take ```Input.ulc``` and output ```Output.wav``` in the specified format. Accepted values are PCM8, PCM16, PCM24, and FLOAT32. + +## Possible issues +* Syntax is flexible enough to cause buffer overflows. +* No block synchronization (if an encoded file is damaged, there is no way to detect where the next block lies) +* The psychoacoustic model used is somewhat bare-bones, so as to avoid extra complexity and memory usage. As an example, blocks are processed with no memory of prior blocks, which could cause some inefficiency in coding (such as not taking advantage of temporal masking effects). However, it does appear to work very well for what it *does* do. +* Noise fill can leak on transients that are followed by a sharp drop in amplitude. + * Because noise-fill is not coupled to the L/R signal, noise will leak to both channels when used. +* Encode/decode tools compile with SSE+SSE2/AVX+AVX2/FMA enabled by default. If the encoder crashes/doesn't work, change these flags in the ```Makefile```. +* The codec is VBR in the way it operates; CBR and ABR are faked by adjusting quality until reaching the desired bitrate, reducing encoding speed. +* Transient detection/window selection is an ongoing area of research, especially as it also affects noise fill. + +## Technical details +* Target bitrate: 32..256kbps+ (44.1kHz, M/S stereo) + * CBR, ABR, and VBR (Quality = 1..100) modes available + * No hard limits on playback rate or coding bitrate +* MDCT-based encoding (using sine window) + * Window switching is combined with so-called 'overlap scaling', the latter of which varies the size of the overlap segment of transient \[sub]blocks. The idea is to center the transient within a window transition region, at which point overlap scaling takes over to clamp down on its leakage without having to switch to use small windows for the entire block, overall resulting in improved quality compared to the more-common '1 long block or N short blocks' strategy. +* Non-linear coefficient quantization for greater control over dynamic range +* Noise-fill mode for coefficients that aren't directly coded (similar to PNS) +* Extremely simple nybble-based syntax (no entropy-code lookups needed) + +## Authors +* **Ruben Nunez** - *Initial work* - [Aikku93](https://github.com/Aikku93) + +## License +ulc-codec is released under the Unlicense terms - do whatever you want with it. + +## Acknowledgements +* Huge thanks to Dennis K (`DekuTree64`) and `musicalman` for listening to (and for playing programming-rubber-ducky to) all my rants and thought processes as I worked through understanding audio codec design, as well as testing out experimental builds and offering immensely helpful critiques. Without them, this codec would never have gotten as far as it has. + +## Gameboy Advance and Nintendo DS player + +As a proof of concept of the decoding complexity, a Gameboy Advance demonstration may be found in the [ulcplayer-gba](https://github.com/Aikku93/ulcplayer-gba) repository. + +While not as challenging as the GBA version, [there is also a Nintendo DS version](https://github.com/Aikku93/ulcplayer-nds). diff --git a/tools/audioconv64/libulc/libfourier/Fourier.h b/tools/audioconv64/libulc/libfourier/Fourier.h new file mode 100644 index 000000000..6f822d95c --- /dev/null +++ b/tools/audioconv64/libulc/libfourier/Fourier.h @@ -0,0 +1,114 @@ +/************************************************/ +#pragma once +/************************************************/ +#ifdef __cplusplus +extern "C" { +#endif +/************************************************/ + +// x86 compilation options +// #define FOURIER_ALLOW_SSE +// #define FOURIER_ALLOW_FMA +// #define FOURIER_ALLOW_AVX + +/************************************************/ + +// DCT-II/DCT-IV (scaled) +// Arguments: +// Buf[N] +// Tmp[N] +// Implemented transforms (matrix form): +// MtxDCT2 = Table[Cos[(n-1/2)(k-1 )Pi/N], {k,N}, {n,N}] +// MtxDCT4 = Table[Cos[(n-1/2)(k-1/2)Pi/N], {k,N}, {n,N}] +// Implementations from: +// "Signal Processing based on Stable radix-2 DCT I-IV Algorithms having Orthogonal Factors" +// DOI: 10.13001/1081-3810.3207 +// Notes: +// -N must be a power of two, and >= 8. +void Fourier_DCT2(float *Buf, float *Tmp, int N); +void Fourier_DCT4(float *Buf, float *Tmp, int N); + +// Centered FFT/iFFT (scaled) +// Arguments: +// Buf[N] +// Tmp[N] +// FFT outputs N/2 complex lines (packed as {Re,Im}). +// Centered DFT derivation: +// "The Centered Discrete Fourier Transform and a Parallel Implementation of the FFT" +// DOI: 10.1109/ICASSP.2011.5946834 +// Notes: +// -N must be a power of two, and >= 16. +void Fourier_FFTReCenter (float *Buf, float *Tmp, int N); // Buf[N], Tmp[N] +void Fourier_iFFTReCenter(float *Buf, float *Tmp, int N); // Buf[N], Tmp[N] + +// MDCT+MDST (based on DCT-IV; scaled) +// Arguments: +// MDCT[N] +// MDST[N] +// New[N] +// Lap[N] +// BufTmp[N] +// Implemented transforms (matrix form): +// MtxMDCT = Table[Cos[(n-1/2 - N/2 + N*2)(k-1/2)Pi/N], {k, N}, {n,2N}] +// MtxMDST = Table[Sin[(n-1/2 + N/2 + N*2)(k-1/2)Pi/N], {k, N}, {n,2N}] +// Notes: +// -N must be a power of two, and >= 16. +// -Overlap must be a power of two, and >= 16 or 0. +// -Shifted basis (note the signs in the matrices). +// -Sine window (modulated lapped transform) is +// always used for lapping. +// -New can be the same as BufTmp. However, this +// implies trashing of the buffer contents. +void Fourier_MDCT_MDST(float *MDCT, float *MDST, const float *New, float *Lap, float *BufTmp, int N, int Overlap); + +// IMDCT (based on DCT-IV; scaled) +// Arguments: +// BufOut[N] +// BufIn[N] +// BufLap[N/2] +// BufTmp[N] +// Implemented transforms (matrix form): +// mtxIMDCT = Table[Cos[(n-1/2 - N/2 + N*2)(k-1/2)Pi/N], {k,2N}, {n, N}] +// Notes: +// -N must be a power of two, and >= 16. +// -Overlap must be a power of two, and >= 16 or 0. +// -BufOut must not be the same as BufIn. +// -Shifted basis (note the sign in the matrix). +// This re-inverts (ie. removes) the phase shift +// in the MDCT implementation from above. +// -BufIn can be the same as BufTmp. However, this +// implies trashing of the buffer contents. +void Fourier_IMDCT(float *BufOut, const float *BufIn, float *BufLap, float *BufTmp, int N, int Overlap); + +/************************************************/ + +// Apply amplitude to target +// Arguments: +// Dst[N*2] (Re/Im pairs) +// Src[N*2] (Re/Im pairs) +// Amplitude[N] +// Notes: +// -N must be a multiple of 16. +void Fourier_ApplyAmplitude(float *Dst, const float *Src, const float *Amplitude, float DryLevel, int N); + +// Apply window to target +// Arguments: +// Dst[N] +// Src[N] +// Window[N] (or Window[N/2] for symmetric windows) +// Notes: +// -N must be a multiple of 8 (or 16 for symmetric windows). +// -When applying overlapping windows, Dst[] is used as +// the overlap source AND target, as an accumulator. +void Fourier_ApplyWindow (float *Dst, const float *Src, const float *Window, int N); +void Fourier_OverlapWindow (float *Dst, const float *Src, const float *Window, int N); +void Fourier_ApplySymmetricWindow (float *Dst, const float *Src, const float *Window, int N); +void Fourier_OverlapSymmetricWindow(float *Dst, const float *Src, const float *Window, int N); + +/************************************************/ +#ifdef __cplusplus +} +#endif +/************************************************/ +// EOF +/************************************************/ diff --git a/tools/audioconv64/libulc/libfourier/Fourier_ApplyAmplitude.c b/tools/audioconv64/libulc/libfourier/Fourier_ApplyAmplitude.c new file mode 100644 index 000000000..dc3ad008e --- /dev/null +++ b/tools/audioconv64/libulc/libfourier/Fourier_ApplyAmplitude.c @@ -0,0 +1,78 @@ +/************************************************/ +#include "Fourier.h" +#include "Fourier_Helper.h" +/************************************************/ + +#define FUNC_ARGS_DECL (float *Dst, const float *Src, const float *Amplitude, float DryLevel, int N) +#define FUNC_ARGS (Dst, Src, Amplitude, DryLevel, N) + +/************************************************/ + +// Declare specialized routines +void Fourier_ApplyAmplitude_Generic FUNC_ARGS_DECL; +#ifdef FOURIER_IS_X86 +# ifdef FOURIER_ALLOW_SSE +void Fourier_ApplyAmplitude_SSE FUNC_ARGS_DECL; +# ifdef FOURIER_ALLOW_FMA +void Fourier_ApplyAmplitude_SSE_FMA FUNC_ARGS_DECL; +# endif +# endif +# ifdef FOURIER_ALLOW_AVX +void Fourier_ApplyAmplitude_AVX FUNC_ARGS_DECL; +# ifdef FOURIER_ALLOW_FMA +void Fourier_ApplyAmplitude_AVX_FMA FUNC_ARGS_DECL; +# endif +# endif +#endif + +/************************************************/ + +static const struct Fourier_FuncTbl_t DispatchTbl_ApplyAmplitude = { + .Generic = Fourier_ApplyAmplitude_Generic, +#ifdef FOURIER_IS_X86 +# ifdef FOURIER_ALLOW_SSE + .SSE = Fourier_ApplyAmplitude_SSE, +# ifdef FOURIER_ALLOW_FMA + .SSE_FMA = Fourier_ApplyAmplitude_SSE_FMA, +# endif +# endif +# ifdef FOURIER_ALLOW_AVX + .AVX = Fourier_ApplyAmplitude_AVX, +# ifdef FOURIER_ALLOW_FMA + .AVX_FMA = Fourier_ApplyAmplitude_AVX_FMA, +# endif +# endif +#endif +}; + +/************************************************/ + +typedef void (*Dispatcher_ApplyAmplitude_t) FUNC_ARGS_DECL; +static void InitDispatcher_ApplyAmplitude FUNC_ARGS_DECL; + +static Dispatcher_ApplyAmplitude_t Dispatcher_ApplyAmplitude = InitDispatcher_ApplyAmplitude; + +static void InitDispatcher_ApplyAmplitude FUNC_ARGS_DECL { + Dispatcher_ApplyAmplitude = (Dispatcher_ApplyAmplitude_t)Fourier_GetDispatchFnc(&DispatchTbl_ApplyAmplitude); + Dispatcher_ApplyAmplitude FUNC_ARGS; +} + +/************************************************/ + +void Fourier_ApplyAmplitude FUNC_ARGS_DECL { + Dispatcher_ApplyAmplitude FUNC_ARGS; +} + +/************************************************/ + +#include "Fourier_ApplyAmplitude_Template.h" +void Fourier_ApplyAmplitude_Generic FUNC_ARGS_DECL { + Fourier_ApplyAmplitude_Template FUNC_ARGS; +} + +#undef FUNC_ARGS_DECL +#undef FUNC_ARGS + +/************************************************/ +// EOF +/************************************************/ diff --git a/tools/audioconv64/libulc/libfourier/Fourier_ApplyAmplitude_Template.h b/tools/audioconv64/libulc/libfourier/Fourier_ApplyAmplitude_Template.h new file mode 100644 index 000000000..29c2e0312 --- /dev/null +++ b/tools/audioconv64/libulc/libfourier/Fourier_ApplyAmplitude_Template.h @@ -0,0 +1,66 @@ +/************************************************/ +#include "Fourier.h" +#include "Fourier_Helper.h" +/************************************************/ + +#define AMPLITUDE_MAX_RATIO 1.0e10f + +/************************************************/ + +FOURIER_FORCE_INLINE +void Fourier_ApplyAmplitude_Template(float *Dst, const float *Src, const float *Amplitude, float DryLevel, int N) { + int n; + FOURIER_ASSUME_ALIGNED(Dst, FOURIER_ALIGNMENT); + FOURIER_ASSUME_ALIGNED(Src, FOURIER_ALIGNMENT); + FOURIER_ASSUME_ALIGNED(Amplitude, FOURIER_ALIGNMENT); + FOURIER_ASSUME(N >= FOURIER_VSTRIDE); + + //! Because we will end up using all eight SSE + //! registers for this, the code is written in + //! a way that the compiler should never spill + //! anything to the stack. +#if FOURIER_VSTRIDE > 1 + Fourier_Vec_t x0, x1, x2, x3, x4, x5, x6, x7 = FOURIER_VSET1(DryLevel); + for(n=0;n= FOURIER_VSTRIDE); + for(n=0;n= FOURIER_VSTRIDE); + for(n=0;n= FOURIER_VSTRIDE*2); + SrcA = Src; + SrcB = Src + N; + DstA = Dst; + DstB = Dst + N; + for(n=0;n= FOURIER_VSTRIDE*2); + SrcA = Src; + SrcB = Src + N; + DstA = Dst; + DstB = Dst + N; + for(n=0;n= FOURIER_VSTRIDE*2); + + // Compute Re -> Tmp[], Im -> Tmp[]+N/2 + SrcA = Buf + N/2; + SrcB = Buf + N/2; + Dst = Tmp; +#if FOURIER_VSTRIDE > 1 + for(n=0;n 1 + for(n=0;n= FOURIER_VSTRIDE*2); + + // Deinterleave {Re,Im} and transform to xe,xo + DstA = Tmp; + DstB = Tmp + N; + Src = Buf; +#if FOURIER_VSTRIDE > 1 + for(n=0;n 1 + for(n=0;n= 8); + + // Stop condition + if(N == 8) { + Fourier_DCT2_8(Buf); + return; + } + + // Perform butterflies + // u = H_n.x + { + const float *SrcLo = Buf; + const float *SrcHi = Buf + N; + float *DstLo = Tmp; + float *DstHi = Tmp + N/2; +#if FOURIER_VSTRIDE > 1 + Fourier_Vec_t a, b; + Fourier_Vec_t s, d; + for(i=0;i 1 + Fourier_Vec_t a, b; + for(i=0;i= 8); + + // Stop condition + if(N == 8) { + Fourier_DCT4_8(Buf); + return; + } + + // Perform rotation butterflies + // u = R_n.x + { + const float *SrcLo = Buf; + const float *SrcHi = Buf + N; + float *DstLo = Tmp; + float *DstHi = Tmp + N/2; +#if FOURIER_VSTRIDE > 1 + Fourier_Vec_t a, b; + Fourier_Vec_t t0, t1 = FOURIER_VMUL(FOURIER_VSET1(1.0f/N), FOURIER_VADD(FOURIER_VSET_LINEAR_RAMP(), FOURIER_VSET1(0.5f))); + Fourier_Vec_t c = Fourier_Cos(t1); + Fourier_Vec_t s = Fourier_Sin(t1); + Fourier_Vec_t wc = Fourier_Cos(FOURIER_VSET1((float)FOURIER_VSTRIDE / N)); + Fourier_Vec_t ws = Fourier_Sin(FOURIER_VSET1((float)FOURIER_VSTRIDE / N)); + for(i=0;i 1 + { + Fourier_Vec_t a, b; + Fourier_Vec_t t0, t1; + for(i=0;i +#include +/************************************************/ +#if (defined(_M_IX86) || defined(_M_X64) || defined(__i386__)) +# if defined(_MSC_VER) +# define FOURIER_IS_X86 +# include +# elif defined(__GNUC__) +# define FOURIER_IS_X86 +# include +# else +# warning "Unknown compiler. Disabling x86-specific code." +# endif +# if defined(FOURIER_IS_X86) +# if (defined(__AVX__) && defined(FOURIER_ALLOW_AVX)) || (defined(__FMA__) && defined(FOURIER_ALLOW_FMA)) +# include +# endif +# if (defined(__SSE__) && defined(FOURIER_ALLOW_SSE)) +# include +# endif +# endif +#endif +/************************************************/ +#if defined(_MSC_VER) +# define FOURIER_FORCE_INLINE static inline __forceinline +# define FOURIER_ASSUME(Cond) __assume(Cond) +# define FOURIER_ASSUME_ALIGNED(x,Align) __assume(((uintptr_t)(x) & (Align)) == 0) +#elif defined(__GNUC__) +# define FOURIER_FORCE_INLINE static inline __attribute__ ((always_inline)) +# define FOURIER_ASSUME(Cond) (Cond) ? ((void)0) : __builtin_unreachable() +# define FOURIER_ASSUME_ALIGNED(x,Align) x = __builtin_assume_aligned(x,Align) +#else +# define FOURIER_FORCE_INLINE static inline +# define FOURIER_ASSUME(Cond) +# define FOURIER_ASSUME_ALIGNED(x,Align) +#endif +/************************************************/ + +#if (defined(FOURIER_IS_X86) && defined(__AVX__) && defined(FOURIER_ALLOW_AVX)) + typedef __m256 Fourier_Vec_t; +# define FOURIER_VSTRIDE 8 +# define FOURIER_ALIGNMENT 32 +# define FOURIER_VLOAD(Src) _mm256_load_ps(Src) +# define FOURIER_VLOADU(Src) _mm256_loadu_ps(Src) +# define FOURIER_VSTORE(Dst, x) _mm256_store_ps(Dst, x) +# define FOURIER_VSTOREU(Dst, x) _mm256_storeu_ps(Dst, x) +# define FOURIER_VSET1(x) _mm256_set1_ps(x) +# define FOURIER_VSET_LINEAR_RAMP() _mm256_setr_ps(0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f) +# define FOURIER_VADD(x, y) _mm256_add_ps(x, y) +# define FOURIER_VSUB(x, y) _mm256_sub_ps(x, y) +# define FOURIER_VMUL(x, y) _mm256_mul_ps(x, y) +# define FOURIER_VDIV(x, y) _mm256_div_ps(x, y) +# define FOURIER_VREVERSE_LANE(x) _mm256_shuffle_ps(x, x, 0x1B) +# define FOURIER_VREVERSE(x) _mm256_permute2f128_ps(FOURIER_VREVERSE_LANE(x), FOURIER_VREVERSE_LANE(x), 0x01) +# define FOURIER_VNEGATE_ODD(x) _mm256_xor_ps(x, _mm256_setr_ps(0.0f, -0.0f, 0.0f, -0.0f, 0.0f, -0.0f, 0.0f, -0.0f)) +# if (defined(__FMA__) && defined(FOURIER_ALLOW_FMA)) +# define FOURIER_VFMA(x, y, a) _mm256_fmadd_ps(x, y, a) +# define FOURIER_VFMS(x, y, a) _mm256_fmsub_ps(x, y, a) +# define FOURIER_VNFMA(x, y, a) _mm256_fnmadd_ps(x, y, a) +# else +# define FOURIER_VFMA(x, y, a) _mm256_add_ps(_mm256_mul_ps(x, y), a) +# define FOURIER_VFMS(x, y, a) _mm256_sub_ps(_mm256_mul_ps(x, y), a) +# define FOURIER_VNFMA(x, y, a) _mm256_sub_ps(a, _mm256_mul_ps(x, y)) +# endif +# define FOURIER_VSQRT(x) _mm256_sqrt_ps(x) +# define FOURIER_VAND(x, y) _mm256_and_ps(x, y) +# define FOURIER_VANDNOT(x, y) _mm256_andnot_ps(y, x) +# define FOURIER_VOR(x, y) _mm256_or_ps(x, y) +# define FOURIER_VCMPEQ(x, y) _mm256_cmp_ps(x, y, 0) +# define FOURIER_VCMPNE(x, y) _mm256_cmp_ps(x, y, 4) +# define FOURIER_VCMPLT(x, y) _mm256_cmp_ps(x, y, 1) + FOURIER_FORCE_INLINE void FOURIER_VINTERLEAVE(Fourier_Vec_t a, Fourier_Vec_t b, Fourier_Vec_t *Lo, Fourier_Vec_t *Hi) { + __m256 t0 = _mm256_unpacklo_ps(a, b); + __m256 t1 = _mm256_unpackhi_ps(a, b); + *Lo = _mm256_permute2f128_ps(t0, t1, 0x20); + *Hi = _mm256_permute2f128_ps(t0, t1, 0x31); + } + FOURIER_FORCE_INLINE void FOURIER_VSPLIT_EVEN_ODD(Fourier_Vec_t l, Fourier_Vec_t h, Fourier_Vec_t *Even, Fourier_Vec_t *Odd) { + __m256 a = _mm256_permute2f128_ps(l, h, 0x20); + __m256 b = _mm256_permute2f128_ps(l, h, 0x31); + *Even = _mm256_shuffle_ps(a, b, 0x88); + *Odd = _mm256_shuffle_ps(a, b, 0xDD); + } + FOURIER_FORCE_INLINE void FOURIER_VSPLIT_EVEN_ODDREV(Fourier_Vec_t l, Fourier_Vec_t h, Fourier_Vec_t *Even, Fourier_Vec_t *Odd) { + __m256 a = _mm256_permute2f128_ps(l, h, 0x20); + __m256 b = _mm256_permute2f128_ps(l, h, 0x31); + *Even = _mm256_shuffle_ps(a, b, 0x88); + b = _mm256_shuffle_ps(b, a, 0x77); + *Odd = _mm256_permute2f128_ps(b, b, 0x01); + } +#elif (defined(FOURIER_IS_X86) && defined(__SSE__) && defined(FOURIER_ALLOW_SSE)) + typedef __m128 Fourier_Vec_t; +# define FOURIER_VSTRIDE 4 +# define FOURIER_ALIGNMENT 16 +# define FOURIER_VLOAD(Src) _mm_load_ps(Src) +# define FOURIER_VLOADU(Src) _mm_loadu_ps(Src) +# define FOURIER_VSTORE(Dst, x) _mm_store_ps(Dst, x) +# define FOURIER_VSTOREU(Dst, x) _mm_storeu_ps(Dst, x) +# define FOURIER_VSET1(x) _mm_set1_ps(x) +# define FOURIER_VSET_LINEAR_RAMP() _mm_setr_ps(0.0f, 1.0f, 2.0f, 3.0f) +# define FOURIER_VADD(x, y) _mm_add_ps(x, y) +# define FOURIER_VSUB(x, y) _mm_sub_ps(x, y) +# define FOURIER_VMUL(x, y) _mm_mul_ps(x, y) +# define FOURIER_VDIV(x, y) _mm_div_ps(x, y) +# define FOURIER_VREVERSE(x) _mm_shuffle_ps(x, x, 0x1B) +# define FOURIER_VNEGATE_ODD(x) _mm_xor_ps(x, _mm_setr_ps(0.0f, -0.0f, 0.0f, -0.0f)) +# if (defined(__FMA__) && defined(FOURIER_ALLOW_FMA)) +# define FOURIER_VFMA(x, y, a) _mm_fmadd_ps(x, y, a) +# define FOURIER_VFMS(x, y, a) _mm_fmsub_ps(x, y, a) +# define FOURIER_VNFMA(x, y, a) _mm_fnmadd_ps(x, y, a) +# else +# define FOURIER_VFMA(x, y, a) _mm_add_ps(_mm_mul_ps(x, y), a) +# define FOURIER_VFMS(x, y, a) _mm_sub_ps(_mm_mul_ps(x, y), a) +# define FOURIER_VNFMA(x, y, a) _mm_sub_ps(a, _mm_mul_ps(x, y)) +# endif +# define FOURIER_VSQRT(x) _mm_sqrt_ps(x) +# define FOURIER_VAND(x, y) _mm_and_ps(x, y) +# define FOURIER_VANDNOT(x, y) _mm_andnot_ps(y, x) +# define FOURIER_VOR(x, y) _mm_or_ps(x, y) +# define FOURIER_VCMPEQ(x, y) _mm_cmpeq_ps(x, y) +# define FOURIER_VCMPNE(x, y) _mm_cmpneq_ps(x, y) +# define FOURIER_VCMPLT(x, y) _mm_cmplt_ps(x, y) + FOURIER_FORCE_INLINE void FOURIER_VINTERLEAVE(Fourier_Vec_t a, Fourier_Vec_t b, Fourier_Vec_t *Lo, Fourier_Vec_t *Hi) { + *Lo = _mm_unpacklo_ps(a, b); + *Hi = _mm_unpackhi_ps(a, b); + } + FOURIER_FORCE_INLINE void FOURIER_VSPLIT_EVEN_ODD(Fourier_Vec_t l, Fourier_Vec_t h, Fourier_Vec_t *Even, Fourier_Vec_t *Odd) { + *Even = _mm_shuffle_ps(l, h, 0x88); + *Odd = _mm_shuffle_ps(l, h, 0xDD); + } + FOURIER_FORCE_INLINE void FOURIER_VSPLIT_EVEN_ODDREV(Fourier_Vec_t l, Fourier_Vec_t h, Fourier_Vec_t *Even, Fourier_Vec_t *Odd) { + *Even = _mm_shuffle_ps(l, h, 0x88); + *Odd = _mm_shuffle_ps(h, l, 0x77); + } +#else + typedef float Fourier_Vec_t; +# define FOURIER_VSTRIDE 1 +# define FOURIER_ALIGNMENT 4 +# define FOURIER_VLOAD(Src) (*Src) +# define FOURIER_VLOADU(Src) (*Src) +# define FOURIER_VSTORE(Dst, x) (*Dst = x) +# define FOURIER_VSTOREU(Dst, x) (*Dst = x) +# define FOURIER_VSET1(x) (x) +# define FOURIER_VSET_LINEAR_RAMP() (0.0f) +# define FOURIER_VADD(x, y) ((x) + (y)) +# define FOURIER_VSUB(x, y) ((x) - (y)) +# define FOURIER_VMUL(x, y) ((x) * (y)) +# define FOURIER_VDIV(x, y) ((x) / (y)) +# define FOURIER_VREVERSE(x) (x) +# define FOURIER_VNEGATE_ODD(x) (x) +# define FOURIER_VFMA(x, y, a) ((x)*(y) + (a)) +# define FOURIER_VFMS(x, y, a) ((x)*(y) - (a)) +# define FOURIER_VNFMA(x, y, a) ((a) - (x)*(y)) +# define FOURIER_VSQRT(x) sqrtf(x) +#endif + +/************************************************/ + +// Function tables for different CPU specs +struct Fourier_FuncTbl_t { + void *Generic; +#ifdef FOURIER_IS_X86 +# if defined(FOURIER_ALLOW_SSE) || defined(FOURIER_ALLOW_AVX) + void *SSE; +# if defined(FOURIER_ALLOW_FMA) + void *SSE_FMA; +# endif +# endif +# if defined(FOURIER_ALLOW_AVX) + void *AVX; +# if defined(FOURIER_ALLOW_FMA) + void *AVX_FMA; +# endif +# endif +#endif +}; + +/************************************************/ + +// Get best function based on CPU specs +FOURIER_FORCE_INLINE +void *Fourier_GetDispatchFnc(const struct Fourier_FuncTbl_t *FuncTbl) { + void *Fnc = FuncTbl->Generic; +#ifdef FOURIER_IS_X86 + uint32_t cpuInfo[4] = {0}; +# if defined(_MSC_VER) + __cpuid(cpuInfo, 1); +# else + __get_cpuid(1, &cpuInfo[0], &cpuInfo[1], &cpuInfo[2], &cpuInfo[3]); +# endif +# if defined(FOURIER_ALLOW_SSE) + if((cpuInfo[3] & (1<<25)) != 0) + Fnc = FuncTbl->SSE; +# if defined(FOURIER_ALLOW_FMA) + if((cpuInfo[3] & (1<<25)) != 0 && (cpuInfo[2] & (1<<12)) != 0) + Fnc = FuncTbl->SSE_FMA; +# endif +# endif +# if defined(FOURIER_ALLOW_AVX) + if((cpuInfo[2] & (1<<28)) != 0) + Fnc = FuncTbl->AVX; +# if defined(FOURIER_ALLOW_FMA) + if((cpuInfo[2] & (1<<28)) != 0 && (cpuInfo[2] & (1<<12)) != 0) + Fnc = FuncTbl->AVX_FMA; +# endif +# endif +#endif + return Fnc; +} + +/************************************************/ + +// Sin[z] approximation (where z = x*Pi/2, x < Pi/2) +// Coefficients derived by RMSE minimization +FOURIER_FORCE_INLINE +Fourier_Vec_t Fourier_Sin(Fourier_Vec_t x) { + Fourier_Vec_t x2 = FOURIER_VMUL(x, x); + Fourier_Vec_t Res = FOURIER_VFMA(x2, FOURIER_VSET1(+0x1.3C8B08p-13f), FOURIER_VSET1(-0x1.3237ECp-8f)); + Res = FOURIER_VFMA(x2, Res, FOURIER_VSET1(+0x1.4667ACp-4f)); + Res = FOURIER_VFMA(x2, Res, FOURIER_VSET1(-0x1.4ABBB8p-1f)); + Res = FOURIER_VFMA(x2, Res, FOURIER_VSET1(+0x1.921FB4p0f)); + return FOURIER_VMUL(x, Res); +} + +/************************************************/ + +// Cos[z] approximation (where z = x*Pi/2, x < Pi/2) +// Coefficients derived by RMSE minimization +FOURIER_FORCE_INLINE +Fourier_Vec_t Fourier_Cos(Fourier_Vec_t x) { + Fourier_Vec_t x2 = FOURIER_VMUL(x, x); + Fourier_Vec_t Res = FOURIER_VFMA(x2, FOURIER_VSET1(+0x1.C2EA62p-11f), FOURIER_VSET1(-0x1.550810p-6f)); + Res = FOURIER_VFMA(x2, Res, FOURIER_VSET1(+0x1.03BDD6p-2f)); + Res = FOURIER_VFMA(x2, Res, FOURIER_VSET1(-0x1.3BD3B2p0f)); + Res = FOURIER_VFMA(x2, Res, FOURIER_VSET1(1.0f)); + return Res; +} + +/************************************************/ + +// End-condition routines +void Fourier_DCT2_8(float *x); // 8-point DCT-II +void Fourier_DCT4_8(float *x); // 8-point DCT-IV + +/************************************************/ +// EOF +/************************************************/ diff --git a/tools/audioconv64/libulc/libfourier/Fourier_IMDCT.c b/tools/audioconv64/libulc/libfourier/Fourier_IMDCT.c new file mode 100644 index 000000000..29fba9114 --- /dev/null +++ b/tools/audioconv64/libulc/libfourier/Fourier_IMDCT.c @@ -0,0 +1,76 @@ +/************************************************/ +#include "Fourier.h" +#include "Fourier_Helper.h" +/************************************************/ + +#define FUNC_ARGS_LIST float *BufOut, const float *BufIn, float *BufLap, float *BufTmp, int N, int Overlap + +/************************************************/ + +// Declare specialized routines +void Fourier_IMDCT_Generic(FUNC_ARGS_LIST); +#ifdef FOURIER_IS_X86 +# ifdef FOURIER_ALLOW_SSE +void Fourier_IMDCT_SSE (FUNC_ARGS_LIST); +# ifdef FOURIER_ALLOW_FMA +void Fourier_IMDCT_SSE_FMA(FUNC_ARGS_LIST); +# endif +# endif +# ifdef FOURIER_ALLOW_AVX +void Fourier_IMDCT_AVX (FUNC_ARGS_LIST); +# ifdef FOURIER_ALLOW_FMA +void Fourier_IMDCT_AVX_FMA(FUNC_ARGS_LIST); +# endif +# endif +#endif + +/************************************************/ + +static const struct Fourier_FuncTbl_t DispatchTbl_IMDCT = { + .Generic = Fourier_IMDCT_Generic, +#ifdef FOURIER_IS_X86 +# ifdef FOURIER_ALLOW_SSE + .SSE = Fourier_IMDCT_SSE, +# ifdef FOURIER_ALLOW_FMA + .SSE_FMA = Fourier_IMDCT_SSE_FMA, +# endif +# endif +# ifdef FOURIER_ALLOW_AVX + .AVX = Fourier_IMDCT_AVX, +# ifdef FOURIER_ALLOW_FMA + .AVX_FMA = Fourier_IMDCT_AVX_FMA, +# endif +# endif +#endif +}; + +/************************************************/ + +typedef void (*Dispatcher_IMDCT_t)(FUNC_ARGS_LIST); +static void InitDispatcher_IMDCT(FUNC_ARGS_LIST); + +static Dispatcher_IMDCT_t Dispatcher_IMDCT = InitDispatcher_IMDCT; + +static void InitDispatcher_IMDCT(FUNC_ARGS_LIST) { + Dispatcher_IMDCT = (Dispatcher_IMDCT_t)Fourier_GetDispatchFnc(&DispatchTbl_IMDCT); + Dispatcher_IMDCT(BufOut, BufIn, BufLap, BufTmp, N, Overlap); +} + +/************************************************/ + +void Fourier_IMDCT(FUNC_ARGS_LIST) { + Dispatcher_IMDCT(BufOut, BufIn, BufLap, BufTmp, N, Overlap); +} + +/************************************************/ + +#include "Fourier_IMDCT_Template.h" +void Fourier_IMDCT_Generic(FUNC_ARGS_LIST) { + Fourier_IMDCT_Template(BufOut, BufIn, BufLap, BufTmp, N, Overlap); +} + +#undef FUNC_ARGS_LIST + +/************************************************/ +// EOF +/************************************************/ diff --git a/tools/audioconv64/libulc/libfourier/Fourier_IMDCT_Template.h b/tools/audioconv64/libulc/libfourier/Fourier_IMDCT_Template.h new file mode 100644 index 000000000..8dc44e24c --- /dev/null +++ b/tools/audioconv64/libulc/libfourier/Fourier_IMDCT_Template.h @@ -0,0 +1,98 @@ +/************************************************/ +#include "Fourier.h" +#include "Fourier_Helper.h" +/************************************************/ + +// Implementation notes for IMDCT: +// IMDCT is implemented via DCT-IV, which can be thought of +// as splitting the MDCT inputs into four regions: +// {A,B,C,D} +// and then taking the DCT-IV of: +// {C_r + D, B_r - A} +// On IMDCT, we get back these latter values following an +// inverse DCT-IV (which is itself a DCT-IV due to its +// involutive nature). +// From a prior call, we keep C_r+D buffered, which becomes +// A_r+B after accounting for movement to the next block. +// We can then state: +// Reverse(A_r + B) - (B_r - A) = (A + B_r) - (B_r - A) = 2A +// (A_r + B) + Reverse(B_r - A) = (A_r + B) + (B - A_r) = 2B +// ^ Buffered ^ New input data +// Allowing us to reconstruct the inputs A,B. +FOURIER_FORCE_INLINE +void Fourier_IMDCT_Template(float *BufOut, const float *BufIn, float *BufLap, float *BufTmp, int N, int Overlap) { + int i; + FOURIER_ASSUME_ALIGNED(BufOut, FOURIER_ALIGNMENT); + FOURIER_ASSUME_ALIGNED(BufIn, FOURIER_ALIGNMENT); + FOURIER_ASSUME_ALIGNED(BufLap, FOURIER_ALIGNMENT); + FOURIER_ASSUME_ALIGNED(BufTmp, FOURIER_ALIGNMENT); + FOURIER_ASSUME(N >= 16); + FOURIER_ASSUME(Overlap >= 0 && Overlap <= N); + + const float *Lap = BufLap + N/2; + const float *Tmp = BufTmp + N/2; + float *OutLo = BufOut; + float *OutHi = BufOut + N; + + // Undo transform + for(i=0;i 1 + Fourier_Vec_t a, b; + for(i=0;i<(N-Overlap)/2;i+=FOURIER_VSTRIDE) { + Lap -= FOURIER_VSTRIDE; a = FOURIER_VLOAD(Lap); + b = FOURIER_VLOAD(Tmp); Tmp += FOURIER_VSTRIDE; + a = FOURIER_VREVERSE(a); + b = FOURIER_VREVERSE(b); + FOURIER_VSTORE(OutLo, a); OutLo += FOURIER_VSTRIDE; + OutHi -= FOURIER_VSTRIDE; FOURIER_VSTORE(OutHi, b); + } + Fourier_Vec_t t0, t1 = FOURIER_VMUL(FOURIER_VSET1(1.0f/Overlap), FOURIER_VADD(FOURIER_VSET_LINEAR_RAMP(), FOURIER_VSET1(0.5f))); + Fourier_Vec_t c = Fourier_Cos(t1); + Fourier_Vec_t s = Fourier_Sin(t1); + Fourier_Vec_t wc = Fourier_Cos(FOURIER_VSET1((float)FOURIER_VSTRIDE / Overlap)); + Fourier_Vec_t ws = Fourier_Sin(FOURIER_VSET1((float)FOURIER_VSTRIDE / Overlap)); + for(;i= 16); + FOURIER_ASSUME(Overlap >= 0 && Overlap <= N); + + float *LapLo = Lap; + float *LapHi = Lap + N; + const float *NewLo = New; + const float *NewHi = New + N; + float *MDCTMid = MDCT + N/2; + float *MDSTMid = MDST + N/2; + + // Perform windowed lapping + // NOTE: We compute MDST using DCT-IV, so sign-flip every other + // value here, and then reverse the whole array after DCT-IV. + { +#if FOURIER_VSTRIDE > 1 + Fourier_Vec_t A, Br, C, Dr; + Fourier_Vec_t t0 = FOURIER_VSET1(0.0f); + for(n=0;n<(N-Overlap)/2;n+=FOURIER_VSTRIDE) { + A = FOURIER_VLOAD(LapLo + n); + Br = FOURIER_VLOAD(LapHi-FOURIER_VSTRIDE - n); + C = FOURIER_VLOAD(NewLo + n); + Dr = FOURIER_VLOAD(NewHi-FOURIER_VSTRIDE - n); + Br = FOURIER_VREVERSE(Br); + C = FOURIER_VREVERSE(C); + FOURIER_VSTORE(LapLo + n, t0); + FOURIER_VSTORE(LapHi -FOURIER_VSTRIDE - n, Dr); + FOURIER_VSTORE(MDCTMid-FOURIER_VSTRIDE - n, C); + FOURIER_VSTORE(MDSTMid-FOURIER_VSTRIDE - n, FOURIER_VNEGATE_ODD(C)); + C = FOURIER_VSUB(Br, A); + Dr = FOURIER_VADD(Br, A); + FOURIER_VSTORE(MDCTMid + n, C); + FOURIER_VSTORE(MDSTMid + n, FOURIER_VNEGATE_ODD(Dr)); + } + Fourier_Vec_t t1 = FOURIER_VMUL(FOURIER_VSET1(1.0f/Overlap), FOURIER_VADD(FOURIER_VSET_LINEAR_RAMP(), FOURIER_VSET1(0.5f))); + Fourier_Vec_t c = Fourier_Cos(t1); + Fourier_Vec_t s = Fourier_Sin(t1); + Fourier_Vec_t wc = Fourier_Cos(FOURIER_VSET1((float)FOURIER_VSTRIDE / Overlap)); + Fourier_Vec_t ws = Fourier_Sin(FOURIER_VSET1((float)FOURIER_VSTRIDE / Overlap)); + for(;n 1 + Fourier_Vec_t v0, v1; + for(n=0;n diff --git a/tools/audioconv64/libulc/libfourier/README.md b/tools/audioconv64/libulc/libfourier/README.md new file mode 100644 index 000000000..2cee8d6a1 --- /dev/null +++ b/tools/audioconv64/libulc/libfourier/README.md @@ -0,0 +1,21 @@ +# libfourier +This is the repository for Fourier-related routines used by my code. + +There is a lot of "templating" happening so I can compile for SSE/AVX/FMA/whatever using the same code. + +## Motivation + +It's fun to work on this rather than just use off-the-shelf components. + +Sure, I could maybe get better performance using heavily-tested libraries, but it really was more fun to roll my own. + +## Contents + +* DCT (type II and IV) +* MDCT+MDST and IMDCT +* Centered FFTs/iFFTs of real data +* Specialized routines used by some of my projects +* etc. + +## License +All code here is released under the Unlicense terms - go wild with it. diff --git a/tools/audioconv64/libulc/ulcDecoder.c b/tools/audioconv64/libulc/ulcDecoder.c new file mode 100644 index 000000000..d68daf0e4 --- /dev/null +++ b/tools/audioconv64/libulc/ulcDecoder.c @@ -0,0 +1,306 @@ +/**************************************/ +//! ulc-codec: Ultra-Low-Complexity Audio Codec +//! Copyright (C) 2022, Ruben Nunez (Aikku; aik AT aol DOT com DOT au) +//! Refer to the project README file for license terms. +/**************************************/ +#include +#include +#include +/**************************************/ +#include "libfourier/Fourier.h" +#include "ulcDecoder.h" +#include "ulcHelper.h" +/**************************************/ +#define BUFFER_ALIGNMENT 64u //! Always align memory to 64-byte boundaries (preparation for AVX-512) +/**************************************/ + +//! Just for consistency +#define MIN_CHANS 1 +#define MAX_CHANS 255 +#define MIN_BANDS 256 +#define MAX_BANDS 32768 + +/**************************************/ + +//! Initialize decoder state +int ULC_DecoderState_Init(struct ULC_DecoderState_t *State) { + //! Clear anything that is needed for EncoderState_Destroy() + State->BufferData = NULL; + + //! Verify parameters + int nChan = State->nChan; + int BlockSize = State->BlockSize; + if(nChan < MIN_CHANS || nChan > MAX_CHANS) return -1; + if(BlockSize < MIN_BANDS || BlockSize > MAX_BANDS) return -1; + if((BlockSize & (-BlockSize)) != BlockSize) return -1; + + //! Get buffer offsets and allocation size + int AllocSize = 0; +#define CREATE_BUFFER(Name, Sz) int Name##_Offs = AllocSize; AllocSize += Sz + CREATE_BUFFER(TransformBuffer, sizeof(float) * ( BlockSize )); + CREATE_BUFFER(TransformTemp, sizeof(float) * (nChan* BlockSize ) * 2); + CREATE_BUFFER(TransformInvLap, sizeof(float) * (nChan*(BlockSize/2))); +#undef CREATE_BUFFER + + //! Allocate buffer space + char *Buf = State->BufferData = malloc(BUFFER_ALIGNMENT-1 + AllocSize); + if(!Buf) return -1; + + //! Initialize state + int i; + Buf += (-(uintptr_t)Buf) & (BUFFER_ALIGNMENT-1); + State->LastSubBlockSize = 0; + State->TransformBuffer = (float*)(Buf + TransformBuffer_Offs); + State->TransformTemp = (float*)(Buf + TransformTemp_Offs); + State->TransformInvLap = (float*)(Buf + TransformInvLap_Offs); + for(i=0;iTransformInvLap[i] = 0.0f; + + //! Success + return 1; +} + +/**************************************/ + +//! Destroy decoder state +void ULC_DecoderState_Destroy(struct ULC_DecoderState_t *State) { + //! Free buffer space + free(State->BufferData); +} + +/**************************************/ + +//! Decode block +#define ESCAPE_SEQUENCE_STOP (-1) +#define ESCAPE_SEQUENCE_STOP_NOISEFILL (-2) +static inline uint32_t Block_Decode_UpdateRandomSeed(void) { + static uint32_t Seed = 1234567; + Seed ^= Seed << 13; //! Xorshift + Seed ^= Seed >> 17; + Seed ^= Seed << 5; + return Seed; +} +static inline uint8_t Block_Decode_ReadNybble(const uint8_t **Src, int *Size) { + //! Fetch and shift nybble + uint8_t x = *(*Src); + *Size += 4; + if((*Size)%8u == 0) x >>= 4, (*Src)++; + return x&0xF; +} +static inline int Block_Decode_ReadQuantizer(const uint8_t **Src, int *Size) { + int qi = Block_Decode_ReadNybble(Src, Size); //! Fh,0h..Dh: Quantizer change + if(qi == 0xF) return ESCAPE_SEQUENCE_STOP_NOISEFILL; //! Fh,Fh,Zh,Yh,Xh: Noise fill (to end; exp-decay) + if(qi == 0xE) qi += Block_Decode_ReadNybble(Src, Size); //! Fh,Eh,0h..Ch: Quantizer change (extended precision) + if(qi == 0xE + 0xF) return ESCAPE_SEQUENCE_STOP; //! Fh,Eh,Fh: Zeros fill (to end) + return qi; +} +static inline float Block_Decode_ExpandQuantizer(int qi) { + return 0x1.0p-31f * ((1u<<(31-5)) >> qi); //! 1 / (2^5 * 2^qi) +} +static inline int Block_Decode_DecodeSubBlockCoefs(float *CoefDst, int N, const uint8_t **Src, int *Size) { + int32_t n, v; + + //! Check first quantizer for Stop code + v = Block_Decode_ReadQuantizer(Src, Size); + if(v == ESCAPE_SEQUENCE_STOP) { + //! [Fh,]Eh,Fh: Stop + do *CoefDst++ = 0.0f; while(--N); + return 1; + } + + //! Unpack the [sub]block's coefficients + float Quant = Block_Decode_ExpandQuantizer(v); + for(;;) { + //! -7h..-2h, +2..+7h: Normal + v = Block_Decode_ReadNybble(Src, Size); + if(v != 0x0 && v != 0x1 && v != 0x8 && v != 0xF) { //! <- Exclude all control codes + //! Store linearized, dequantized coefficient + v = (v^0x8) - 0x8; //! Sign extension + v = (v < 0) ? (-v*v) : (+v*v); + *CoefDst++ = v * Quant; + if(--N == 0) break; + continue; + } + + //! 0h,0h..Fh: Zeros fill (1 .. 16 coefficients) + if(v == 0x0) { + n = Block_Decode_ReadNybble(Src, Size) + 1; + if(n > N) return 0; + N -= n; + do *CoefDst++ = 0.0f; while(--n); + if(N == 0) break; + continue; + } + + //! 1h,Yh,Xh: 33 .. 288 zeros fill + if(v == 0x1) { + n = Block_Decode_ReadNybble(Src, Size); + n = Block_Decode_ReadNybble(Src, Size) | (n<<4); + n += 33; + if(n > N) return 0; + N -= n; + do *CoefDst++ = 0.0f; while(--n); + if(N == 0) break; + continue; + } + + //! 8h,Zh,Yh,Xh: 16 .. 527 noise fill + if(v == 0x8) { + n = Block_Decode_ReadNybble(Src, Size); + n = Block_Decode_ReadNybble(Src, Size) | (n<<4); + v = Block_Decode_ReadNybble(Src, Size); + n = (v&1) | (n<<1); + v = (v>>1) + 1; + n += 16; + if(n > N) return 0; + N -= n; { + float p = (v*v) * Quant * (1.0f/4); + do { + if(Block_Decode_UpdateRandomSeed() & 0x80000000) p = -p; + *CoefDst++ = p; + } while(--n); + } + if(N == 0) break; + continue; + } + + //! Fh,0h..Dh: Quantizer change + //! Fh,Eh,0h..Ch: Quantizer change (extended precision) + v = Block_Decode_ReadQuantizer(Src, Size); + if(v >= 0) { + Quant = Block_Decode_ExpandQuantizer(v); + continue; + } + + //! Fh,Fh,Zh,Yh,Xh: Noise fill (to end; exp-decay) + if(v == ESCAPE_SEQUENCE_STOP_NOISEFILL) { + v = Block_Decode_ReadNybble(Src, Size) + 1; + n = Block_Decode_ReadNybble(Src, Size); + n = Block_Decode_ReadNybble(Src, Size) | (n<<4); + float p = (v*v) * Quant * (1.0f/16); + float r = 1.0f + (n*n)*-0x1.0p-19f; + do { + if(Block_Decode_UpdateRandomSeed() & 0x80000000) p = -p; + *CoefDst++ = p, p *= r; + } while(--N); + break; + } + + //! Fh,Eh,Dh: Unused + //! Fh,Eh,Eh: Unused + //! Fh,Eh,Fh: Zeros fill (to end) + if(v == ESCAPE_SEQUENCE_STOP) { + do *CoefDst++ = 0.0f; while(--N); + break; + } + } + return 1; +} +int ULC_DecodeBlock(struct ULC_DecoderState_t *State, float *DstData, const void *_SrcBuffer) { + //! Spill state to local variables to make things easier to read + int n; + int nChan = State->nChan; + int BlockSize = State->BlockSize; + float *TransformBuffer = State->TransformBuffer; + float *TransformTemp = State->TransformTemp; + float *TransformInvLap = State->TransformInvLap; + const uint8_t *SrcBuffer = _SrcBuffer; + + //! Begin decoding + int Chan, Size = 0; + int LastSubBlockSize = 0; //! <- Shuts gcc up + int WindowCtrl; { + //! Read window control information + WindowCtrl = Block_Decode_ReadNybble(&SrcBuffer, &Size); + if(WindowCtrl & 0x8) WindowCtrl |= Block_Decode_ReadNybble(&SrcBuffer, &Size) << 4; + else WindowCtrl |= 1 << 4; + } + for(Chan=0;ChanLastSubBlockSize; + + //! Process subblocks + float *Dst = DstData + Chan*BlockSize; + float *Src = TransformBuffer; + float *Lap = TransformInvLap; + ULC_SubBlockDecimationPattern_t DecimationPattern = ULCi_SubBlockDecimationPattern(WindowCtrl); + do { + int SubBlockSize = BlockSize >> (DecimationPattern&0x7); + if(!Block_Decode_DecodeSubBlockCoefs(Src, SubBlockSize, &SrcBuffer, &Size)) { + //! Corrupt block + return 0; + } + + //! Get+update overlap size and limit to that of the last subblock + int OverlapSize = SubBlockSize; + if(DecimationPattern&0x8) + OverlapSize >>= (WindowCtrl & 0x7); + if(OverlapSize > LastSubBlockSize) + OverlapSize = LastSubBlockSize; + LastSubBlockSize = SubBlockSize; + + //! A single long block can be read straight into the output buffer + if(SubBlockSize == BlockSize) { + Fourier_IMDCT(Dst, Src, Lap, TransformTemp, SubBlockSize, OverlapSize); + break; + } + + //! For small blocks, we store the decoded data to a scratch buffer + float *DecBuf = TransformTemp + SubBlockSize; + Fourier_IMDCT(DecBuf, Src, Lap, TransformTemp, SubBlockSize, OverlapSize); + + //! Output samples from the lapping buffer, and cycle + //! the new samples through it for the next call + int nAvailable = (BlockSize - SubBlockSize) / 2; + float *LapDst = Lap + BlockSize/2; + const float *LapSrc = LapDst; + if(SubBlockSize <= nAvailable) { + //! We have enough data in the lapping buffer + //! to output a full subblock directly from it, + //! so we do that and then shift any remaining + //! data before re-filling the buffer. + for(n=0;n>= 4); + + //! Move to next channel + TransformInvLap += BlockSize/2; + } + + //! Undo M/S transform + //! NOTE: Not orthogonal; must be fully normalized on the encoder side. + for(Chan=1;ChanLastSubBlockSize = LastSubBlockSize; + return Size; +} + +/**************************************/ +//! EOF +/**************************************/ diff --git a/tools/audioconv64/libulc/ulcDecoder.h b/tools/audioconv64/libulc/ulcDecoder.h new file mode 100644 index 000000000..01aeab8a1 --- /dev/null +++ b/tools/audioconv64/libulc/ulcDecoder.h @@ -0,0 +1,60 @@ +/**************************************/ +//! ulc-codec: Ultra-Low-Complexity Audio Codec +//! Copyright (C) 2022, Ruben Nunez (Aikku; aik AT aol DOT com DOT au) +//! Refer to the project README file for license terms. +/**************************************/ +#pragma once +/**************************************/ + +//! Decoder state structure +//! NOTE: +//! -The global state data must be set before calling ULC_DecoderState_Init() +//! -{nChan, BlockSize,} must not change after calling ULC_EncoderState_Init() +struct ULC_DecoderState_t { + //! Global state (do not change after initialization) + int nChan; //! Channels in encoding scheme + int BlockSize; //! Transform block size + + //! Decoding state + //! Buffer memory layout: + //! Data: + //! char _Padding[]; + //! float TransformBuffer[BlockSize] + //! float TransformTemp [nChan * BlockSize] + //! float TransformInvLap[nChan * BlockSize/2] + //! BufferData contains the pointer returned by malloc() + //! TransformTemp[] is large because we need to interleave the output. + int LastSubBlockSize; //! Size of last [sub]block processed + void *BufferData; + float *TransformBuffer; + float *TransformTemp; + float *TransformInvLap; +}; + +/**************************************/ + +//! Initialize decoder state +//! On success, returns a non-negative value +//! On failure, returns a negative value +int ULC_DecoderState_Init(struct ULC_DecoderState_t *State); + +//! Destroy decoder state +void ULC_DecoderState_Destroy(struct ULC_DecoderState_t *State); + +/**************************************/ + +//! Decode block +//! NOTE: +//! -Output data will have its channels arranged sequentially; +//! For example: +//! { +//! 0,1,2,3...BlockSize-1, //! Chan0 +//! 0,1,2,3...BlockSize-1, //! Chan1 +//! } +//! -SrcBuffer will only be accessed via bytes. +//! Returns the number of bits read. +int ULC_DecodeBlock(struct ULC_DecoderState_t *State, float *DstData, const void *SrcBuffer); + +/**************************************/ +//! EOF +/**************************************/ diff --git a/tools/audioconv64/libulc/ulcEncoder.c b/tools/audioconv64/libulc/ulcEncoder.c new file mode 100644 index 000000000..a8d09c575 --- /dev/null +++ b/tools/audioconv64/libulc/ulcEncoder.c @@ -0,0 +1,162 @@ +/**************************************/ +//! ulc-codec: Ultra-Low-Complexity Audio Codec +//! Copyright (C) 2024, Ruben Nunez (Aikku; aik AT aol DOT com DOT au) +//! Refer to the project README file for license terms. +/**************************************/ +#include +#include +#include +/**************************************/ +#include "libfourier/Fourier.h" +#include "ulcEncoder.h" +#include "ulcHelper.h" +#include "ulcEncoder_Internals.h" +/**************************************/ +#define BUFFER_ALIGNMENT 64u //! Always align memory to 64-byte boundaries (preparation for AVX-512) +/**************************************/ + +#define MIN_CHANS 1 +#define MAX_CHANS 255 +#define MIN_BANDS 256 //! Limited by the transient detector's decimation +#define MAX_BANDS 32768 +#define MIN_OVERLAP 16 //! Depends on SIMD routines; setting as 16 arbitrarily + +//! Initialize encoder state +int ULC_EncoderState_Init(struct ULC_EncoderState_t *State) { + //! Clear anything that is needed for EncoderState_Destroy() + State->BufferData = NULL; + + //! Verify parameters + int nChan = State->nChan; + int BlockSize = State->BlockSize; + if(nChan < MIN_CHANS || nChan > MAX_CHANS) return -1; + if(BlockSize < MIN_BANDS || BlockSize > MAX_BANDS) return -1; + if((BlockSize & (-BlockSize)) != BlockSize) return -1; + + //! Get buffer offsets and allocation size + //! NOTE: TransformTemp must be able to contain at least two + //! blocks' worth of data (MDCT+MDST coefficients for analysis). + int AllocSize = 0; +#define CREATE_BUFFER(Name, Sz) int Name##_Offs = AllocSize; AllocSize += Sz + CREATE_BUFFER(SampleBuffer, sizeof(float) * (nChan*BlockSize) * 2); + CREATE_BUFFER(TransformBuffer, sizeof(float) * (nChan*BlockSize)); +#if ULC_USE_NOISE_CODING + CREATE_BUFFER(TransformNoise, sizeof(float) * (nChan*BlockSize)); +#endif + CREATE_BUFFER(TransformFwdLap, sizeof(float) * (nChan*BlockSize)); + CREATE_BUFFER(TransformTemp, sizeof(float) * ((nChan + (nChan < 2)) * BlockSize)); + CREATE_BUFFER(TransformIndex, sizeof(int) * (nChan*BlockSize)); + CREATE_BUFFER(TransientBuffer, sizeof(struct ULC_TransientData_t) * ULC_MAX_BLOCK_DECIMATION_FACTOR*2); +#undef CREATE_BUFFER + + //! Allocate buffer space + char *Buf = State->BufferData = malloc(BUFFER_ALIGNMENT-1 + AllocSize); + if(!Buf) return -1; + + //! Initialize pointers + Buf += (-(uintptr_t)Buf) & (BUFFER_ALIGNMENT-1); + State->SampleBuffer = (float*)(Buf + SampleBuffer_Offs); + State->TransformBuffer = (float*)(Buf + TransformBuffer_Offs); +#if ULC_USE_NOISE_CODING + State->TransformNoise = (float*)(Buf + TransformNoise_Offs); +#endif + State->TransformFwdLap = (float*)(Buf + TransformFwdLap_Offs); + State->TransformTemp = (float*)(Buf + TransformTemp_Offs); + State->TransformIndex = (int *)(Buf + TransformIndex_Offs); + State->TransientBuffer = (struct ULC_TransientData_t*)(Buf + TransientBuffer_Offs); + + //! Set initial state + int i; + State->NextWindowCtrl = 0x10; //! No decimation, full overlap. Doesn't really matter, though. + for(i=0;i<3; i++) State->TransientFilter[i] = 0.0f; + for(i=0;iSampleBuffer [i] = 0.0f; + for(i=0;iTransformFwdLap[i] = 0.0f; + for(i=0;iTransientBuffer[i] = (struct ULC_TransientData_t){.Sum = 0.0f, .SumW = 0.0f}; + } + + //! Success + return 1; +} + +/**************************************/ + +//! Destroy encoder state +void ULC_EncoderState_Destroy(struct ULC_EncoderState_t *State) { + //! Free buffer space + free(State->BufferData); +} + +/**************************************/ + +//! Encode block (CBR mode) +int ULC_EncodeBlock_CBR_Core(struct ULC_EncoderState_t *State, void *DstBuffer, float RateKbps, int MaxCoef) { + int Size; + int nOutCoef = -1; + int BitBudget = (int)((State->BlockSize * RateKbps) * 1000.0f/State->RateHz); //! NOTE: Truncate + + //! Perform a binary search for the optimal nOutCoef + int Lo = 0, Hi = MaxCoef; + if(Lo < Hi) do { + nOutCoef = (Lo + Hi) / 2u; + Size = ULCi_EncodePass(State, DstBuffer, nOutCoef); + if(Size < BitBudget) Lo = nOutCoef; + else if(Size > BitBudget) Hi = nOutCoef-1; + else { + //! Should very, VERY rarely happen, but just in case + Lo = nOutCoef; + break; + } + } while(Lo < Hi-1); + + //! Avoid going over budget + int nOutCoefFinal = Lo; + if(nOutCoefFinal != nOutCoef) Size = ULCi_EncodePass(State, DstBuffer, nOutCoef = nOutCoefFinal); + return Size; +} +const void *ULC_EncodeBlock_CBR(struct ULC_EncoderState_t *State, const float *SrcData, int *Size, float RateKbps) { + void *Buf = (void*)State->TransformTemp; + int MaxCoef = ULCi_TransformBlock(State, SrcData); + int Sz = ULC_EncodeBlock_CBR_Core(State, Buf, RateKbps, MaxCoef); + if(Size) *Size = Sz; + return Buf; +} + +/**************************************/ + +//! Encode block (ABR mode) +const void *ULC_EncodeBlock_ABR(struct ULC_EncoderState_t *State, const float *SrcData, int *Size, float RateKbps, float AvgComplexity) { + void *Buf = (void*)State->TransformTemp; + int MaxCoef = ULCi_TransformBlock(State, SrcData); + float TargetKbps = RateKbps * State->BlockComplexity / AvgComplexity; + int Sz = ULC_EncodeBlock_CBR_Core(State, Buf, TargetKbps, MaxCoef); + if(Size) *Size = Sz; + return Buf; +} + +/**************************************/ + +//! Encode block (VBR mode) +const void *ULC_EncodeBlock_VBR(struct ULC_EncoderState_t *State, const float *SrcData, int *Size, float Quality) { + //! NOTE: The constant in front of the logarithm was experimentally + //! dervied; I have no idea what relation it bears to actual encoding. + void *Buf = (void*)State->TransformTemp; + float TargetComplexity = 0x1.E4EFB7p3f*logf(100.0f / Quality); //! 0x1.E4EFB7p3 = E^E. This seems to closely match ABR mode's peak rates + int MaxCoef = ULCi_TransformBlock(State, SrcData); + int nTargetCoef = MaxCoef; { + //! TargetComplexity == 0 which would result in a + //! divide-by-zero error. So instead we just leave + //! nTargetCoef alone at its maximum value. + if(TargetComplexity > 0.0f) { + float fTarget = (State->nChan*State->BlockSize) * State->BlockComplexity / TargetComplexity; + if(fTarget < MaxCoef) nTargetCoef = (int)fTarget; + } + } + int Sz = ULCi_EncodePass(State, Buf, nTargetCoef); + if(Size) *Size = Sz; + return Buf; +} + +/**************************************/ +//! EOF +/**************************************/ diff --git a/tools/audioconv64/libulc/ulcEncoder.h b/tools/audioconv64/libulc/ulcEncoder.h new file mode 100644 index 000000000..3b7b44af0 --- /dev/null +++ b/tools/audioconv64/libulc/ulcEncoder.h @@ -0,0 +1,140 @@ +/**************************************/ +//! ulc-codec: Ultra-Low-Complexity Audio Codec +//! Copyright (C) 2023, Ruben Nunez (Aikku; aik AT aol DOT com DOT au) +//! Refer to the project README file for license terms. +/**************************************/ +#pragma once +/**************************************/ + +//! 0 == No psychoacoustic optimizations +//! 1 == Use psychoacoustic model +#define ULC_USE_PSYCHOACOUSTICS 1 + +//! 0 == No noise-fill coding +//! 1 == Use noise-fill where useful +#define ULC_USE_NOISE_CODING 1 + +//! 0 == No window switching +//! 1 == Use window switching +#define ULC_USE_WINDOW_SWITCHING 1 + +//! Maximum number of subblocks present in a block +#if ULC_USE_WINDOW_SWITCHING +# define ULC_MAX_SUBBLOCKS 4 +#else +# define ULC_MAX_SUBBLOCKS 1 +#endif + +//! Maximum allowed block decimation factor +#if ULC_USE_WINDOW_SWITCHING +# define ULC_MAX_BLOCK_DECIMATION_FACTOR 8 +#else +# define ULC_MAX_BLOCK_DECIMATION_FACTOR 1 +#endif + +//! Smallest possible coefficient amplitude +#define ULC_COEF_EPS (0x1.0p-31f) //! 5+0xE+0xC = Maximum extended-precision quantizer + +/**************************************/ + +//! Encoder state structure +//! NOTE: +//! -The global state data must be set before calling ULC_EncoderState_Init() +//! -{RateHz, nChan, BlockSize, ModulationWindow} must not change after calling ULC_EncoderState_Init() +struct ULC_TransientData_t { + float Sum, SumW; +}; +struct ULC_EncoderState_t { + //! Global state (do not change after initialization) + int RateHz; //! Playback rate (used for rate control) + int nChan; //! Channels in encoding scheme + int BlockSize; //! Transform block size + + //! Encoding state + //! Buffer memory layout: + //! char _Padding[]; + //! float SampleBuffer [nChan*BlockSize * 2] + //! float TransformBuffer[nChan*BlockSize] + //! float TransformNoise [nChan*BlockSize] <- With ULC_USE_NOISE_CODING only + //! float TransformFwdLap[nChan*BlockSize] + //! float TransformTemp [MAX(2,nChan)*BlockSize] + //! int TransformIndex [nChan*BlockSize] + //! ULC_TransientData_t TransientBuffer[ULC_MAX_BLOCK_DECIMATION_FACTOR*2] + //! BufferData contains the original pointer returned by malloc() + int WindowCtrl; //! Window control parameter (for last coded block) + int NextWindowCtrl; //! Window control parameter (for data in SampleBuffer) + float BlockComplexity; //! Coefficient distribution complexity (0 = Highly tonal, 1 = Highly noisy) + float TransientFilter[3]; + void *BufferData; + float *SampleBuffer; + float *TransformBuffer; +#if ULC_USE_NOISE_CODING + float *TransformNoise; +#endif + float *TransformFwdLap; + float *TransformTemp; + int *TransformIndex; + struct ULC_TransientData_t *TransientBuffer; +}; + +/**************************************/ + +//! Initialize encoder state +//! On success, returns a non-negative value +//! On failure, returns a negative value +int ULC_EncoderState_Init(struct ULC_EncoderState_t *State); + +//! Destroy encoder state +void ULC_EncoderState_Destroy(struct ULC_EncoderState_t *State); + +/**************************************/ + +//! Encode block +//! NOTE: +//! -Input data must have its channels interleaved; +//! For example: +//! { +//! Chan0Sample0, Chan1Sample0, Chan0Sample1, Chan1Sample1, ... +//! } +//! Notes regarding coding modes: +//! -CBR will encode as many coefficients as possible for a given +//! RateKbps, never exceeding this value (for example, if 128.0kbps +//! is the target and the encoding choice is between 127.0kbps and +//! 128.01kbps, 127.0kbps will always be chosen). +//! The rate is matched via binary search, and so this encoding +//! mode (and ABR, which uses the same mechanism) is the slowest. +//! -ABR mode tries to balance the number of coefficients in each +//! block based on their complexity. It will achieve an average +//! bitrate very close to the target, but may be slightly off due +//! to rounding errors. +//! To run in ABR mode, the file must first be analyzed to get the +//! average complexity, and then this is passed to the routine +//! alongside the desired RateKbps (note that AvgComplexity can be +//! passed arbitrarily without a pre-pass, but the target bitrate +//! might not be achieved). +//! This encoding mode is just as slow as CBR mode, as the algorithm +//! chooses a target bitrate for each block, which must go through +//! the CBR encoding routine for each block. +//! -VBR mode attempts to estimate the average complexity of the file +//! based on a Quality parameter, and then proceeds to encode the +//! file based on this metric alone. This mode is the fastest, but +//! there is no guarantee for the bitrate. +//! Generally, though: +//! 0 < Quality <= 10 = Average <30kbps +//! 10 < Quality <= 20 = Average <40kbps +//! 20 < Quality <= 30 = Average <50kbps +//! 30 < Quality <= 40 = Average <60kbps +//! 40 < Quality <= 50 = Average <75kbps +//! 50 < Quality <= 60 = Average <95kbps +//! 60 < Quality <= 70 = Average <125kbps +//! 70 < Quality <= 80 = Average <175kbps +//! 80 < Quality <= 90 = Average <300kbps +//! Returns a pointer to the compressed data, and the block size in +//! bits in Size (if NULL, size is not returned). +const void *ULC_EncodeBlock_CBR(struct ULC_EncoderState_t *State, const float *SrcData, int *Size, float RateKbps); +const void *ULC_EncodeBlock_ABR(struct ULC_EncoderState_t *State, const float *SrcData, int *Size, float RateKbps, float AvgComplexity); +const void *ULC_EncodeBlock_VBR(struct ULC_EncoderState_t *State, const float *SrcData, int *Size, float Quality); + +/**************************************/ +//! EOF +/**************************************/ diff --git a/tools/audioconv64/libulc/ulcEncoder_BlockTransform.c b/tools/audioconv64/libulc/ulcEncoder_BlockTransform.c new file mode 100644 index 000000000..ab1b3c2a0 --- /dev/null +++ b/tools/audioconv64/libulc/ulcEncoder_BlockTransform.c @@ -0,0 +1,360 @@ +/**************************************/ +//! ulc-codec: Ultra-Low-Complexity Audio Codec +//! Copyright (C) 2025, Ruben Nunez (Aikku; aik AT aol DOT com DOT au) +//! Refer to the project README file for license terms. +/**************************************/ +#include +#include +#include +/**************************************/ +#include "libfourier/Fourier.h" +#include "ulcHelper.h" +#include "ulcEncoder_Internals.h" +/**************************************/ + +//! Implementation for heapsort +//! This is used because qsort() tends to use quicksort, which +//! has some nasty pathological cases, so I'd rather have a +//! sort method that has more consistent timing, even if it's +//! very slightly slower for the average case. +static void Heapsort_SiftDown(const float *SortValues, int *Order, int Root, int N) { + //! NOTE: Most of the sorting time is spent in this function, + //! so I've tried to optimize it as best as I could. The + //! code below is what performed best on my Intel i7 CPU, + //! so mileage may vary. There may exist other variations + //! that have even better performance, but this is good + //! enough that it probably doesn't need it anyway. + int Child = 2*Root+1; + if(Child < N) for(;;) { + //! Get the values of the root and child + //! NOTE: If a sibling exists, check it also. + //! NOTE: Using pre-increment (combined with post-decrement + //! for the FALSE case) appears to result in better performance? + int RootIdx = Order[Root]; + int ChildIdx = Order[Child]; + if(++Child < N && SortValues[Order[Child]] < SortValues[ChildIdx]) + ChildIdx = Order[Child]; + else Child--; + + //! Already in order? Exit + if(SortValues[ChildIdx] > SortValues[RootIdx]) return; + + //! Swap to restore heap order, and travel further down the heap + //! NOTE: Stashing Order[Root],Order[Child] in temps appears to + //! give slightly worse performance. This may be an x86 quirk + //! due to its low register count, though. + Order[Root] = ChildIdx; + Order[Child] = RootIdx; + Root = Child, Child = 2*Root+1; + if(Child >= N) return; + } +} +static void SortIndices(int *SortedIndices, const float *SortValues, int *Temp, int N) { + int n; + + //! Start with mapping the indices directly + int *Order = Temp; + for(n=0;n= 0); + + //! Pop all elements off the heap one at a time + n = N-1; + do { + SortedIndices[Order[0]] = n; + Order[0] = Order[n]; + Heapsort_SiftDown(SortValues, Order, 0, n); + } while(--n); + SortedIndices[Order[0]] = n; + } +} + +/**************************************/ + +//! Transform a block and prepare its coefficients +int ULCi_TransformBlock(struct ULC_EncoderState_t *State, const float *Data) { + int nChan = State->nChan; + int BlockSize = State->BlockSize; + + //! Append new data samples + { + int n, Chan; + float *Old = State->SampleBuffer; + float *New = State->SampleBuffer + BlockSize*nChan; + + //! Shift last block + for(n=0;nWindowCtrl = State->NextWindowCtrl; + int NextWindowCtrl = State->NextWindowCtrl = ULCi_GetWindowCtrl( + State->SampleBuffer, + State->TransientBuffer, + State->TransientFilter, + State->TransformTemp, + BlockSize, + nChan, + State->RateHz + ); + int NextBlockOverlap; { + int Pattern = ULCi_SubBlockDecimationPattern(NextWindowCtrl); + NextBlockOverlap = BlockSize >> (Pattern&0x7); + if(Pattern&0x8) NextBlockOverlap >>= (NextWindowCtrl&0x7); + } + + //! Transform channels and insert keys for each codeable coefficient + //! It's not /strictly/ required to calculate nNzCoef, but it can + //! speed things up in the rate-control step + int nNzCoef = 0; { + int n, Chan; + float *BufferSamples = State->SampleBuffer; + float *BufferMDCT = State->TransformBuffer; + float *BufferIndex = (float*)State->TransformIndex; + float *BufferFwdLap = State->TransformFwdLap; +#if ULC_USE_NOISE_CODING + float *BufferNoise = State->TransformNoise; + + //! Clear the noise buffer; we'll be adding two MDCT/MDST lines into one pseudo-DFT line here + //! and then split it out into {Weight,Weight*LogNoiseLevel} pairs + for(n=0;nTransformTemp; +#if ULC_USE_PSYCHOACOUSTICS + float *MaskingNp = BufferSamples; //! NOTE: Aliasing of BufferSamples + float *BufferAmp2 = BufferTemp + BlockSize; //! NOTE: Using upper half of BufferTemp + + //! Clear the amplitude buffer; we'll be accumulating all channels here + for(n=0;n> (DecimationPattern&0x7); + int OverlapSize; { + //! If we're still in the same block, poll the next subblock's + //! size. Otherwise, use the next block's first [sub]block + DecimationPattern >>= 4; + if(DecimationPattern) { + OverlapSize = BlockSize >> (DecimationPattern&0x7); + if(DecimationPattern&0x8) OverlapSize >>= (WindowCtrl&0x7); + } else OverlapSize = NextBlockOverlap; + + //! Limit overlap to the maximum allowed by this subblock + if(OverlapSize > SubBlockSize) OverlapSize = SubBlockSize; + } + + //! Cycle data through the lapping buffer + float *SmpBuf = BufferTemp; { + /*! | . | .____________| + | . |/. | + | . | . | + | . | . | + |____________./| . | + | . | . | + <- L -><-M-><- R -> + <-BlockSize/2->|<-BlockSize/2-> + <- BlockSize -> + + L_Size = (BlockSize - SubBlockSize)/2 + M_Size = SubBlockSize + R_Size = (BlockSize - SubBlockSize)/2 + L_Offs = 0 + M_Offs = (BlockSize - SubBlockSize)/2 + R_Offs = (BlockSize + SubBlockSize)/2 + + The L segment contains all 0s. + The M segment contains our lapping data. + The R segment contains data that we must transform. + + Therefore: + When using Fourier_MDCT_MDST(), we must align BufferFwdLap + with the M segment, and cycle data through the R segment. + !*/ + + //! Do we have the full [sub]block in the R side of the lapping buffer? + int nAvailable = (BlockSize-SubBlockSize)/2; + float *SmpDst = SmpBuf; + const float *SmpSrc = BufferSamples; + float *LapDst = BufferFwdLap + (BlockSize+SubBlockSize)/2; + const float *LapSrc = LapDst; + if(nAvailable < SubBlockSize) { + //! Don't have enough samples in lapping buffer + //! for the full block - stream new data in and + //! refill the lapping buffer + for(n=0;nRateHz); +#endif + //! Move to the next subblock + BufferSamples += SubBlockSize; + BufferMDCT += SubBlockSize; + BufferIndex += SubBlockSize; +#if ULC_USE_PSYCHOACOUSTICS + BufferAmp2 += SubBlockSize/2; +#endif +#if ULC_USE_NOISE_CODING + BufferNoise += SubBlockSize; +#endif + } while(DecimationPattern); + + //! Move to the next channel + BufferFwdLap += BlockSize; +#if ULC_USE_PSYCHOACOUSTICS + BufferAmp2 -= BlockSize/2; //! <- Accumulated across all channels - rewind +#endif + } + BufferMDCT -= BlockSize*nChan; //! Rewind to start of buffer + BufferIndex -= BlockSize*nChan; + + //! Finalize and store block complexity + if(Complexity) { + //! Based off the same principles of normalized entropy: + //! Entropy = (Log[Total[x]] - Total[x*Log[x]]/Total[x]) / Log[N] + //! Instead of accumulating log values, we accumulate + //! raw values, meaning we need to take the log: + //! Total[x*Log[x]]/Total[x] -> Log[Total[x*x]/Total[x]] + //! Simplifying: + //! (Log[Total[x]] - Log[Total[x*x]/Total[x]]) / Log[N] + //! =(Log[Total[x] / (Total[x*x]/Total[x])) / Log[N] + //! =Log[Total[x]^2 / Total[x^2]] / Log[N] + float ComplexityScale = 0x1.62E430p-1f*(31 - __builtin_clz(BlockSize)); //! 0x1.62E430p-1 = 1/Log2[E] for change-of-base + Complexity = logf(SQR(ComplexityW) / Complexity) / ComplexityScale; + if(Complexity < 0.0f) Complexity = 0.0f; //! In case of round-off error + if(Complexity > 1.0f) Complexity = 1.0f; + } + State->BlockComplexity = Complexity; +#if ULC_USE_PSYCHOACOUSTICS + //! Perform psychoacoustics analysis + //! NOTE: Trashes BufferAmp2[] + ULCi_CalculatePsychoacoustics(MaskingNp, BufferAmp2, BufferTemp, BlockSize, State->RateHz, WindowCtrl); + + //! Add the psychoacoustics adjustment to the importance levels + //! NOTE: No need to split this section into subblock handling. + //! All the coefficients and their levels are in order relative + //! to that of the output of psychoacoustics. + //! NOTE: Because we stored out-of-range values as -INFINITY, + //! we can do simple arithmetic on them without affecting things. + for(Chan=0;ChanTransformTemp; + int *BufferIdx = State->TransformIndex; + SortIndices(BufferIdx, (float*)BufferIdx, BufferTmp, nChan * BlockSize); + } + return nNzCoef; +} + +/**************************************/ +//! EOF +/**************************************/ diff --git a/tools/audioconv64/libulc/ulcEncoder_Encode.c b/tools/audioconv64/libulc/ulcEncoder_Encode.c new file mode 100644 index 000000000..642e1b257 --- /dev/null +++ b/tools/audioconv64/libulc/ulcEncoder_Encode.c @@ -0,0 +1,364 @@ +/**************************************/ +//! ulc-codec: Ultra-Low-Complexity Audio Codec +//! Copyright (C) 2024, Ruben Nunez (Aikku; aik AT aol DOT com DOT au) +//! Refer to the project README file for license terms. +/**************************************/ +#include +#include +/**************************************/ +#include "libfourier/Fourier.h" +#include "ulcEncoder.h" +#include "ulcHelper.h" +#include "ulcEncoder_Internals.h" +/**************************************/ + +//! The target memory is always aligned to 64 bytes, so just +//! use whatever is most performant on the target architecture +typedef uint8_t BitStream_t; //! <- MUST BE UNSIGNED +#define BITSTREAM_NBITS (8u*sizeof(BitStream_t)) + +/**************************************/ + +//! Write nybble to output +static void WriteNybble(BitStream_t x, BitStream_t *Dst, int *Size) { + //! Push nybble + BitStream_t *p = &Dst[*Size / BITSTREAM_NBITS]; + *p >>= 4; + *p |= x << (BITSTREAM_NBITS - 4); + *Size += 4; +} + +//! Write quantizer to output +static void WriteQuantizer(int qi, BitStream_t *DstBuffer, int *Size, int Lead) { + int s = qi - 5; + if(Lead) { + WriteNybble(0xF, DstBuffer, Size); + } + if(s < 0xE) { + //! Fh,0h..Dh: Quantizer change + WriteNybble(s, DstBuffer, Size); + } else { + //! Fh,Eh,0h..Ch: Quantizer change (extended precision) + WriteNybble( 0xE, DstBuffer, Size); + WriteNybble(s-0xE, DstBuffer, Size); + } +} + +/**************************************/ + +//! Build quantizer from weighted average +static int BuildQuantizer(float MaxVal) { + //! NOTE: `MaxVal` approaches (4/Pi)^2 as BlockSize + //! approaches infinity, due to the p=1 norm of + //! the MDCT matrix. Therefore, we use a bias of 5 + //! in the syntax to allow for a full range (that is + //! to say: 7^2 * 2^-5 = 1.53125, 1.53125 >= 4/Pi). + //! As for rounding, we want to minimize the error + //! of e = |2^-q - MaxVal|: + //! We start with qm = IntegerPart[-Log2[MaxVal]]. + //! We then choose q = qm, or qm+1, whichever has + //! smaller error. + //! Some identities: + //! MaxVal <= 2^-qm + //! MaxVal >= 2^-(qm+1) + //! Now we can say: + //! q = qm, |2^-qm - MaxVal| < |2^-(qm+1) - MaxVal| + //! qm+1, |2^-(qm+1) - MaxVal| < |2^-qm - MaxVal| + //! In other words, we add 1 to qm if the latter + //! condition is met. Now simplification: + //! |2^-(qm+1) - MaxVal| < |2^-qm - MaxVal| + //! We know that 2^-(qm+1) <= MaxVal, so we can remove + //! the first absolute after negation: + //! (MaxVal - 2^-(qm+1)) < |2^-qm - MaxVal| + //! And we know that 2^-qm <= MaxVal: + //! (MaxVal - 2^-(qm+1)) < (2^-qm - MaxVal) + //! Now some algebraic manipulation leads to: + //! 2^-qm/MaxVal > (4/3) + //! Putting it all together, we can finally state: + //! q = IntegerPart[-Log2[MaxVal*2/3]] + //! And since we have a bias of 5: + //! q = IntegerPart[5 - Log2[MaxVal*2/3]] + //! = IntegerPart[5 - (Log2[MaxVal] + Log2[2/3])] + //! = IntegerPart[(5 - Log2[2/3]) - Log2[MaxVal]] + int q = (int)(0x1.657006p2f + -0x1.715476p0f*logf(MaxVal)); //! 0x1.657006p2 = 5-Log2[2/3], 0x1.715476p0 == 1/Ln[2] for change of base + if(q < 5) q = 5; + if(q > 5 + 0xE + 0xC) q = 5 + 0xE + 0xC; //! 5+Eh+Ch = Maximum extended-precision quantizer value (including a bias of 5) + return q; +} + +/**************************************/ + +//! Encode a range of coefficients +static int WriteQuantizerZone( + int CurIdx, + int EndIdx, + float Quant, + const float *Coef, +#if ULC_USE_NOISE_CODING + const float *CoefNoise, +#endif + const int *CoefIdx, + int NextCodedIdx, + int nOutCoef, + BitStream_t *DstBuffer, + int *Size +) { + for(;;) { + //! Seek the next viable coefficient + while(CurIdx < EndIdx && CoefIdx[CurIdx] >= nOutCoef) CurIdx++; + if(CurIdx >= EndIdx) break; + + //! If the coefficient collapses to become uncodeable, skip it + //! Note that the expression we test here is the exact expansion + //! of ULCi_CompandedQuantizeCoefficient(Coef[CurIdx]*Quant) < 2. + if(ABS(Coef[CurIdx]*Quant) < 2.5f) { CurIdx++; continue; } + + //! We now know the coefficient can be coded, so write out the zeros/noise run(s). + int n, v; + int zR = CurIdx - NextCodedIdx; + while(zR) { + //! If we plan to skip two or less coefficients, try to encode + //! them instead, as this is cheaper+better than filling with zero + if(zR <= 2) { + int Qn1, Qn2; + if(1) Qn1 = ULCi_CompandedQuantizeCoefficient(Coef[NextCodedIdx+0]*Quant, 0x7); + if(zR >= 2) Qn2 = ULCi_CompandedQuantizeCoefficient(Coef[NextCodedIdx+1]*Quant, 0x7); + if(ABS(Qn1) > 1 && (zR < 2 || ABS(Qn2) > 1)) { + if(1) WriteNybble(Qn1, DstBuffer, Size); + if(zR >= 2) WriteNybble(Qn2, DstBuffer, Size); + NextCodedIdx += zR; + break; + } + } +#if ULC_USE_NOISE_CODING + //! Determine the quantized coefficient for noise-fill mode + //! NOTE: The range of noise samples is coded as a trade-off + //! between noise-fill commands and coded coefficients, as + //! noise-fill competes with coefficients we /want/ to code + //! for the bit bandwidth available. Setting the start range + //! too low favours noise-fill over psychoacoustically + //! significant coefficients, but setting it too high can + //! cause degradation in small block sizes as well as when + //! we get many short runs. + //! TODO: Is there a better min-number-of-coefficients? + //! Too low and this fights too much with the coefficients + //! we are actually trying to code (reducing the coding + //! rate of them, and increasing the coding rate of noise). + //! However, with small BlockSize (or SubBlockSize when + //! decimating), smaller runs are useful. + int NoiseQ = 0; + if(zR >= 16) { + v = zR - 16; if(v > 0x01FF) v = 0x01FF; + n = v + 16; + NoiseQ = ULCi_GetNoiseQ(CoefNoise, NextCodedIdx, n, Quant); + } + if(NoiseQ) { + //! 8h,Zh,Yh,Xh: 16 .. 527 noise fill (Xh != 0) + WriteNybble(0x8, DstBuffer, Size); + WriteNybble(v>>5, DstBuffer, Size); + WriteNybble(v>>1, DstBuffer, Size); + WriteNybble((v&1) | ((NoiseQ-1)<<1), DstBuffer, Size); + } else { +#endif + //! Determine which run type to use and get the number of zeros coded + //! NOTE: A short run takes 2 nybbles, and a long run takes 4 nybbles. + //! So two short runs of maximum length code up to 32 zeros with the + //! same efficiency as a long run, meaning that long runs start with + //! 33 zeros. + if(zR < 33) { + //! 0h,0h..Fh: Zeros fill (1 .. 16 coefficients) + v = zR - 1; if(v > 0xF) v = 0xF; + n = v + 1; + WriteNybble(0x0, DstBuffer, Size); + WriteNybble(v, DstBuffer, Size); + } else { + //! 1h,Yh,Xh: 33 .. 542 zeros fill (Xh == 0) + v = zR - 33; if(v > 0xFF) v = 0xFF; + n = v + 33; + WriteNybble(0x1, DstBuffer, Size); + WriteNybble(v>>4, DstBuffer, Size); + WriteNybble(v>>0, DstBuffer, Size); + } +#if ULC_USE_NOISE_CODING + } +#endif + //! Skip the zeros + NextCodedIdx += n; + zR -= n; + } + + //! -7h..-2h, +2h..+7h: Normal coefficient + int Qn = ULCi_CompandedQuantizeCoefficient(Coef[CurIdx]*Quant, 0x7); + WriteNybble(Qn, DstBuffer, Size); + NextCodedIdx++; + CurIdx++; + } + return NextCodedIdx; +} + +//! Encode a [sub]block +static void WriteSubBlock( + int Idx, + int SubBlockSize, + const float *Coef, +#if ULC_USE_NOISE_CODING + const float *CoefNoise, +#endif + const int *CoefIdx, + int nOutCoef, + BitStream_t *DstBuffer, + int *Size +) { + //! Encode direct coefficients + int EndIdx = Idx+SubBlockSize; + int NextCodedIdx = Idx; + int PrevQuant = -1; + int QuantStartIdx = -1; + float QuantMin = 1000.0f, QuantMax = -1000.0f; + do { + //! Seek the next coefficient + while(Idx < EndIdx && CoefIdx[Idx] >= nOutCoef) Idx++; + + //! Read coefficient and set the first quantizer's first coefficient index + //! NOTE: Set NewMin=0.0 upon reaching the end. This causes the range + //! check to fail in the next step, causing the final quantizer zone to dump + //! if we had any data (if we don't, the check "passes" because NewMax==0). + float NewMin = 0.0f; + float NewMax = QuantMax; + float CurLevel = 0.0f; + if(Idx < EndIdx) { + CurLevel = ABS(Coef[Idx]); + NewMin = (CurLevel < QuantMin) ? CurLevel : QuantMin; + NewMax = (CurLevel > QuantMax) ? CurLevel : QuantMax; + if(QuantStartIdx == -1) QuantStartIdx = Idx; + } + + //! Level out of range in this quantizer zone? + const float MaxRange = 4.0f; + if(NewMax > NewMin*MaxRange) { + //! Write/update the quantizer + int qi = BuildQuantizer(QuantMax); + if(qi != PrevQuant) { + WriteQuantizer(qi, DstBuffer, Size, PrevQuant != -1); + PrevQuant = qi; + } + + //! Write the quantizer zone we just searched through + //! and start a new one from this coefficient + NextCodedIdx = WriteQuantizerZone( + QuantStartIdx, + Idx, + (float)(1u << qi), + Coef, +#if ULC_USE_NOISE_CODING + CoefNoise, +#endif + CoefIdx, + NextCodedIdx, + nOutCoef, + DstBuffer, + Size + ); + QuantStartIdx = Idx; + QuantMin = QuantMax = CurLevel; + } else { + //! Update ranges + QuantMin = NewMin; + QuantMax = NewMax; + } + } while(++Idx <= EndIdx); + + //! Decide what to do about the tail coefficients + //! If we're at the edge of the block, it might work better to just fill with 0h + int n = EndIdx - NextCodedIdx; + if(n > 4) { + //! If we coded anything, then we must specify the lead sequence + if(PrevQuant != -1) { + WriteNybble(0xF, DstBuffer, Size); + } + + //! Analyze the remaining data for noise-fill mode +#if ULC_USE_NOISE_CODING + int NoiseQ = 0, NoiseDecay = 0; + if(PrevQuant != -1 && n >= 16) { //! Don't use noise-fill for ultra-short tails + ULCi_GetHFExtParams( + CoefNoise, + NextCodedIdx, + n, + (float)(1u << PrevQuant), + &NoiseQ, + &NoiseDecay + ); + } + if(NoiseQ) { + //! Fh,Fh,Zh,Yh,Xh: Noise fill (to end; exp-decay) + WriteNybble(0xF, DstBuffer, Size); + WriteNybble(NoiseQ-1, DstBuffer, Size); + WriteNybble(NoiseDecay>>4, DstBuffer, Size); + WriteNybble(NoiseDecay, DstBuffer, Size); + } else { +#endif + //! Fh,Eh,Fh: Stop + WriteNybble(0xE, DstBuffer, Size); + WriteNybble(0xF, DstBuffer, Size); +#if ULC_USE_NOISE_CODING + } +#endif + } else if(n > 0) { + //! If we have less than 4 coefficients, it's cheaper to + //! store a zero run than to do anything else. + WriteNybble(0x0, DstBuffer, Size); + WriteNybble(n-1, DstBuffer, Size); + } +} + +/**************************************/ + +//! Encode block to target buffer +//! Returns the block size (in bits) and the number of coded (non-zero) coefficients +int ULCi_EncodePass(const struct ULC_EncoderState_t *State, void *_DstBuffer, int nOutCoef) { + int BlockSize = State->BlockSize; + int Chan, nChan = State->nChan; + const float *Coef = State->TransformBuffer; +#if ULC_USE_NOISE_CODING + const float *CoefNoise = State->TransformNoise; +#endif + const int *CoefIdx = State->TransformIndex; + BitStream_t *DstBuffer = _DstBuffer; + + //! Begin coding + int Idx = 0; + int Size = 0; //! Block size (in bits) + int WindowCtrl = State->WindowCtrl; { + WriteNybble(WindowCtrl, DstBuffer, &Size); + if(WindowCtrl & 0x8) WriteNybble(WindowCtrl >> 4, DstBuffer, &Size); + } + for(Chan=0;Chan> (DecimationPattern&0x7); + WriteSubBlock( + Idx, + SubBlockSize, + Coef, +#if ULC_USE_NOISE_CODING + CoefNoise, +#endif + CoefIdx, + nOutCoef, + DstBuffer, + &Size + ); + Idx += SubBlockSize; + } while(DecimationPattern >>= 4); + } + + //! Align the output stream and pad size to bytes + DstBuffer[Size/BITSTREAM_NBITS] >>= (-Size) % BITSTREAM_NBITS; + Size = (Size+7) &~ 7; + return Size; +} + +/**************************************/ +//! EOF +/**************************************/ diff --git a/tools/audioconv64/libulc/ulcEncoder_Internals.h b/tools/audioconv64/libulc/ulcEncoder_Internals.h new file mode 100644 index 000000000..0a3da6d99 --- /dev/null +++ b/tools/audioconv64/libulc/ulcEncoder_Internals.h @@ -0,0 +1,88 @@ +/**************************************/ +//! ulc-codec: Ultra-Low-Complexity Audio Codec +//! Copyright (C) 2024, Ruben Nunez (Aikku; aik AT aol DOT com DOT au) +//! Refer to the project README file for license terms. +/**************************************/ +#pragma once +/**************************************/ +#include "ulcEncoder.h" +/**************************************/ + +//! Get optimal log base-2 overlap and window scalings for transients +//! Bit codes for transient region coding, and their window sizes: +//! First nybble: +//! 0xxx: No decimation. xxx = Overlap scaling +//! 1xxx: Decimate. xxx = Overlap scaling for the transient subblock +//! Second nybble (when first nybble is 1xxx; otherwise, this is implicitly 0001): +//! 1xxx: Decimation by 1/8: Position = 0~7 +//! 1000: N/8*,N/8,N/4,N/2 +//! 1001: N/8,N/8*,N/4,N/2 +//! 1010: N/4,N/8*,N/8,N/2 +//! 1011: N/4,N/8,N/8*,N/2 +//! 1100: N/2,N/8*,N/8,N/4 +//! 1101: N/2,N/8,N/8*,N/4 +//! 1110: N/2,N/4,N/8*,N/8 +//! 1111: N/2,N/4,N/8,N/8* +//! 01xx: Decimation by 1/4: Position = 0~3 +//! 0100: N/4*,N/4,N/2 +//! 0101: N/4,N/4*,N/2 +//! 0110: N/2,N/4*,N/4 +//! 0111: N/2,N/4,N/4* +//! 001x: Decimation by 1/2: Position = 0~1 +//! 0010: N/2*,N/2 +//! 0011: N/2,N/2* +//! 0001: No decimation (not coded in the bitstream) +//! 0001: N/1* +//! The starred subblocks set overlap scaling with regards to the last [sub]block. +int ULCi_GetWindowCtrl( + const float *BlockData, + struct ULC_TransientData_t *TransientBuffer, + float *TransientFilter, + float *TmpBuffer, + int BlockSize, + int nChan, + int RateHz +); + +//! Transform a block and prepare its coefficients +int ULCi_TransformBlock(struct ULC_EncoderState_t *State, const float *Data); + +//! Encode block to target buffer +//! Returns the block size (in bits) and the number of coded (non-zero) coefficients +int ULCi_EncodePass(const struct ULC_EncoderState_t *State, void *_DstBuffer, int nOutCoef); + +/**************************************/ +#if ULC_USE_PSYCHOACOUSTICS +/**************************************/ + +//! Calculate masking levels for each frequency line +void ULCi_CalculatePsychoacoustics( + float *MaskingNp, + float *BufferAmp2, + void *BufferTemp, + int BlockSize, + int RateHz, + uint32_t WindowCtrl +); + +/**************************************/ +#endif +/**************************************/ +#if ULC_USE_NOISE_CODING +/**************************************/ + +//! Compute noise spectrum (logarithmic output) +//! NOTE: The output data is set to pairs of {Weight,Weight*Log[Level]}. +void ULCi_CalculateNoiseLogSpectrum(float *Data, void *Temp, int N, int RateHz); + +//! Get the quantized noise amplitude for encoding +int ULCi_GetNoiseQ(const float *Data, int Band, int N, float q); + +//! Compute quantized HF extension parameters for encoding +void ULCi_GetHFExtParams(const float *Data, int Band, int N, float q, int *_NoiseQ, int *_NoiseDecay); + +/**************************************/ +#endif +/**************************************/ +//! EOF +/**************************************/ diff --git a/tools/audioconv64/libulc/ulcEncoder_NoiseFill.c b/tools/audioconv64/libulc/ulcEncoder_NoiseFill.c new file mode 100644 index 000000000..83b91cf05 --- /dev/null +++ b/tools/audioconv64/libulc/ulcEncoder_NoiseFill.c @@ -0,0 +1,100 @@ +/**************************************/ +//! ulc-codec: Ultra-Low-Complexity Audio Codec +//! Copyright (C) 2025, Ruben Nunez (Aikku; aik AT aol DOT com DOT au) +//! Refer to the project README file for license terms. +/**************************************/ +#include +/**************************************/ +#include "ulcEncoder.h" +#include "ulcHelper.h" +/**************************************/ +#if ULC_USE_NOISE_CODING +/**************************************/ + +//! Get the quantized noise amplitude for encoding +int ULCi_GetNoiseQ(const float *Data, int Band, int N, float q) { + //! Fixup for DCT+DST -> Pseudo-DFT + Data += Band / 2 * 2; + N = (N + (Band & 1) + 1) / 2; + + //! Analyze for the noise amplitude (geometric mean over N coefficients) + float Amplitude; { + int n; + float Sum = 0.0f, SumW = 0.0f; + for(n=0;n Pseudo-DFT + Data += Band / 2 * 2; + N = (N + (Band & 1) + 1) / 2; + + //! Solve for least-squares (in the log domain, for exponential fitting) + float Amplitude, Decay; { + //! If couldn't solve, disable noise fill + if(!GetHFExtParams_LeastSquares(Data, N, &Amplitude, &Decay)) { + *_NoiseQ = *_NoiseDecay = 0; + return; + } + + //! Convert to linear units + Amplitude = expf(Amplitude); + Decay = (Decay < 0.0f) ? expf(Decay) : 1.0f; //! <- Ensure E^LogDecay is <= 1.0 + } + + //! Quantize amplitude and decay + //! Amplitude has already been scaled by 4.0 (plus normalization), + //! but we need to scale to 16.0 here because HF extension uses + //! a 4bit amplitude instead of 3bit like "normal" noise fill does + int NoiseQ = ULCi_CompandedQuantizeCoefficientUnsigned(Amplitude*q*4.0f, 1 + 0xF); + int NoiseDecay = ULCi_CompandedQuantizeUnsigned((Decay-1.0f) * -0x1.0p19f); //! (1-Decay) * 2^19 + if(!NoiseDecay) return; //! <- On immediate falloff, disable fill + if(NoiseDecay > 0xFF) NoiseDecay = 0xFF; + *_NoiseQ = NoiseQ; + *_NoiseDecay = NoiseDecay; +} + +/**************************************/ +#endif +/**************************************/ +//! EOF +/**************************************/ diff --git a/tools/audioconv64/libulc/ulcEncoder_Psyopt.c b/tools/audioconv64/libulc/ulcEncoder_Psyopt.c new file mode 100644 index 000000000..ecbcd8634 --- /dev/null +++ b/tools/audioconv64/libulc/ulcEncoder_Psyopt.c @@ -0,0 +1,256 @@ +/**************************************/ +//! ulc-codec: Ultra-Low-Complexity Audio Codec +//! Copyright (C) 2025, Ruben Nunez (Aikku; aik AT aol DOT com DOT au) +//! Refer to the project README file for license terms. +/**************************************/ +#include +#include +/**************************************/ +#include "ulcEncoder.h" +#include "ulcHelper.h" +#include "ulcEncoder_Internals.h" +/**************************************/ +#if (ULC_USE_PSYCHOACOUSTICS || ULC_USE_NOISE_CODING) +/**************************************/ + +struct LineSum_t { + int LineEnd; + double Floor; + double Peak; + double PeakW; +}; + +static void InitLineSum(struct LineSum_t *LineSum) { + LineSum->LineEnd = 0; + LineSum->Floor = 0.0; + LineSum->Peak = 0.0; + LineSum->PeakW = 0.0; +} + +static void UpdateLineSum( + const float *Src, + const float *SrcLog, + struct LineSum_t *LineSum, + int LineEnd //! <- Range /exclusive/ +) { + int Line; + double SumFloor = LineSum->Floor; + double SumPeak = LineSum->Peak; + double SumPeakW = LineSum->PeakW; + for(Line=LineSum->LineEnd;LineLineEnd = LineEnd; + LineSum->Floor = SumFloor; + LineSum->Peak = SumPeak; + LineSum->PeakW = SumPeakW; +} + +/**************************************/ +#endif +/**************************************/ +#if ULC_USE_PSYCHOACOUSTICS +/**************************************/ + +//! Calculate masking levels for each frequency line +void ULCi_CalculatePsychoacoustics( + float *MaskingNp, + float *BufferAmp2, + void *BufferTemp, + int BlockSize, + int RateHz, + uint32_t WindowCtrl +) { + const int ULC_N_BARK_BANDS = 25; + float NyquistHz = (float)RateHz * 0.5f; + + //! DCT+DST -> Pseudo-DFT + BlockSize /= 2; + + //! Compute logarithm for all lines to speed up calculations + { + int Line; + for(Line=0;Line> (DecimationPattern&0x7); + + //! Iterate over all Bark bands + //! If any band is silent, then we use the ratio of the + //! prior band, since we will later interpolate using + //! these values. + //! Because the Bark bands (and thus frequency lines) + //! are being scanned in increasing order, we optimize + //! by accumulating a "low" and "high" sum, which will + //! correspond to the start and end of the analysis + //! region, and subtract low from high to get the sum. + int BarkBand; + float UnmaskRatio = 0.0f; + float *BarkUnmasked = (float*)BufferTemp; + struct LineSum_t LineSumLo, LineSumHi; + InitLineSum(&LineSumLo); + InitLineSum(&LineSumHi); + for(BarkBand=0;BarkBand SubBlockSize-1) LineBeg = SubBlockSize-1; + if(LineEnd > SubBlockSize) LineEnd = SubBlockSize; + + //! Update levels for this band + UpdateLineSum(BufferAmp2, MaskingNp, &LineSumLo, LineBeg); + UpdateLineSum(BufferAmp2, MaskingNp, &LineSumHi, LineEnd); + double SumFloor = LineSumHi.Floor - LineSumLo.Floor; + double SumPeak = LineSumHi.Peak - LineSumLo.Peak; + double SumPeakW = LineSumHi.PeakW - LineSumLo.PeakW; + + //! Get the final unmasking ratio for this band + //! This basically amounts to how much CANNOT be + //! masked by this band, and then applies a + //! scaling to this Bark band so as to normalize + //! by the amount of energy present. + if(SumPeakW > 0.0) { + SumPeak = SumPeak / SumPeakW; + SumFloor = SumFloor / (double)(LineEnd - LineBeg); + UnmaskRatio = (float)(SumPeak - SumFloor - log(SumPeakW)); + } + BarkUnmasked[BarkBand] = UnmaskRatio; + } + + //! Now generate unmasking level for each frequency line + int Line; + for(Line=0;Line>= 4); +} + +/**************************************/ +#endif +/**************************************/ +#if ULC_USE_NOISE_CODING +/**************************************/ + +//! Compute noise spectrum (logarithmic output) +//! The code here is very similar to above. +//! The main difference is that we're extracting the noise +//! level after masking with the tone level, rather than +//! the other way around. +void ULCi_CalculateNoiseLogSpectrum(float *Data, void *Temp, int N, int RateHz) { + const int ULC_N_BARK_BANDS = 25; + float NyquistHz = (float)RateHz * 0.5f; + + //! DCT+DST -> Pseudo-DFT + N /= 2; + + //! Compute logarithm for all lines to speed up calculations + float *LogData = (float*)Temp; { + int Line; + for(Line=0;Line N-1) LineBeg = N-1; + if(LineEnd > N) LineEnd = N; + + //! Sum levels for this band + UpdateLineSum(Data, LogData, &LineSumLo, LineBeg); + UpdateLineSum(Data, LogData, &LineSumHi, LineEnd); + double SumFloor = LineSumHi.Floor - LineSumLo.Floor; + double SumPeak = LineSumHi.Peak - LineSumLo.Peak; + double SumPeakW = LineSumHi.PeakW - LineSumLo.PeakW; + + //! Get the remaining energy after subtracting the + //! unmasked energy, and treat this as noise energy + if(SumPeakW > 0.0) { + double LineScale = 1.0 / (double)(LineEnd - LineBeg); + SumPeak = SumPeak / SumPeakW; + SumFloor = SumFloor * LineScale; + NoiseLevel = 0.5f*(float)(log(SumPeakW*LineScale) + SumFloor - SumPeak); + } + BarkNoise[BarkBand] = NoiseLevel; + } + + //! Now generate noise level for each frequency line + //! Note that I have not gone insane in the weight; + //! X^2 is definitely too much, X is a bit too much, + //! but Sqrt[X] seems to be the best tradeoff. + //! The higher the exponent goes, the more likely + //! the noise is to trigger very loudly, because we + //! give more weight to the louder sections, which + //! isn't exactly what we want; we want to match the + //! overall shape, but without necessarily hitting + //! each peak exactly. + int Line; + for(Line=0;Line +/**************************************/ +#include "ulcEncoder.h" +#include "ulcHelper.h" +/**************************************/ + +//! Filter input data into transient pulses +//! NOTE: +//! -TransientBuffer[] must be ULC_MAX_BLOCK_DECIMATION_FACTOR*2 in size +//! and will be updated as (L = R_Old, R = New). Initialize with 0. +//! Internal layout is: +//! { +//! struct ULC_TransientData_t L[ULC_MAX_BLOCK_DECIMATION_FACTOR]; +//! struct ULC_TransientData_t R[ULC_MAX_BLOCK_DECIMATION_FACTOR]; +//! } +//! -TransientFilter[] must be 3 elements in size. Initialize with 0. +//! Internal layout is: +//! { +//! float EnvPostMaskHP; +//! float EnvPostMaskBP; +//! float EnvBlockMask; +//! } +//! -TmpBuffer[] must be BlockSize*2 elements in size. +//! NOTE: All rates were determined experimentally, based on what +//! resulted in the best sensitivity without excessive glitching. +static void UpdateFilters(float *Dst, const float *Src, float *Taps, uint32_t N) { + uint32_t n; + for(n=0;n=0;n--) { + //! NOTE: Cross-multiply HP with BP energy and vice-versa + //! to normalize the levels with respect to one another + float vHP = BufEnergy[n*2+0], dHP = vHP - EnvPreMaskHP; + float vBP = BufEnergy[n*2+1], dBP = vBP - EnvPreMaskBP; + EnvPreMaskHP += dHP * (1.0f-EnvPreMaskHP_Rate); + EnvPreMaskBP += dBP * (1.0f-EnvPreMaskBP_Rate); + BufEnergy[n*2+0] = SQR(dHP*EnvPreMaskBP) + SQR(dBP*EnvPreMaskHP); + } + + //! Integrate the energy curves over each segment + { + int i, BinSize = BlockSize / ULC_MAX_BLOCK_DECIMATION_FACTOR; + struct ULC_TransientData_t *Dst = TransientBuffer + ULC_MAX_BLOCK_DECIMATION_FACTOR; //! Align to new block + i = ULC_MAX_BLOCK_DECIMATION_FACTOR; do { + //! Swap out the old "new" data, and clear the new "new" data + Dst[-ULC_MAX_BLOCK_DECIMATION_FACTOR] = *Dst; + *Dst = (struct ULC_TransientData_t){.Sum = 0.0f, .SumW = 0.0f}; + + //! Smooth the signal to account for the block size. + //! Larger blocks get less smoothing to capture changes more + //! easily, smaller blocks get more smoothing because they + //! don't need to capture smooth-ish changes. + float EnvBlockMask = TransientFilter[2]; + float EnvBlockMask_Rate = expf(-0x1.1AF110p-6f * BlockSize / RateHz); //! -0.00015dB/ms*BlockSize (1000 * Log[10^(-0.00015/20)]) + for(n=0;nSum += EnvBlockMask, Dst->SumW += 1; + } + TransientFilter[2] = EnvBlockMask; + + //! Move to next segment + BufEnergy += BinSize*2; + } while(Dst++, --i); + } +} + +/**************************************/ + +//! Get optimal log base-2 overlap and window scalings for transients +int ULCi_GetWindowCtrl( + const float *BlockData, + struct ULC_TransientData_t *TransientBuffer, + float *TransientFilter, + float *TmpBuffer, + int BlockSize, + int nChan, + int RateHz +) { + int n; + + //! Perform filtering to obtain transient analysis + //! then seek to this "new" block's transient data + TransientFiltering(BlockData, TransientBuffer, TransientFilter, TmpBuffer, BlockSize, nChan, RateHz); + TransientBuffer += ULC_MAX_BLOCK_DECIMATION_FACTOR; + + //! Keep trying to increase the window size until the + //! transient drops off compared to the last window + //! NOTE: Default to maximum overlap, no decimation + int Log2SubBlockSize = 31 - __builtin_clz(BlockSize/ULC_MAX_BLOCK_DECIMATION_FACTOR); + int Decimation = 0b0001; + float TransientRatio = 0.0f; { + //! First, enforce a minimum SubBlockSize of 64 + int nSegments = ULC_MAX_BLOCK_DECIMATION_FACTOR; + int SegmentSize = 1; + if(Log2SubBlockSize < 6) { + int Shift = 6 - Log2SubBlockSize; + nSegments >>= Shift; + SegmentSize <<= Shift; + Log2SubBlockSize = 6; + } + + //! Begin searching for the right windows + for(;;) { + //! For this iteration, we are preparing for a larger window. + //! Putting this here avoids awkward formulations later on. + Log2SubBlockSize++; + + //! Get the maximum Attack/Release ratio for all segments + //! by removing the masking (Release) level and then comparing + //! the remaining energy to that of the last segment + int Segment; + int MaxSegment = 0; + float AvgRatio = 0.0f, MaxRatio = -1000.0f; + for(Segment=0;Segment MaxRatio) MaxSegment = Segment, MaxRatio = Ratio; + } + AvgRatio /= nSegments; + + //! If this window causes the transient ratio to drop too much, + //! stop enlarging and use the windows from the last iteration. + //! On the other hand: If the ratio /increases/ as the window + //! size increases, then the transient is likely close to the + //! center of two subblocks; in this case, we /can/ continue + //! increasing the window size, but we must be very careful to + //! only do this if the transient is significant enough - when + //! it's not, increasing the window size will cause 'metallic' + //! artifacts around the transient, as well as post-echo. + if(MaxRatio-TransientRatio < 0x1.62E430p-1f) break; //! 0x1.62E430p-1 = Log[2] + + //! Set new decimation pattern + transient ratio, and continue + //! NOTE: Only try larger subblocks if the transient isn't too powerful, + //! otherwise we risk smearing it, which would defeat the purpose. + Decimation = nSegments + MaxSegment; + TransientRatio = MaxRatio; + if(nSegments > 1 && TransientRatio < 0x1.62E430p-1f) { //! 0x1.62E430p-1 = Log[2] + //! Increase spectral resolution + nSegments /= 2; + SegmentSize *= 2; + } else break; + } + } + + //! If the transient isn't powerful enough, just return a full-overlap block + if(TransientRatio < 0x1.62E430p-2f) return 0x10; + + //! Determine overlap size from the ratio + //! NOTE: Log2SubBlockSize was pre-incremented earlier, so account for this here. + TransientRatio *= 0x1.715476p0f; //! 0x1.715476p0 = 1/Log[2] for change of base + int OverlapScale = (TransientRatio < 0.5f) ? 0 : (TransientRatio >= 6.5f) ? 7 : lrintf(TransientRatio); + if(Log2SubBlockSize-OverlapScale < 5+1) OverlapScale = Log2SubBlockSize - (5+1); //! Minimum 32-sample overlap + + //! Return the combined overlap+window switching parameters + return OverlapScale + 0x8*(Decimation != 1) + 0x10*Decimation; +} + +/**************************************/ +//! EOF +/**************************************/ diff --git a/tools/audioconv64/libulc/ulcHelper.h b/tools/audioconv64/libulc/ulcHelper.h new file mode 100644 index 000000000..658d949e3 --- /dev/null +++ b/tools/audioconv64/libulc/ulcHelper.h @@ -0,0 +1,140 @@ +/**************************************/ +//! ulc-codec: Ultra-Low-Complexity Audio Codec +//! Copyright (C) 2025, Ruben Nunez (Aikku; aik AT aol DOT com DOT au) +//! Refer to the project README file for license terms. +/**************************************/ +#pragma once +/**************************************/ +#include +#include +#include +/**************************************/ +#include "ulcEncoder.h" +/**************************************/ +#define ABS(x) ((x) < 0 ? (-(x)) : (x)) +#define SQR(x) ((x)*(x)) +/**************************************/ +#define ULC_FORCED_INLINE static inline __attribute__((always_inline)) +/**************************************/ + +//! Subblock decimation pattern +//! Each subblock is coded in 4 bits (LSB to MSB): +//! Bit0..2: Subblock shift (ie. BlockSize >> Shift) +//! Bit3: Transient flag (ie. apply overlap scaling to that subblock) +typedef uint_least16_t ULC_SubBlockDecimationPattern_t; +ULC_FORCED_INLINE +ULC_SubBlockDecimationPattern_t ULCi_SubBlockDecimationPattern(int WindowCtrl) { + static const ULC_SubBlockDecimationPattern_t Pattern[] = { + 0x0000 | 0x0000, //! 0000: N/1 (Unused) + 0x0000 | 0x0008, //! 0001: N/1* + 0x0011 | 0x0008, //! 0010: N/2*,N/2 + 0x0011 | 0x0080, //! 0011: N/2,N/2* + 0x0122 | 0x0008, //! 0100: N/4*,N/4,N/2 + 0x0122 | 0x0080, //! 0101: N/4,N/4*,N/2 + 0x0221 | 0x0080, //! 0110: N/2,N/4*,N/4 + 0x0221 | 0x0800, //! 0111: N/2,N/4,N/4* + 0x1233 | 0x0008, //! 1000: N/8*,N/8,N/4,N/2 + 0x1233 | 0x0080, //! 1001: N/8,N/8*,N/4,N/2 + 0x1332 | 0x0080, //! 1010: N/4,N/8*,N/8,N/2 + 0x1332 | 0x0800, //! 1011: N/4,N/8,N/8*,N/2 + 0x2331 | 0x0080, //! 1100: N/2,N/8*,N/8,N/4 + 0x2331 | 0x0800, //! 1101: N/2,N/8,N/8*,N/4 + 0x3321 | 0x0800, //! 1110: N/2,N/4,N/8*,N/8 + 0x3321 | 0x8000, //! 1111: N/2,N/4,N/8,N/8* + }; + return Pattern[WindowCtrl >> 4]; +} + +/**************************************/ + +//! Quantize value (mathematically optimal) +ULC_FORCED_INLINE +int ULCi_CompandedQuantizeUnsigned(float v) { + //! Given x pre-scaled by the quantizer, and x' being companded x: + //! xq = Floor[x'] + (x - Floor[x']^2 >= (Floor[x']+1)^2 - x) + //! ie. We round up when (x'+1)^2 has less error; note the signs, + //! as Floor[x']+1 will always overshoot, and Floor[x'] can only + //! undershoot, so we avoid Abs[] by respecting this observation. + //! Letting u be the rounding term and thus xq = Floor[x'] + u, + //! and letting vq = Floor[x'], + //! u = (x - vq^2 >= (vq+1)^2 - x) // Add x + //! = (2x - vq^2 >= (vq+1)^2) // Expand (vq+1)^2 + //! = (2x - vq^2 >= 1 + 2*vq + vq^2) // Add vq^2 + //! = (2x >= 1 + 2*vq + 2vq^2) // Divide by 2 + //! = (x >= 0.5 + vq + vq^2) // Factor vq+vq^2 + //! = (x >= 0.5 + vq*(1+vq)) + //! ... all of which is apparently unnecessary, and correct + //! rounding can be achieved via: + //! xq = Sqrt[x - 0.25] + 0.5 | x > 0.25, + //! xq = 0 | otherwise + //! Which gives the smallest coefficient that returns xq>0 as 0.5. + return (v >= 0.5f) ? (int)(0.5f + sqrtf(v - 0.25f)) : 0; +} +ULC_FORCED_INLINE +int ULCi_CompandedQuantize(float v) { + int vq = ULCi_CompandedQuantizeUnsigned(ABS(v)); + return (v < 0.0f) ? (-vq) : (+vq); +} + +//! Quantize coefficient +//! This is its own function in case we need to change the rounding +//! behaviour for coefficients, relative to simply minimizing RMSE. +ULC_FORCED_INLINE +int ULCi_CompandedQuantizeCoefficientUnsigned(float v, int Limit) { + int vq = ULCi_CompandedQuantizeUnsigned(v); + return (vq < Limit) ? vq : Limit; +} +ULC_FORCED_INLINE +int ULCi_CompandedQuantizeCoefficient(float v, int Limit) { + int vq = ULCi_CompandedQuantizeCoefficientUnsigned(ABS(v), Limit); + return (v < 0.0f) ? (-vq) : (+vq); +} + +/**************************************/ + +//! Convert frequency Hz to line index (assuming centered frequency bins) +ULC_FORCED_INLINE +float ULCi_FreqToLine(float fHz, float NyquistHz, uint32_t N) { + return (fHz * (float)N / NyquistHz) - 0.5f; +} + +//! Convert line index to frequency Hz (assuming centered frequency bins) +ULC_FORCED_INLINE +float ULCi_LineToFreq(uint32_t Line, float NyquistHz, uint32_t N) { + return ((float)Line + 0.5f) * NyquistHz / (float)N; +} + +//! Convert frequency Hz to Bark band +//! Wang, Sekey & Gersho, 1992 definition: +//! Bark(f) = 6*ArcSinh[f/600] +ULC_FORCED_INLINE +float ULCi_FreqToBark(float fHz) { + return 6.0f*asinhf(fHz * (1.0f/600.0f)); +} + +//! Convert Bark band to frequency Hz +//! Inverse of above definition +ULC_FORCED_INLINE +float ULCi_BarkToFreq(float Bark) { + return 600.0f * sinhf(Bark * (1.0f/6.0f)); +} + +/**************************************/ + +//! Fast log(x) approximation +//! Borrowed from: https://quadst.rip/ln-approx +//! Modified to use memcpy() instead of type-aliasing. +ULC_FORCED_INLINE +float FastLog(float x) { + uint32_t bx; memcpy(&bx, &x, sizeof(float)); + uint32_t ex = bx >> 23; + int32_t t = (int32_t)ex - 127; + bx = (127 << 23) | (bx & ((1<<23)-1)); + memcpy(&x, &bx, sizeof(uint32_t)); + //return -1.49278f + (2.11263f + (-0.729104f + 0.10969f*x)*x)*x + 0.6931471806f*t; + return -1.7417939f + (2.8212026f + (-1.4699568f + (0.44717955f - 0.056570851f*x)*x)*x)*x + 0.6931471806f*t; +} + +/**************************************/ +//! EOF +/**************************************/ diff --git a/tools/audioconv64/libvadpcm.c b/tools/audioconv64/libvadpcm.c new file mode 100644 index 000000000..8c618ac1a --- /dev/null +++ b/tools/audioconv64/libvadpcm.c @@ -0,0 +1,6 @@ +#include "vadpcm/codec/autocorr.c" +#include "vadpcm/codec/predictor.c" +#include "vadpcm/codec/random.c" +#include "vadpcm/codec/encode.c" +#include "vadpcm/codec/decode.c" +#include "vadpcm/codec/error.c" diff --git a/tools/audioconv64/libvadpcm.h b/tools/audioconv64/libvadpcm.h new file mode 100644 index 000000000..ea1ecc4dd --- /dev/null +++ b/tools/audioconv64/libvadpcm.h @@ -0,0 +1 @@ +#include "vadpcm/codec/vadpcm.h" \ No newline at end of file diff --git a/tools/audioconv64/libxm.c b/tools/audioconv64/libxm.c new file mode 100644 index 000000000..1c65ef872 --- /dev/null +++ b/tools/audioconv64/libxm.c @@ -0,0 +1,3 @@ +#include "../../src/audio/libxm/play.c" +#include "../../src/audio/libxm/context.c" +#include "../../src/audio/libxm/load.c" diff --git a/tools/audioconv64/libxm.h b/tools/audioconv64/libxm.h new file mode 100644 index 000000000..d455a11c4 --- /dev/null +++ b/tools/audioconv64/libxm.h @@ -0,0 +1,10 @@ +#ifdef __cplusplus +extern "C" { +#endif + +#include "../../src/audio/libxm/xm.h" +#include "../../src/audio/libxm/xm_internal.h" + +#ifdef __cplusplus +} +#endif diff --git a/tools/common/lzh5_compress.c b/tools/audioconv64/lzh5_compress.c similarity index 100% rename from tools/common/lzh5_compress.c rename to tools/audioconv64/lzh5_compress.c diff --git a/tools/common/lzh5_compress.h b/tools/audioconv64/lzh5_compress.h similarity index 100% rename from tools/common/lzh5_compress.h rename to tools/audioconv64/lzh5_compress.h diff --git a/tools/audioconv64/tinysoundfont/LICENSE b/tools/audioconv64/tinysoundfont/LICENSE new file mode 100644 index 000000000..533da73eb --- /dev/null +++ b/tools/audioconv64/tinysoundfont/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2017-2023 Bernhard Schelling (Based on SFZero, Copyright (C) 2012 Steve Folta, https://github.com/stevefolta/SFZero) + +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. diff --git a/tools/audioconv64/tinysoundfont/README.md b/tools/audioconv64/tinysoundfont/README.md new file mode 100644 index 000000000..12a43e98e --- /dev/null +++ b/tools/audioconv64/tinysoundfont/README.md @@ -0,0 +1,9 @@ +TinySoundFont (vendored) +======================== + +Pinned copy of https://github.com/schellingb/TinySoundFont (MIT). + +Used only by audioconv64 to parse SF2 banks when converting to SF64. +Do not use at N64 runtime. + +Update by replacing `tsf.h` from upstream `master` and keeping this LICENSE. diff --git a/tools/audioconv64/tinysoundfont/tsf.h b/tools/audioconv64/tinysoundfont/tsf.h new file mode 100644 index 000000000..15d2ae5fd --- /dev/null +++ b/tools/audioconv64/tinysoundfont/tsf.h @@ -0,0 +1,2079 @@ +/* TinySoundFont - v0.9 - SoundFont2 synthesizer - https://github.com/schellingb/TinySoundFont + no warranty implied; use at your own risk + Do this: + #define TSF_IMPLEMENTATION + before you include this file in *one* C or C++ file to create the implementation. + // i.e. it should look like this: + #include ... + #include ... + #define TSF_IMPLEMENTATION + #include "tsf.h" + + [OPTIONAL] #define TSF_NO_STDIO to remove stdio dependency + [OPTIONAL] #define TSF_MALLOC, TSF_REALLOC, and TSF_FREE to avoid stdlib.h + [OPTIONAL] #define TSF_MEMCPY, TSF_MEMSET to avoid string.h + [OPTIONAL] #define TSF_POW, TSF_POWF, TSF_EXPF, TSF_LOG, TSF_TAN, TSF_LOG10, TSF_SQRT to avoid math.h + + NOT YET IMPLEMENTED + - Support for ChorusEffectsSend and ReverbEffectsSend generators + - Better low-pass filter without lowering performance too much + - Support for modulators + + LICENSE (MIT) + + Copyright (C) 2017-2025 Bernhard Schelling + Based on SFZero, Copyright (C) 2012 Steve Folta (https://github.com/stevefolta/SFZero) + + 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. + +*/ + +#ifndef TSF_INCLUDE_TSF_INL +#define TSF_INCLUDE_TSF_INL + +#ifdef __cplusplus +extern "C" { +# define CPP_DEFAULT0 = 0 +#else +# define CPP_DEFAULT0 +#endif + +//define this if you want the API functions to be static +#ifdef TSF_STATIC +#define TSFDEF static +#else +#define TSFDEF extern +#endif + +// The load functions will return a pointer to a struct tsf which all functions +// thereafter take as the first parameter. +// On error the tsf_load* functions will return NULL most likely due to invalid +// data (or if the file did not exist in tsf_load_filename). +typedef struct tsf tsf; + +#ifndef TSF_NO_STDIO +// Directly load a SoundFont from a .sf2 file path +TSFDEF tsf* tsf_load_filename(const char* filename); +#endif + +// Load a SoundFont from a block of memory +TSFDEF tsf* tsf_load_memory(const void* buffer, int size); + +// Stream structure for the generic loading +struct tsf_stream +{ + // Custom data given to the functions as the first parameter + void* data; + + // Function pointer will be called to read 'size' bytes into ptr (returns number of read bytes) + int (*read)(void* data, void* ptr, unsigned int size); + + // Function pointer will be called to skip ahead over 'count' bytes (returns 1 on success, 0 on error) + int (*skip)(void* data, unsigned int count); +}; + +// Generic SoundFont loading method using the stream structure above +TSFDEF tsf* tsf_load(struct tsf_stream* stream); + +// Copy a tsf instance from an existing one, use tsf_close to close it as well. +// All copied tsf instances and their original instance are linked, and share the underlying soundfont. +// This allows loading a soundfont only once, but using it for multiple independent playbacks. +// (This function isn't thread-safe without locking.) +TSFDEF tsf* tsf_copy(tsf* f); + +// Free the memory related to this tsf instance +TSFDEF void tsf_close(tsf* f); + +// Stop all playing notes immediately and reset all channel parameters +TSFDEF void tsf_reset(tsf* f); + +// Returns the preset index from a bank and preset number, or -1 if it does not exist in the loaded SoundFont +TSFDEF int tsf_get_presetindex(const tsf* f, int bank, int preset_number); + +// Returns the number of presets in the loaded SoundFont +TSFDEF int tsf_get_presetcount(const tsf* f); + +// Returns the name of a preset index >= 0 and < tsf_get_presetcount() +TSFDEF const char* tsf_get_presetname(const tsf* f, int preset_index); + +// Returns the name of a preset by bank and preset number +TSFDEF const char* tsf_bank_get_presetname(const tsf* f, int bank, int preset_number); + +// Supported output modes by the render methods +enum TSFOutputMode +{ + // Two channels with single left/right samples one after another + TSF_STEREO_INTERLEAVED, + // Two channels with all samples for the left channel first then right + TSF_STEREO_UNWEAVED, + // A single channel (stereo instruments are mixed into center) + TSF_MONO +}; + +// Thread safety: +// +// 1. Rendering / voices: +// +// Your audio output which calls the tsf_render* functions will most likely +// run on a different thread than where the playback tsf_note* functions +// are called. In which case some sort of concurrency control like a +// mutex needs to be used so they are not called at the same time. +// Alternatively, you can pre-allocate a maximum number of voices that can +// play simultaneously by calling tsf_set_max_voices after loading. +// That way memory re-allocation will not happen during tsf_note_on and +// TSF should become mostly thread safe. +// There is a theoretical chance that ending notes would negatively influence +// a voice that is rendering at the time but it is hard to say. +// Also be aware, this has not been tested much. +// +// 2. Channels: +// +// Calls to tsf_channel_set_... functions may allocate new channels +// if no channel with that number was previously used. Make sure to +// create all channels at the beginning as required if you call tsf_render* +// from a different thread. + +// Setup the parameters for the voice render methods +// outputmode: if mono or stereo and how stereo channel data is ordered +// samplerate: the number of samples per second (output frequency) +// global_gain_db: volume gain in decibels (>0 means higher, <0 means lower) +TSFDEF void tsf_set_output(tsf* f, enum TSFOutputMode outputmode, int samplerate, float global_gain_db CPP_DEFAULT0); + +// Set the global gain as a volume factor +// global_gain: the desired volume where 1.0 is 100% +TSFDEF void tsf_set_volume(tsf* f, float global_gain); + +// Set the maximum number of voices to play simultaneously +// Depending on the soundfond, one note can cause many new voices to be started, +// so don't keep this number too low or otherwise sounds may not play. +// max_voices: maximum number to pre-allocate and set the limit to +// (tsf_set_max_voices returns 0 if allocation failed, otherwise 1) +TSFDEF int tsf_set_max_voices(tsf* f, int max_voices); + +// Start playing a note +// preset_index: preset index >= 0 and < tsf_get_presetcount() +// key: note value between 0 and 127 (60 being middle C) +// vel: velocity as a float between 0.0 (equal to note off) and 1.0 (full) +// bank: instrument bank number (alternative to preset_index) +// preset_number: preset number (alternative to preset_index) +// (tsf_note_on returns 0 if the allocation of a new voice failed, otherwise 1) +// (tsf_bank_note_on returns 0 if preset does not exist or allocation failed, otherwise 1) +TSFDEF int tsf_note_on(tsf* f, int preset_index, int key, float vel); +TSFDEF int tsf_bank_note_on(tsf* f, int bank, int preset_number, int key, float vel); + +// Stop playing a note +// (bank_note_off returns 0 if preset does not exist, otherwise 1) +TSFDEF void tsf_note_off(tsf* f, int preset_index, int key); +TSFDEF int tsf_bank_note_off(tsf* f, int bank, int preset_number, int key); + +// Stop playing all notes (end with sustain and release) +TSFDEF void tsf_note_off_all(tsf* f); + +// Returns the number of active voices +TSFDEF int tsf_active_voice_count(tsf* f); + +// Render output samples into a buffer +// You can either render as signed 16-bit values (tsf_render_short) or +// as 32-bit float values (tsf_render_float) +// buffer: target buffer of size samples * output_channels * sizeof(type) +// samples: number of samples to render +// flag_mixing: if 0 clear the buffer first, otherwise mix into existing data +TSFDEF void tsf_render_short(tsf* f, short* buffer, int samples, int flag_mixing CPP_DEFAULT0); +TSFDEF void tsf_render_float(tsf* f, float* buffer, int samples, int flag_mixing CPP_DEFAULT0); + +// Higher level channel based functions, set up channel parameters +// channel: channel number +// preset_index: preset index >= 0 and < tsf_get_presetcount() +// preset_number: preset number (alternative to preset_index) +// flag_mididrums: 0 for normal channels, otherwise apply MIDI drum channel rules +// bank: instrument bank number (alternative to preset_index) +// pan: stereo panning value from 0.0 (left) to 1.0 (right) (default 0.5 center) +// volume: linear volume scale factor (default 1.0 full) +// pitch_wheel: pitch wheel position 0 to 16383 (default 8192 unpitched) +// pitch_range: range of the pitch wheel in semitones (default 2.0, total +/- 2 semitones) +// tuning: tuning of all playing voices in semitones (default 0.0, standard (A440) tuning) +// flag_sustain: 0 to end notes that were held sustained and disable holding sustain otherwise enable it +// (tsf_set_preset_number and set_bank_preset return 0 if preset does not exist, otherwise 1) +// (tsf_channel_set_... return 0 if a new channel needed allocation and that failed, otherwise 1) +TSFDEF int tsf_channel_set_presetindex(tsf* f, int channel, int preset_index); +TSFDEF int tsf_channel_set_presetnumber(tsf* f, int channel, int preset_number, int flag_mididrums CPP_DEFAULT0); +TSFDEF int tsf_channel_set_bank(tsf* f, int channel, int bank); +TSFDEF int tsf_channel_set_bank_preset(tsf* f, int channel, int bank, int preset_number); +TSFDEF int tsf_channel_set_pan(tsf* f, int channel, float pan); +TSFDEF int tsf_channel_set_volume(tsf* f, int channel, float volume); +TSFDEF int tsf_channel_set_pitchwheel(tsf* f, int channel, int pitch_wheel); +TSFDEF int tsf_channel_set_pitchrange(tsf* f, int channel, float pitch_range); +TSFDEF int tsf_channel_set_tuning(tsf* f, int channel, float tuning); +TSFDEF int tsf_channel_set_sustain(tsf* f, int channel, int flag_sustain); + +// Start or stop playing notes on a channel (needs channel preset to be set) +// channel: channel number +// key: note value between 0 and 127 (60 being middle C) +// vel: velocity as a float between 0.0 (equal to note off) and 1.0 (full) +// (tsf_channel_note_on returns 0 on allocation failure of new voice, otherwise 1) +TSFDEF int tsf_channel_note_on(tsf* f, int channel, int key, float vel); +TSFDEF void tsf_channel_note_off(tsf* f, int channel, int key); +TSFDEF void tsf_channel_note_off_all(tsf* f, int channel); //end with sustain and release +TSFDEF void tsf_channel_sounds_off_all(tsf* f, int channel); //end immediately + +// Apply a MIDI control change to the channel (not all controllers are supported!) +// (tsf_channel_midi_control returns 0 on allocation failure of new channel, otherwise 1) +TSFDEF int tsf_channel_midi_control(tsf* f, int channel, int controller, int control_value); + +// Get current values set on the channels +TSFDEF int tsf_channel_get_preset_index(tsf* f, int channel); +TSFDEF int tsf_channel_get_preset_bank(tsf* f, int channel); +TSFDEF int tsf_channel_get_preset_number(tsf* f, int channel); +TSFDEF float tsf_channel_get_pan(tsf* f, int channel); +TSFDEF float tsf_channel_get_volume(tsf* f, int channel); +TSFDEF int tsf_channel_get_pitchwheel(tsf* f, int channel); +TSFDEF float tsf_channel_get_pitchrange(tsf* f, int channel); +TSFDEF float tsf_channel_get_tuning(tsf* f, int channel); + +#ifdef __cplusplus +# undef CPP_DEFAULT0 +} +#endif + +// end header +// --------------------------------------------------------------------------------------------------------- +#endif //TSF_INCLUDE_TSF_INL + +#ifdef TSF_IMPLEMENTATION +#undef TSF_IMPLEMENTATION + +// The lower this block size is the more accurate the effects are. +// Increasing the value significantly lowers the CPU usage of the voice rendering. +// If LFO affects the low-pass filter it can be hearable even as low as 8. +#ifndef TSF_RENDER_EFFECTSAMPLEBLOCK +#define TSF_RENDER_EFFECTSAMPLEBLOCK 64 +#endif + +// When using tsf_render_short, to do the conversion a buffer of a fixed size is +// allocated on the stack. On low memory platforms this could be made smaller. +// Increasing this above 512 should not have a significant impact on performance. +// The value should be a multiple of TSF_RENDER_EFFECTSAMPLEBLOCK. +#ifndef TSF_RENDER_SHORTBUFFERBLOCK +#define TSF_RENDER_SHORTBUFFERBLOCK 512 +#endif + +// Grace release time for quick voice off (avoid clicking noise) +#define TSF_FASTRELEASETIME 0.01f + +#if !defined(TSF_MALLOC) || !defined(TSF_FREE) || !defined(TSF_REALLOC) +# include +# define TSF_MALLOC malloc +# define TSF_FREE free +# define TSF_REALLOC realloc +#endif + +#if !defined(TSF_MEMCPY) || !defined(TSF_MEMSET) +# include +# define TSF_MEMCPY memcpy +# define TSF_MEMSET memset +#endif + +#if !defined(TSF_POW) || !defined(TSF_POWF) || !defined(TSF_EXPF) || !defined(TSF_LOG) || !defined(TSF_TAN) || !defined(TSF_LOG10) || !defined(TSF_SQRT) +# include +# if !defined(__cplusplus) && !defined(NAN) && !defined(powf) && !defined(expf) && !defined(sqrtf) +# define powf (float)pow // deal with old math.h +# define expf (float)exp // files that come without +# define sqrtf (float)sqrt // powf, expf and sqrtf +# endif +# define TSF_POW pow +# define TSF_POWF powf +# define TSF_EXPF expf +# define TSF_LOG log +# define TSF_TAN tan +# define TSF_LOG10 log10 +# define TSF_SQRTF sqrtf +#endif + +#ifndef TSF_NO_STDIO +# include +#endif + +#define TSF_TRUE 1 +#define TSF_FALSE 0 +#define TSF_BOOL unsigned char +#define TSF_PI 3.14159265358979323846264338327950288 +#define TSF_NULL 0 + +#ifdef __cplusplus +extern "C" { +#endif + +typedef char tsf_fourcc[4]; +typedef signed char tsf_s8; +typedef unsigned char tsf_u8; +typedef unsigned short tsf_u16; +typedef signed short tsf_s16; +typedef unsigned int tsf_u32; +typedef char tsf_char20[20]; + +#define TSF_FourCCEquals(value1, value2) (value1[0] == value2[0] && value1[1] == value2[1] && value1[2] == value2[2] && value1[3] == value2[3]) + +struct tsf +{ + struct tsf_preset* presets; + float* fontSamples; + struct tsf_voice* voices; + struct tsf_channels* channels; + + int presetNum; + int voiceNum; + int maxVoiceNum; + unsigned int voicePlayIndex; + + enum TSFOutputMode outputmode; + float outSampleRate; + float globalGainDB; + int* refCount; +}; + +#ifndef TSF_NO_STDIO +static int tsf_stream_stdio_read(FILE* f, void* ptr, unsigned int size) { return (int)fread(ptr, 1, size, f); } +static int tsf_stream_stdio_skip(FILE* f, unsigned int count) { return !fseek(f, count, SEEK_CUR); } +TSFDEF tsf* tsf_load_filename(const char* filename) +{ + tsf* res; + struct tsf_stream stream = { TSF_NULL, (int(*)(void*,void*,unsigned int))&tsf_stream_stdio_read, (int(*)(void*,unsigned int))&tsf_stream_stdio_skip }; + #if __STDC_WANT_SECURE_LIB__ + FILE* f = TSF_NULL; fopen_s(&f, filename, "rb"); + #else + FILE* f = fopen(filename, "rb"); + #endif + if (!f) + { + //if (e) *e = TSF_FILENOTFOUND; + return TSF_NULL; + } + stream.data = f; + res = tsf_load(&stream); + fclose(f); + return res; +} +#endif + +struct tsf_stream_memory { const char* buffer; unsigned int total, pos; }; +static int tsf_stream_memory_read(struct tsf_stream_memory* m, void* ptr, unsigned int size) { if (size > m->total - m->pos) size = m->total - m->pos; TSF_MEMCPY(ptr, m->buffer+m->pos, size); m->pos += size; return size; } +static int tsf_stream_memory_skip(struct tsf_stream_memory* m, unsigned int count) { if (m->pos + count > m->total) return 0; m->pos += count; return 1; } +TSFDEF tsf* tsf_load_memory(const void* buffer, int size) +{ + struct tsf_stream stream = { TSF_NULL, (int(*)(void*,void*,unsigned int))&tsf_stream_memory_read, (int(*)(void*,unsigned int))&tsf_stream_memory_skip }; + struct tsf_stream_memory f = { 0, 0, 0 }; + f.buffer = (const char*)buffer; + f.total = size; + stream.data = &f; + return tsf_load(&stream); +} + +enum { TSF_LOOPMODE_NONE, TSF_LOOPMODE_CONTINUOUS, TSF_LOOPMODE_SUSTAIN }; + +enum { TSF_SEGMENT_NONE, TSF_SEGMENT_DELAY, TSF_SEGMENT_ATTACK, TSF_SEGMENT_HOLD, TSF_SEGMENT_DECAY, TSF_SEGMENT_SUSTAIN, TSF_SEGMENT_RELEASE, TSF_SEGMENT_DONE }; + +struct tsf_hydra +{ + struct tsf_hydra_phdr *phdrs; struct tsf_hydra_pbag *pbags; struct tsf_hydra_pmod *pmods; + struct tsf_hydra_pgen *pgens; struct tsf_hydra_inst *insts; struct tsf_hydra_ibag *ibags; + struct tsf_hydra_imod *imods; struct tsf_hydra_igen *igens; struct tsf_hydra_shdr *shdrs; + int phdrNum, pbagNum, pmodNum, pgenNum, instNum, ibagNum, imodNum, igenNum, shdrNum; +}; + +union tsf_hydra_genamount { struct { tsf_u8 lo, hi; } range; tsf_s16 shortAmount; tsf_u16 wordAmount; }; +struct tsf_hydra_phdr { tsf_char20 presetName; tsf_u16 preset, bank, presetBagNdx; tsf_u32 library, genre, morphology; }; +struct tsf_hydra_pbag { tsf_u16 genNdx, modNdx; }; +struct tsf_hydra_pmod { tsf_u16 modSrcOper, modDestOper; tsf_s16 modAmount; tsf_u16 modAmtSrcOper, modTransOper; }; +struct tsf_hydra_pgen { tsf_u16 genOper; union tsf_hydra_genamount genAmount; }; +struct tsf_hydra_inst { tsf_char20 instName; tsf_u16 instBagNdx; }; +struct tsf_hydra_ibag { tsf_u16 instGenNdx, instModNdx; }; +struct tsf_hydra_imod { tsf_u16 modSrcOper, modDestOper; tsf_s16 modAmount; tsf_u16 modAmtSrcOper, modTransOper; }; +struct tsf_hydra_igen { tsf_u16 genOper; union tsf_hydra_genamount genAmount; }; +struct tsf_hydra_shdr { tsf_char20 sampleName; tsf_u32 start, end, startLoop, endLoop, sampleRate; tsf_u8 originalPitch; tsf_s8 pitchCorrection; tsf_u16 sampleLink, sampleType; }; + +#define TSFR(FIELD) stream->read(stream->data, &i->FIELD, sizeof(i->FIELD)); +static void tsf_hydra_read_phdr(struct tsf_hydra_phdr* i, struct tsf_stream* stream) { TSFR(presetName) TSFR(preset) TSFR(bank) TSFR(presetBagNdx) TSFR(library) TSFR(genre) TSFR(morphology) } +static void tsf_hydra_read_pbag(struct tsf_hydra_pbag* i, struct tsf_stream* stream) { TSFR(genNdx) TSFR(modNdx) } +static void tsf_hydra_read_pmod(struct tsf_hydra_pmod* i, struct tsf_stream* stream) { TSFR(modSrcOper) TSFR(modDestOper) TSFR(modAmount) TSFR(modAmtSrcOper) TSFR(modTransOper) } +static void tsf_hydra_read_pgen(struct tsf_hydra_pgen* i, struct tsf_stream* stream) { TSFR(genOper) TSFR(genAmount) } +static void tsf_hydra_read_inst(struct tsf_hydra_inst* i, struct tsf_stream* stream) { TSFR(instName) TSFR(instBagNdx) } +static void tsf_hydra_read_ibag(struct tsf_hydra_ibag* i, struct tsf_stream* stream) { TSFR(instGenNdx) TSFR(instModNdx) } +static void tsf_hydra_read_imod(struct tsf_hydra_imod* i, struct tsf_stream* stream) { TSFR(modSrcOper) TSFR(modDestOper) TSFR(modAmount) TSFR(modAmtSrcOper) TSFR(modTransOper) } +static void tsf_hydra_read_igen(struct tsf_hydra_igen* i, struct tsf_stream* stream) { TSFR(genOper) TSFR(genAmount) } +static void tsf_hydra_read_shdr(struct tsf_hydra_shdr* i, struct tsf_stream* stream) { TSFR(sampleName) TSFR(start) TSFR(end) TSFR(startLoop) TSFR(endLoop) TSFR(sampleRate) TSFR(originalPitch) TSFR(pitchCorrection) TSFR(sampleLink) TSFR(sampleType) } +#undef TSFR + +struct tsf_riffchunk { tsf_fourcc id; tsf_u32 size; }; +struct tsf_envelope { float delay, attack, hold, decay, sustain, release, keynumToHold, keynumToDecay; }; +struct tsf_voice_envelope { unsigned char segment, segmentIsExponential : 1, isAmpEnv : 1; short midiVelocity; float level, slope; int samplesUntilNextSegment; struct tsf_envelope parameters; }; +struct tsf_voice_lowpass { double QInv, a0, a1, b1, b2, z1, z2; TSF_BOOL active; }; +struct tsf_voice_lfo { int samplesUntil; float level, delta; }; + +struct tsf_region +{ + int loop_mode; + unsigned int sample_rate; + unsigned char lokey, hikey, lovel, hivel; + unsigned int group, offset, end, loop_start, loop_end; + int transpose, tune, pitch_keycenter, pitch_keytrack; + float attenuation, pan; + struct tsf_envelope ampenv, modenv; + int initialFilterQ, initialFilterFc; + int modEnvToPitch, modEnvToFilterFc, modLfoToFilterFc, modLfoToVolume; + float delayModLFO; + int freqModLFO, modLfoToPitch; + float delayVibLFO; + int freqVibLFO, vibLfoToPitch; +}; + +struct tsf_preset +{ + tsf_char20 presetName; + tsf_u16 preset, bank; + struct tsf_region* regions; + int regionNum; +}; + +struct tsf_voice +{ + int playingPreset, playingKey, playingChannel, heldSustain; + struct tsf_region* region; + double pitchInputTimecents, pitchOutputFactor; + double sourceSamplePosition; + float noteGainDB, panFactorLeft, panFactorRight; + unsigned int playIndex, loopStart, loopEnd; + struct tsf_voice_envelope ampenv, modenv; + struct tsf_voice_lowpass lowpass; + struct tsf_voice_lfo modlfo, viblfo; +}; + +struct tsf_channel +{ + unsigned short presetIndex, bank, pitchWheel, midiPan, midiVolume, midiExpression, midiRPN, midiData : 14, sustain : 1; + float panOffset, gainDB, pitchRange, tuning; +}; + +struct tsf_channels +{ + void (*setupVoice)(tsf* f, struct tsf_voice* voice); + int channelNum, activeChannel; + struct tsf_channel channels[1]; +}; + +static double tsf_timecents2Secsd(double timecents) { return TSF_POW(2.0, timecents / 1200.0); } +static float tsf_timecents2Secsf(float timecents) { return TSF_POWF(2.0f, timecents / 1200.0f); } +static float tsf_cents2Hertz(float cents) { return 8.176f * TSF_POWF(2.0f, cents / 1200.0f); } +static float tsf_decibelsToGain(float db) { return (db > -100.f ? TSF_POWF(10.0f, db * 0.05f) : 0); } +static float tsf_gainToDecibels(float gain) { return (gain <= .00001f ? -100.f : (float)(20.0 * TSF_LOG10(gain))); } + +static TSF_BOOL tsf_riffchunk_read(struct tsf_riffchunk* parent, struct tsf_riffchunk* chunk, struct tsf_stream* stream) +{ + TSF_BOOL IsRiff, IsList; + if (parent && sizeof(tsf_fourcc) + sizeof(tsf_u32) > parent->size) return TSF_FALSE; + if (!stream->read(stream->data, &chunk->id, sizeof(tsf_fourcc)) || *chunk->id <= ' ' || *chunk->id >= 'z') return TSF_FALSE; + if (!stream->read(stream->data, &chunk->size, sizeof(tsf_u32))) return TSF_FALSE; + if (parent && sizeof(tsf_fourcc) + sizeof(tsf_u32) + chunk->size > parent->size) return TSF_FALSE; + if (parent) parent->size -= sizeof(tsf_fourcc) + sizeof(tsf_u32) + chunk->size; + IsRiff = TSF_FourCCEquals(chunk->id, "RIFF"), IsList = TSF_FourCCEquals(chunk->id, "LIST"); + if (IsRiff && parent) return TSF_FALSE; //not allowed + if (!IsRiff && !IsList) return TSF_TRUE; //custom type without sub type + if (!stream->read(stream->data, &chunk->id, sizeof(tsf_fourcc)) || *chunk->id <= ' ' || *chunk->id >= 'z') return TSF_FALSE; + chunk->size -= sizeof(tsf_fourcc); + return TSF_TRUE; +} + +static void tsf_region_clear(struct tsf_region* i, TSF_BOOL for_relative) +{ + TSF_MEMSET(i, 0, sizeof(struct tsf_region)); + i->hikey = i->hivel = 127; + i->pitch_keycenter = 60; // C4 + if (for_relative) return; + + i->pitch_keytrack = 100; + + i->pitch_keycenter = -1; + + // SF2 defaults in timecents. + i->ampenv.delay = i->ampenv.attack = i->ampenv.hold = i->ampenv.decay = i->ampenv.release = -12000.0f; + i->modenv.delay = i->modenv.attack = i->modenv.hold = i->modenv.decay = i->modenv.release = -12000.0f; + + i->initialFilterFc = 13500; + + i->delayModLFO = -12000.0f; + i->delayVibLFO = -12000.0f; +} + +static void tsf_region_operator(struct tsf_region* region, tsf_u16 genOper, union tsf_hydra_genamount* amount, struct tsf_region* merge_region) +{ + enum + { + _GEN_TYPE_MASK = 0x0F, + GEN_FLOAT = 0x01, + GEN_INT = 0x02, + GEN_UINT_ADD = 0x03, + GEN_UINT_ADD15 = 0x04, + GEN_KEYRANGE = 0x05, + GEN_VELRANGE = 0x06, + GEN_LOOPMODE = 0x07, + GEN_GROUP = 0x08, + GEN_KEYCENTER = 0x09, + + _GEN_LIMIT_MASK = 0xF0, + GEN_INT_LIMIT12K = 0x10, //min -12000, max 12000 + GEN_INT_LIMITFC = 0x20, //min 1500, max 13500 + GEN_INT_LIMITQ = 0x30, //min 0, max 960 + GEN_INT_LIMIT960 = 0x40, //min -960, max 960 + GEN_INT_LIMIT16K4500 = 0x50, //min -16000, max 4500 + GEN_FLOAT_LIMIT12K5K = 0x60, //min -12000, max 5000 + GEN_FLOAT_LIMIT12K8K = 0x70, //min -12000, max 8000 + GEN_FLOAT_LIMIT1200 = 0x80, //min -1200, max 1200 + GEN_FLOAT_LIMITPAN = 0x90, //* .001f, min -.5f, max .5f, + GEN_FLOAT_LIMITATTN = 0xA0, //* .1f, min 0, max 144.0 + GEN_FLOAT_MAX1000 = 0xB0, //min 0, max 1000 + GEN_FLOAT_MAX1440 = 0xC0, //min 0, max 1440 + + _GEN_MAX = 59 + }; + #define _TSFREGIONOFFSET(TYPE, FIELD) (unsigned char)(((TYPE*)&((struct tsf_region*)0)->FIELD) - (TYPE*)0) + #define _TSFREGIONENVOFFSET(TYPE, ENV, FIELD) (unsigned char)(((TYPE*)&((&(((struct tsf_region*)0)->ENV))->FIELD)) - (TYPE*)0) + static const struct { unsigned char mode, offset; } genMetas[_GEN_MAX] = + { + { GEN_UINT_ADD , _TSFREGIONOFFSET(unsigned int, offset ) }, // 0 StartAddrsOffset + { GEN_UINT_ADD , _TSFREGIONOFFSET(unsigned int, end ) }, // 1 EndAddrsOffset + { GEN_UINT_ADD , _TSFREGIONOFFSET(unsigned int, loop_start ) }, // 2 StartloopAddrsOffset + { GEN_UINT_ADD , _TSFREGIONOFFSET(unsigned int, loop_end ) }, // 3 EndloopAddrsOffset + { GEN_UINT_ADD15 , _TSFREGIONOFFSET(unsigned int, offset ) }, // 4 StartAddrsCoarseOffset + { GEN_INT | GEN_INT_LIMIT12K , _TSFREGIONOFFSET( int, modLfoToPitch ) }, // 5 ModLfoToPitch + { GEN_INT | GEN_INT_LIMIT12K , _TSFREGIONOFFSET( int, vibLfoToPitch ) }, // 6 VibLfoToPitch + { GEN_INT | GEN_INT_LIMIT12K , _TSFREGIONOFFSET( int, modEnvToPitch ) }, // 7 ModEnvToPitch + { GEN_INT | GEN_INT_LIMITFC , _TSFREGIONOFFSET( int, initialFilterFc ) }, // 8 InitialFilterFc + { GEN_INT | GEN_INT_LIMITQ , _TSFREGIONOFFSET( int, initialFilterQ ) }, // 9 InitialFilterQ + { GEN_INT | GEN_INT_LIMIT12K , _TSFREGIONOFFSET( int, modLfoToFilterFc ) }, //10 ModLfoToFilterFc + { GEN_INT | GEN_INT_LIMIT12K , _TSFREGIONOFFSET( int, modEnvToFilterFc ) }, //11 ModEnvToFilterFc + { GEN_UINT_ADD15 , _TSFREGIONOFFSET(unsigned int, end ) }, //12 EndAddrsCoarseOffset + { GEN_INT | GEN_INT_LIMIT960 , _TSFREGIONOFFSET( int, modLfoToVolume ) }, //13 ModLfoToVolume + { 0 , (0 ) }, // Unused + { 0 , (0 ) }, //15 ChorusEffectsSend (unsupported) + { 0 , (0 ) }, //16 ReverbEffectsSend (unsupported) + { GEN_FLOAT | GEN_FLOAT_LIMITPAN , _TSFREGIONOFFSET( float, pan ) }, //17 Pan + { 0 , (0 ) }, // Unused + { 0 , (0 ) }, // Unused + { 0 , (0 ) }, // Unused + { GEN_FLOAT | GEN_FLOAT_LIMIT12K5K , _TSFREGIONOFFSET( float, delayModLFO ) }, //21 DelayModLFO + { GEN_INT | GEN_INT_LIMIT16K4500 , _TSFREGIONOFFSET( int, freqModLFO ) }, //22 FreqModLFO + { GEN_FLOAT | GEN_FLOAT_LIMIT12K5K , _TSFREGIONOFFSET( float, delayVibLFO ) }, //23 DelayVibLFO + { GEN_INT | GEN_INT_LIMIT16K4500 , _TSFREGIONOFFSET( int, freqVibLFO ) }, //24 FreqVibLFO + { GEN_FLOAT | GEN_FLOAT_LIMIT12K5K , _TSFREGIONENVOFFSET( float, modenv, delay ) }, //25 DelayModEnv + { GEN_FLOAT | GEN_FLOAT_LIMIT12K8K , _TSFREGIONENVOFFSET( float, modenv, attack ) }, //26 AttackModEnv + { GEN_FLOAT | GEN_FLOAT_LIMIT12K5K , _TSFREGIONENVOFFSET( float, modenv, hold ) }, //27 HoldModEnv + { GEN_FLOAT | GEN_FLOAT_LIMIT12K8K , _TSFREGIONENVOFFSET( float, modenv, decay ) }, //28 DecayModEnv + { GEN_FLOAT | GEN_FLOAT_MAX1000 , _TSFREGIONENVOFFSET( float, modenv, sustain ) }, //29 SustainModEnv + { GEN_FLOAT | GEN_FLOAT_LIMIT12K8K , _TSFREGIONENVOFFSET( float, modenv, release ) }, //30 ReleaseModEnv + { GEN_FLOAT | GEN_FLOAT_LIMIT1200 , _TSFREGIONENVOFFSET( float, modenv, keynumToHold ) }, //31 KeynumToModEnvHold + { GEN_FLOAT | GEN_FLOAT_LIMIT1200 , _TSFREGIONENVOFFSET( float, modenv, keynumToDecay) }, //32 KeynumToModEnvDecay + { GEN_FLOAT | GEN_FLOAT_LIMIT12K5K , _TSFREGIONENVOFFSET( float, ampenv, delay ) }, //33 DelayVolEnv + { GEN_FLOAT | GEN_FLOAT_LIMIT12K8K , _TSFREGIONENVOFFSET( float, ampenv, attack ) }, //34 AttackVolEnv + { GEN_FLOAT | GEN_FLOAT_LIMIT12K5K , _TSFREGIONENVOFFSET( float, ampenv, hold ) }, //35 HoldVolEnv + { GEN_FLOAT | GEN_FLOAT_LIMIT12K8K , _TSFREGIONENVOFFSET( float, ampenv, decay ) }, //36 DecayVolEnv + { GEN_FLOAT | GEN_FLOAT_MAX1440 , _TSFREGIONENVOFFSET( float, ampenv, sustain ) }, //37 SustainVolEnv + { GEN_FLOAT | GEN_FLOAT_LIMIT12K8K , _TSFREGIONENVOFFSET( float, ampenv, release ) }, //38 ReleaseVolEnv + { GEN_FLOAT | GEN_FLOAT_LIMIT1200 , _TSFREGIONENVOFFSET( float, ampenv, keynumToHold ) }, //39 KeynumToVolEnvHold + { GEN_FLOAT | GEN_FLOAT_LIMIT1200 , _TSFREGIONENVOFFSET( float, ampenv, keynumToDecay) }, //40 KeynumToVolEnvDecay + { 0 , (0 ) }, // Instrument (special) + { 0 , (0 ) }, // Reserved + { GEN_KEYRANGE , (0 ) }, //43 KeyRange + { GEN_VELRANGE , (0 ) }, //44 VelRange + { GEN_UINT_ADD15 , _TSFREGIONOFFSET(unsigned int, loop_start ) }, //45 StartloopAddrsCoarseOffset + { 0 , (0 ) }, //46 Keynum (special) + { 0 , (0 ) }, //47 Velocity (special) + { GEN_FLOAT | GEN_FLOAT_LIMITATTN , _TSFREGIONOFFSET( float, attenuation ) }, //48 InitialAttenuation + { 0 , (0 ) }, // Reserved + { GEN_UINT_ADD15 , _TSFREGIONOFFSET(unsigned int, loop_end ) }, //50 EndloopAddrsCoarseOffset + { GEN_INT , _TSFREGIONOFFSET( int, transpose ) }, //51 CoarseTune + { GEN_INT , _TSFREGIONOFFSET( int, tune ) }, //52 FineTune + { 0 , (0 ) }, // SampleID (special) + { GEN_LOOPMODE , _TSFREGIONOFFSET( int, loop_mode ) }, //54 SampleModes + { 0 , (0 ) }, // Reserved + { GEN_INT , _TSFREGIONOFFSET( int, pitch_keytrack ) }, //56 ScaleTuning + { GEN_GROUP , _TSFREGIONOFFSET(unsigned int, group ) }, //57 ExclusiveClass + { GEN_KEYCENTER , _TSFREGIONOFFSET( int, pitch_keycenter ) }, //58 OverridingRootKey + }; + #undef _TSFREGIONOFFSET + #undef _TSFREGIONENVOFFSET + if (amount) + { + int offset; + if (genOper >= _GEN_MAX) return; + offset = genMetas[genOper].offset; + switch (genMetas[genOper].mode & _GEN_TYPE_MASK) + { + case GEN_FLOAT: (( float*)region)[offset] = amount->shortAmount; return; + case GEN_INT: (( int*)region)[offset] = amount->shortAmount; return; + case GEN_UINT_ADD: ((unsigned int*)region)[offset] += amount->shortAmount; return; + case GEN_UINT_ADD15: ((unsigned int*)region)[offset] += amount->shortAmount<<15; return; + case GEN_KEYRANGE: region->lokey = amount->range.lo; region->hikey = amount->range.hi; return; + case GEN_VELRANGE: region->lovel = amount->range.lo; region->hivel = amount->range.hi; return; + case GEN_LOOPMODE: region->loop_mode = ((amount->wordAmount&3) == 3 ? TSF_LOOPMODE_SUSTAIN : ((amount->wordAmount&3) == 1 ? TSF_LOOPMODE_CONTINUOUS : TSF_LOOPMODE_NONE)); return; + case GEN_GROUP: region->group = amount->wordAmount; return; + case GEN_KEYCENTER: region->pitch_keycenter = amount->shortAmount; return; + } + } + else //merge regions and clamp values + { + for (genOper = 0; genOper != _GEN_MAX; genOper++) + { + int offset = genMetas[genOper].offset; + switch (genMetas[genOper].mode & _GEN_TYPE_MASK) + { + case GEN_FLOAT: + { + float *val = &((float*)region)[offset], vfactor, vmin, vmax; + *val += ((float*)merge_region)[offset]; + switch (genMetas[genOper].mode & _GEN_LIMIT_MASK) + { + case GEN_FLOAT_LIMIT12K5K: vfactor = 1.0f; vmin = -12000.0f; vmax = 5000.0f; break; + case GEN_FLOAT_LIMIT12K8K: vfactor = 1.0f; vmin = -12000.0f; vmax = 8000.0f; break; + case GEN_FLOAT_LIMIT1200: vfactor = 1.0f; vmin = -1200.0f; vmax = 1200.0f; break; + case GEN_FLOAT_LIMITPAN: vfactor = 0.001f; vmin = -0.5f; vmax = 0.5f; break; + case GEN_FLOAT_LIMITATTN: vfactor = 0.01f; vmin = 0.0f; vmax = 14.4f; break; + case GEN_FLOAT_MAX1000: vfactor = 1.0f; vmin = 0.0f; vmax = 1000.0f; break; + case GEN_FLOAT_MAX1440: vfactor = 1.0f; vmin = 0.0f; vmax = 1440.0f; break; + default: continue; + } + *val *= vfactor; + if (*val < vmin) *val = vmin; + else if (*val > vmax) *val = vmax; + continue; + } + case GEN_INT: + { + int *val = &((int*)region)[offset], vmin, vmax; + *val += ((int*)merge_region)[offset]; + switch (genMetas[genOper].mode & _GEN_LIMIT_MASK) + { + case GEN_INT_LIMIT12K: vmin = -12000; vmax = 12000; break; + case GEN_INT_LIMITFC: vmin = 1500; vmax = 13500; break; + case GEN_INT_LIMITQ: vmin = 0; vmax = 960; break; + case GEN_INT_LIMIT960: vmin = -960; vmax = 960; break; + case GEN_INT_LIMIT16K4500: vmin = -16000; vmax = 4500; break; + default: continue; + } + if (*val < vmin) *val = vmin; + else if (*val > vmax) *val = vmax; + continue; + } + case GEN_UINT_ADD: + { + ((unsigned int*)region)[offset] += ((unsigned int*)merge_region)[offset]; + continue; + } + } + } + } +} + +static void tsf_region_envtosecs(struct tsf_envelope* p, TSF_BOOL sustainIsGain) +{ + // EG times need to be converted from timecents to seconds. + // Pin very short EG segments. Timecents don't get to zero, and our EG is + // happier with zero values. + p->delay = (p->delay < -11950.0f ? 0.0f : tsf_timecents2Secsf(p->delay)); + p->attack = (p->attack < -11950.0f ? 0.0f : tsf_timecents2Secsf(p->attack)); + p->release = (p->release < -11950.0f ? 0.0f : tsf_timecents2Secsf(p->release)); + + // If we have dynamic hold or decay times depending on key number we need + // to keep the values in timecents so we can calculate it during startNote + if (!p->keynumToHold) p->hold = (p->hold < -11950.0f ? 0.0f : tsf_timecents2Secsf(p->hold)); + if (!p->keynumToDecay) p->decay = (p->decay < -11950.0f ? 0.0f : tsf_timecents2Secsf(p->decay)); + + if (p->sustain < 0.0f) p->sustain = 0.0f; + else if (sustainIsGain) p->sustain = tsf_decibelsToGain(-p->sustain / 10.0f); + else p->sustain = 1.0f - (p->sustain / 1000.0f); +} + +static int tsf_load_presets(tsf* res, struct tsf_hydra *hydra, unsigned int fontSampleCount) +{ + enum { GenInstrument = 41, GenKeyRange = 43, GenVelRange = 44, GenSampleID = 53 }; + // Read each preset. + struct tsf_hydra_phdr *pphdr, *pphdrMax; + res->presetNum = hydra->phdrNum - 1; + res->presets = (struct tsf_preset*)TSF_MALLOC(res->presetNum * sizeof(struct tsf_preset)); + if (!res->presets) return 0; + else { int i; for (i = 0; i != res->presetNum; i++) res->presets[i].regions = TSF_NULL; } + for (pphdr = hydra->phdrs, pphdrMax = pphdr + hydra->phdrNum - 1; pphdr != pphdrMax; pphdr++) + { + int sortedIndex = 0, region_index = 0; + struct tsf_hydra_phdr *otherphdr; + struct tsf_preset* preset; + struct tsf_hydra_pbag *ppbag, *ppbagEnd; + struct tsf_region globalRegion; + for (otherphdr = hydra->phdrs; otherphdr != pphdrMax; otherphdr++) + { + if (otherphdr == pphdr || otherphdr->bank > pphdr->bank) continue; + else if (otherphdr->bank < pphdr->bank) sortedIndex++; + else if (otherphdr->preset > pphdr->preset) continue; + else if (otherphdr->preset < pphdr->preset) sortedIndex++; + else if (otherphdr < pphdr) sortedIndex++; + } + + preset = &res->presets[sortedIndex]; + TSF_MEMCPY(preset->presetName, pphdr->presetName, sizeof(preset->presetName)); + preset->presetName[sizeof(preset->presetName)-1] = '\0'; //should be zero terminated in source file but make sure + preset->bank = pphdr->bank; + preset->preset = pphdr->preset; + preset->regionNum = 0; + + //count regions covered by this preset + for (ppbag = hydra->pbags + pphdr->presetBagNdx, ppbagEnd = hydra->pbags + pphdr[1].presetBagNdx; ppbag != ppbagEnd; ppbag++) + { + unsigned char plokey = 0, phikey = 127, plovel = 0, phivel = 127; + struct tsf_hydra_pgen *ppgen, *ppgenEnd; struct tsf_hydra_inst *pinst; struct tsf_hydra_ibag *pibag, *pibagEnd; struct tsf_hydra_igen *pigen, *pigenEnd; + for (ppgen = hydra->pgens + ppbag->genNdx, ppgenEnd = hydra->pgens + ppbag[1].genNdx; ppgen != ppgenEnd; ppgen++) + { + if (ppgen->genOper == GenKeyRange) { plokey = ppgen->genAmount.range.lo; phikey = ppgen->genAmount.range.hi; continue; } + if (ppgen->genOper == GenVelRange) { plovel = ppgen->genAmount.range.lo; phivel = ppgen->genAmount.range.hi; continue; } + if (ppgen->genOper != GenInstrument) continue; + if (ppgen->genAmount.wordAmount >= hydra->instNum) continue; + pinst = hydra->insts + ppgen->genAmount.wordAmount; + for (pibag = hydra->ibags + pinst->instBagNdx, pibagEnd = hydra->ibags + pinst[1].instBagNdx; pibag != pibagEnd; pibag++) + { + unsigned char ilokey = 0, ihikey = 127, ilovel = 0, ihivel = 127; + for (pigen = hydra->igens + pibag->instGenNdx, pigenEnd = hydra->igens + pibag[1].instGenNdx; pigen != pigenEnd; pigen++) + { + if (pigen->genOper == GenKeyRange) { ilokey = pigen->genAmount.range.lo; ihikey = pigen->genAmount.range.hi; continue; } + if (pigen->genOper == GenVelRange) { ilovel = pigen->genAmount.range.lo; ihivel = pigen->genAmount.range.hi; continue; } + if (pigen->genOper == GenSampleID && ihikey >= plokey && ilokey <= phikey && ihivel >= plovel && ilovel <= phivel) preset->regionNum++; + } + } + } + } + + preset->regions = (struct tsf_region*)TSF_MALLOC(preset->regionNum * sizeof(struct tsf_region)); + if (!preset->regions) + { + int i; for (i = 0; i != res->presetNum; i++) TSF_FREE(res->presets[i].regions); + TSF_FREE(res->presets); + return 0; + } + tsf_region_clear(&globalRegion, TSF_TRUE); + + // Zones. + for (ppbag = hydra->pbags + pphdr->presetBagNdx, ppbagEnd = hydra->pbags + pphdr[1].presetBagNdx; ppbag != ppbagEnd; ppbag++) + { + struct tsf_hydra_pgen *ppgen, *ppgenEnd; struct tsf_hydra_inst *pinst; struct tsf_hydra_ibag *pibag, *pibagEnd; struct tsf_hydra_igen *pigen, *pigenEnd; + struct tsf_region presetRegion = globalRegion; + int hadGenInstrument = 0; + + // Generators. + for (ppgen = hydra->pgens + ppbag->genNdx, ppgenEnd = hydra->pgens + ppbag[1].genNdx; ppgen != ppgenEnd; ppgen++) + { + // Instrument. + if (ppgen->genOper == GenInstrument) + { + struct tsf_region instRegion; + tsf_u16 whichInst = ppgen->genAmount.wordAmount; + if (whichInst >= hydra->instNum) continue; + + tsf_region_clear(&instRegion, TSF_FALSE); + pinst = &hydra->insts[whichInst]; + for (pibag = hydra->ibags + pinst->instBagNdx, pibagEnd = hydra->ibags + pinst[1].instBagNdx; pibag != pibagEnd; pibag++) + { + // Generators. + struct tsf_region zoneRegion = instRegion; + int hadSampleID = 0; + for (pigen = hydra->igens + pibag->instGenNdx, pigenEnd = hydra->igens + pibag[1].instGenNdx; pigen != pigenEnd; pigen++) + { + if (pigen->genOper == GenSampleID) + { + struct tsf_hydra_shdr* pshdr; + + //preset region key and vel ranges are a filter for the zone regions + if (zoneRegion.hikey < presetRegion.lokey || zoneRegion.lokey > presetRegion.hikey) continue; + if (zoneRegion.hivel < presetRegion.lovel || zoneRegion.lovel > presetRegion.hivel) continue; + if (presetRegion.lokey > zoneRegion.lokey) zoneRegion.lokey = presetRegion.lokey; + if (presetRegion.hikey < zoneRegion.hikey) zoneRegion.hikey = presetRegion.hikey; + if (presetRegion.lovel > zoneRegion.lovel) zoneRegion.lovel = presetRegion.lovel; + if (presetRegion.hivel < zoneRegion.hivel) zoneRegion.hivel = presetRegion.hivel; + + //sum regions + tsf_region_operator(&zoneRegion, 0, TSF_NULL, &presetRegion); + + // EG times need to be converted from timecents to seconds. + tsf_region_envtosecs(&zoneRegion.ampenv, TSF_TRUE); + tsf_region_envtosecs(&zoneRegion.modenv, TSF_FALSE); + + // LFO times need to be converted from timecents to seconds. + zoneRegion.delayModLFO = (zoneRegion.delayModLFO < -11950.0f ? 0.0f : tsf_timecents2Secsf(zoneRegion.delayModLFO)); + zoneRegion.delayVibLFO = (zoneRegion.delayVibLFO < -11950.0f ? 0.0f : tsf_timecents2Secsf(zoneRegion.delayVibLFO)); + + // Fixup sample positions + pshdr = &hydra->shdrs[pigen->genAmount.wordAmount]; + zoneRegion.offset += pshdr->start; + zoneRegion.end += pshdr->end; + zoneRegion.loop_start += pshdr->startLoop; + zoneRegion.loop_end += pshdr->endLoop; + if (pshdr->endLoop > 0) zoneRegion.loop_end -= 1; + if (zoneRegion.loop_end > fontSampleCount) zoneRegion.loop_end = fontSampleCount; + if (zoneRegion.pitch_keycenter == -1) zoneRegion.pitch_keycenter = pshdr->originalPitch; + zoneRegion.tune += pshdr->pitchCorrection; + zoneRegion.sample_rate = pshdr->sampleRate; + if (zoneRegion.end && zoneRegion.end < fontSampleCount) zoneRegion.end++; + else zoneRegion.end = fontSampleCount; + + preset->regions[region_index] = zoneRegion; + region_index++; + hadSampleID = 1; + } + else tsf_region_operator(&zoneRegion, pigen->genOper, &pigen->genAmount, TSF_NULL); + } + + // Handle instrument's global zone. + if (pibag == hydra->ibags + pinst->instBagNdx && !hadSampleID) + instRegion = zoneRegion; + + // Modulators (TODO) + //if (ibag->instModNdx < ibag[1].instModNdx) addUnsupportedOpcode("any modulator"); + } + hadGenInstrument = 1; + } + else tsf_region_operator(&presetRegion, ppgen->genOper, &ppgen->genAmount, TSF_NULL); + } + + // Modulators (TODO) + //if (pbag->modNdx < pbag[1].modNdx) addUnsupportedOpcode("any modulator"); + + // Handle preset's global zone. + if (ppbag == hydra->pbags + pphdr->presetBagNdx && !hadGenInstrument) + globalRegion = presetRegion; + } + } + return 1; +} + +#ifdef STB_VORBIS_INCLUDE_STB_VORBIS_H +static int tsf_decode_ogg(const tsf_u8 *pSmpl, const tsf_u8 *pSmplEnd, float** pRes, tsf_u32* pResNum, tsf_u32* pResMax, tsf_u32 resInitial) +{ + float *res = *pRes, *oldres; tsf_u32 resNum = *pResNum; tsf_u32 resMax = *pResMax; stb_vorbis *v; + + // Use whatever stb_vorbis API that is available (either pull or push) + #if !defined(STB_VORBIS_NO_PULLDATA_API) && !defined(STB_VORBIS_NO_FROMMEMORY) + v = stb_vorbis_open_memory(pSmpl, (int)(pSmplEnd - pSmpl), TSF_NULL, TSF_NULL); + #else + { int use, err; v = stb_vorbis_open_pushdata(pSmpl, (int)(pSmplEnd - pSmpl), &use, &err, TSF_NULL); pSmpl += use; } + #endif + if (v == TSF_NULL) return 0; + + for (;;) + { + float** outputs; int n_samples; + + // Decode one frame of vorbis samples with whatever stb_vorbis API that is available + #if !defined(STB_VORBIS_NO_PULLDATA_API) && !defined(STB_VORBIS_NO_FROMMEMORY) + n_samples = stb_vorbis_get_frame_float(v, TSF_NULL, &outputs); + if (!n_samples) break; + #else + if (pSmpl >= pSmplEnd) break; + { int use = stb_vorbis_decode_frame_pushdata(v, pSmpl, (int)(pSmplEnd - pSmpl), TSF_NULL, &outputs, &n_samples); pSmpl += use; } + if (!n_samples) continue; + #endif + + // Expand our output buffer if necessary then copy over the decoded frame samples + resNum += n_samples; + if (resNum > resMax) + { + do { resMax += (resMax ? (resMax < 1048576 ? resMax : 1048576) : resInitial); } while (resNum > resMax); + oldres = res; + res = (float*)TSF_REALLOC(res, resMax * sizeof(float)); + if (!res) { TSF_FREE(oldres); stb_vorbis_close(v); return 0; } + } + TSF_MEMCPY(res + resNum - n_samples, outputs[0], n_samples * sizeof(float)); + } + stb_vorbis_close(v); + *pRes = res; *pResNum = resNum; *pResMax = resMax; + return 1; +} + +static int tsf_decode_sf3_samples(const void* rawBuffer, float** pFloatBuffer, unsigned int* pSmplCount, struct tsf_hydra *hydra) +{ + const tsf_u8* smplBuffer = (const tsf_u8*)rawBuffer; + tsf_u32 smplLength = *pSmplCount, resNum = 0, resMax = 0, resInitial = (smplLength > 0x100000 ? (smplLength & ~0xFFFFF) : 65536); + float *res = TSF_NULL, *oldres; + int i, shdrLast = hydra->shdrNum - 1, is_sf3 = 0; + for (i = 0; i <= shdrLast; i++) + { + struct tsf_hydra_shdr *shdr = &hydra->shdrs[i]; + if (shdr->sampleType & 0x30) // compression flags (sometimes Vorbis flag) + { + const tsf_u8 *pSmpl = smplBuffer + shdr->start, *pSmplEnd = smplBuffer + shdr->end; + if (pSmpl + 4 > pSmplEnd || !TSF_FourCCEquals(pSmpl, "OggS")) + { + shdr->start = shdr->end = shdr->startLoop = shdr->endLoop = 0; + continue; + } + + // Fix up sample indices in shdr (end index is set after decoding) + shdr->start = resNum; + shdr->startLoop += resNum; + shdr->endLoop += resNum; + if (!tsf_decode_ogg(pSmpl, pSmplEnd, &res, &resNum, &resMax, resInitial)) { TSF_FREE(res); return 0; } + shdr->end = resNum; + is_sf3 = 1; + } + else // raw PCM sample + { + float *out; short *in = (short*)smplBuffer + resNum, *inEnd; tsf_u32 oldResNum = resNum; + if (is_sf3) // Fix up sample indices in shdr + { + tsf_u32 fix_offset = resNum - shdr->start; + in -= fix_offset; + shdr->start = resNum; + shdr->end += fix_offset; + shdr->startLoop += fix_offset; + shdr->endLoop += fix_offset; + } + inEnd = in + ((shdr->end >= shdr->endLoop ? shdr->end : shdr->endLoop) - resNum); + if (i == shdrLast || (tsf_u8*)inEnd > (smplBuffer + smplLength)) inEnd = (short*)(smplBuffer + smplLength); + if (inEnd <= in) continue; + + // expand our output buffer if necessary then convert the PCM data from short to float + resNum += (tsf_u32)(inEnd - in); + if (resNum > resMax) + { + do { resMax += (resMax ? (resMax < 1048576 ? resMax : 1048576) : resInitial); } while (resNum > resMax); + oldres = res; + res = (float*)TSF_REALLOC(res, resMax * sizeof(float)); + if (!res) { TSF_FREE(oldres); return 0; } + } + + // Convert the samples from short to float + for (out = res + oldResNum; in < inEnd;) + *(out++) = (float)(*(in++) / 32767.0); + } + } + + // Trim the sample buffer down then return success (unless out of memory) + if (!(*pFloatBuffer = (float*)TSF_REALLOC(res, resNum * sizeof(float)))) *pFloatBuffer = res; + *pSmplCount = resNum; + return (res ? 1 : 0); +} +#endif + +static int tsf_load_samples(void** pRawBuffer, float** pFloatBuffer, unsigned int* pSmplCount, struct tsf_riffchunk *chunkSmpl, struct tsf_stream* stream) +{ + #ifdef STB_VORBIS_INCLUDE_STB_VORBIS_H + // With OGG Vorbis support we cannot pre-allocate the memory for tsf_decode_sf3_samples + tsf_u32 resNum, resMax; float* oldres; + *pSmplCount = chunkSmpl->size; + *pRawBuffer = (void*)TSF_MALLOC(*pSmplCount); + if (!*pRawBuffer || !stream->read(stream->data, *pRawBuffer, chunkSmpl->size)) return 0; + if (chunkSmpl->id[3] != 'o') return 1; + + // Decode custom .sfo 'smpo' format where all samples are in a single ogg stream + resNum = resMax = 0; + if (!tsf_decode_ogg((tsf_u8*)*pRawBuffer, (tsf_u8*)*pRawBuffer + chunkSmpl->size, pFloatBuffer, &resNum, &resMax, 65536)) return 0; + oldres = *pFloatBuffer; + if (!(*pFloatBuffer = (float*)TSF_REALLOC(*pFloatBuffer, resNum * sizeof(float)))) *pFloatBuffer = oldres; + *pSmplCount = resNum; + return (*pFloatBuffer ? 1 : 0); + #else + // Inline convert the samples from short to float + float *res, *out; const short *in; + (void)pRawBuffer; + *pSmplCount = chunkSmpl->size / (unsigned int)sizeof(short); + *pFloatBuffer = (float*)TSF_MALLOC(*pSmplCount * sizeof(float)); + if (!*pFloatBuffer || !stream->read(stream->data, *pFloatBuffer, chunkSmpl->size)) return 0; + for (res = *pFloatBuffer, out = res + *pSmplCount, in = (short*)res + *pSmplCount; out != res;) + *(--out) = (float)(*(--in) / 32767.0); + return 1; + #endif +} + +static int tsf_voice_envelope_release_samples(struct tsf_voice_envelope* e, float outSampleRate) +{ + return (int)((e->parameters.release <= 0 ? TSF_FASTRELEASETIME : e->parameters.release) * outSampleRate); +} + +static void tsf_voice_envelope_nextsegment(struct tsf_voice_envelope* e, short active_segment, float outSampleRate) +{ + switch (active_segment) + { + case TSF_SEGMENT_NONE: + e->samplesUntilNextSegment = (int)(e->parameters.delay * outSampleRate); + if (e->samplesUntilNextSegment > 0) + { + e->segment = TSF_SEGMENT_DELAY; + e->segmentIsExponential = TSF_FALSE; + e->level = 0.0; + e->slope = 0.0; + return; + } + /* fall through */ + case TSF_SEGMENT_DELAY: + e->samplesUntilNextSegment = (int)(e->parameters.attack * outSampleRate); + if (e->samplesUntilNextSegment > 0) + { + if (!e->isAmpEnv) + { + //mod env attack duration scales with velocity (velocity of 1 is full duration, max velocity is 0.125 times duration) + e->samplesUntilNextSegment = (int)(e->parameters.attack * ((145 - e->midiVelocity) / 144.0f) * outSampleRate); + } + e->segment = TSF_SEGMENT_ATTACK; + e->segmentIsExponential = TSF_FALSE; + e->level = 0.0f; + e->slope = 1.0f / e->samplesUntilNextSegment; + return; + } + /* fall through */ + case TSF_SEGMENT_ATTACK: + e->samplesUntilNextSegment = (int)(e->parameters.hold * outSampleRate); + if (e->samplesUntilNextSegment > 0) + { + e->segment = TSF_SEGMENT_HOLD; + e->segmentIsExponential = TSF_FALSE; + e->level = 1.0f; + e->slope = 0.0f; + return; + } + /* fall through */ + case TSF_SEGMENT_HOLD: + e->samplesUntilNextSegment = (int)(e->parameters.decay * outSampleRate); + if (e->samplesUntilNextSegment > 0) + { + e->segment = TSF_SEGMENT_DECAY; + e->level = 1.0f; + if (e->isAmpEnv) + { + // I don't truly understand this; just following what LinuxSampler does. + float mysterySlope = -9.226f / e->samplesUntilNextSegment; + e->slope = TSF_EXPF(mysterySlope); + e->segmentIsExponential = TSF_TRUE; + if (e->parameters.sustain > 0.0f) + { + // Again, this is following LinuxSampler's example, which is similar to + // SF2-style decay, where "decay" specifies the time it would take to + // get to zero, not to the sustain level. The SFZ spec is not that + // specific about what "decay" means, so perhaps it's really supposed + // to specify the time to reach the sustain level. + e->samplesUntilNextSegment = (int)(TSF_LOG(e->parameters.sustain) / mysterySlope); + } + } + else + { + e->slope = -1.0f / e->samplesUntilNextSegment; + e->samplesUntilNextSegment = (int)(e->parameters.decay * (1.0f - e->parameters.sustain) * outSampleRate); + e->segmentIsExponential = TSF_FALSE; + } + return; + } + /* fall through */ + case TSF_SEGMENT_DECAY: + e->segment = TSF_SEGMENT_SUSTAIN; + e->level = e->parameters.sustain; + e->slope = 0.0f; + e->samplesUntilNextSegment = 0x7FFFFFFF; + e->segmentIsExponential = TSF_FALSE; + return; + case TSF_SEGMENT_SUSTAIN: + e->segment = TSF_SEGMENT_RELEASE; + e->samplesUntilNextSegment = tsf_voice_envelope_release_samples(e, outSampleRate); + if (e->isAmpEnv) + { + // I don't truly understand this; just following what LinuxSampler does. + float mysterySlope = -9.226f / e->samplesUntilNextSegment; + e->slope = TSF_EXPF(mysterySlope); + e->segmentIsExponential = TSF_TRUE; + } + else + { + e->slope = -e->level / e->samplesUntilNextSegment; + e->segmentIsExponential = TSF_FALSE; + } + return; + case TSF_SEGMENT_RELEASE: + default: + e->segment = TSF_SEGMENT_DONE; + e->segmentIsExponential = TSF_FALSE; + e->level = e->slope = 0.0f; + e->samplesUntilNextSegment = 0x7FFFFFF; + } +} + +static void tsf_voice_envelope_setup(struct tsf_voice_envelope* e, struct tsf_envelope* new_parameters, int midiNoteNumber, short midiVelocity, TSF_BOOL isAmpEnv, float outSampleRate) +{ + e->parameters = *new_parameters; + if (e->parameters.keynumToHold) + { + e->parameters.hold += e->parameters.keynumToHold * (60.0f - midiNoteNumber); + e->parameters.hold = (e->parameters.hold < -10000.0f ? 0.0f : tsf_timecents2Secsf(e->parameters.hold)); + } + if (e->parameters.keynumToDecay) + { + e->parameters.decay += e->parameters.keynumToDecay * (60.0f - midiNoteNumber); + e->parameters.decay = (e->parameters.decay < -10000.0f ? 0.0f : tsf_timecents2Secsf(e->parameters.decay)); + } + e->midiVelocity = midiVelocity; + e->isAmpEnv = isAmpEnv; + tsf_voice_envelope_nextsegment(e, TSF_SEGMENT_NONE, outSampleRate); +} + +static void tsf_voice_envelope_process(struct tsf_voice_envelope* e, int numSamples, float outSampleRate) +{ + if (e->slope) + { + if (e->segmentIsExponential) e->level *= TSF_POWF(e->slope, (float)numSamples); + else e->level += (e->slope * numSamples); + } + if ((e->samplesUntilNextSegment -= numSamples) <= 0) + tsf_voice_envelope_nextsegment(e, e->segment, outSampleRate); +} + +static void tsf_voice_lowpass_setup(struct tsf_voice_lowpass* e, float Fc) +{ + // Lowpass filter from http://www.earlevel.com/main/2012/11/26/biquad-c-source-code/ + double K = TSF_TAN(TSF_PI * Fc), KK = K * K; + double norm = 1 / (1 + K * e->QInv + KK); + e->a0 = KK * norm; + e->a1 = 2 * e->a0; + e->b1 = 2 * (KK - 1) * norm; + e->b2 = (1 - K * e->QInv + KK) * norm; +} + +static float tsf_voice_lowpass_process(struct tsf_voice_lowpass* e, double In) +{ + double Out = In * e->a0 + e->z1; e->z1 = In * e->a1 + e->z2 - e->b1 * Out; e->z2 = In * e->a0 - e->b2 * Out; return (float)Out; +} + +static void tsf_voice_lfo_setup(struct tsf_voice_lfo* e, float delay, int freqCents, float outSampleRate) +{ + e->samplesUntil = (int)(delay * outSampleRate); + e->delta = (4.0f * tsf_cents2Hertz((float)freqCents) / outSampleRate); + e->level = 0; +} + +static void tsf_voice_lfo_process(struct tsf_voice_lfo* e, int blockSamples) +{ + if (e->samplesUntil > blockSamples) { e->samplesUntil -= blockSamples; return; } + e->level += e->delta * blockSamples; + if (e->level > 1.0f) { e->delta = -e->delta; e->level = 2.0f - e->level; } + else if (e->level < -1.0f) { e->delta = -e->delta; e->level = -2.0f - e->level; } +} + +static void tsf_voice_kill(struct tsf_voice* v) +{ + v->playingPreset = -1; +} + +static void tsf_voice_end(tsf* f, struct tsf_voice* v) +{ + // if maxVoiceNum is set, assume that voice rendering and note queuing are on separate threads + // so to minimize the chance that voice rendering would advance the segment at the same time + // we just do it twice here and hope that it sticks + int repeats = (f->maxVoiceNum ? 2 : 1); + while (repeats--) + { + tsf_voice_envelope_nextsegment(&v->ampenv, TSF_SEGMENT_SUSTAIN, f->outSampleRate); + tsf_voice_envelope_nextsegment(&v->modenv, TSF_SEGMENT_SUSTAIN, f->outSampleRate); + if (v->region->loop_mode == TSF_LOOPMODE_SUSTAIN) + { + // Continue playing, but stop looping. + v->loopEnd = v->loopStart; + } + } +} + +static void tsf_voice_endquick(tsf* f, struct tsf_voice* v) +{ + // if maxVoiceNum is set, assume that voice rendering and note queuing are on separate threads + // so to minimize the chance that voice rendering would advance the segment at the same time + // we just do it twice here and hope that it sticks + int repeats = (f->maxVoiceNum ? 2 : 1); + while (repeats--) + { + v->ampenv.parameters.release = 0.0f; tsf_voice_envelope_nextsegment(&v->ampenv, TSF_SEGMENT_SUSTAIN, f->outSampleRate); + v->modenv.parameters.release = 0.0f; tsf_voice_envelope_nextsegment(&v->modenv, TSF_SEGMENT_SUSTAIN, f->outSampleRate); + } +} + +static void tsf_voice_calcpitchratio(struct tsf_voice* v, float pitchShift, float outSampleRate) +{ + double note = v->playingKey + v->region->transpose + v->region->tune / 100.0; + double adjustedPitch = v->region->pitch_keycenter + (note - v->region->pitch_keycenter) * (v->region->pitch_keytrack / 100.0); + if (pitchShift) adjustedPitch += pitchShift; + v->pitchInputTimecents = adjustedPitch * 100.0; + v->pitchOutputFactor = v->region->sample_rate / (tsf_timecents2Secsd(v->region->pitch_keycenter * 100.0) * outSampleRate); +} + +static void tsf_voice_render(tsf* f, struct tsf_voice* v, float* outputBuffer, int numSamples) +{ + struct tsf_region* region = v->region; + float* input = f->fontSamples; + float* outL = outputBuffer; + float* outR = (f->outputmode == TSF_STEREO_UNWEAVED ? outL + numSamples : TSF_NULL); + + // Cache some values, to give them at least some chance of ending up in registers. + TSF_BOOL updateModEnv = (region->modEnvToPitch || region->modEnvToFilterFc); + TSF_BOOL updateModLFO = (v->modlfo.delta && (region->modLfoToPitch || region->modLfoToFilterFc || region->modLfoToVolume)); + TSF_BOOL updateVibLFO = (v->viblfo.delta && (region->vibLfoToPitch)); + TSF_BOOL isLooping = (v->loopStart < v->loopEnd); + unsigned int tmpLoopStart = v->loopStart, tmpLoopEnd = v->loopEnd; + double tmpSampleEndDbl = (double)region->end, tmpLoopEndDbl = (double)tmpLoopEnd + 1.0; + double tmpSourceSamplePosition = v->sourceSamplePosition; + struct tsf_voice_lowpass tmpLowpass = v->lowpass; + + TSF_BOOL dynamicLowpass = (region->modLfoToFilterFc || region->modEnvToFilterFc); + float tmpSampleRate = f->outSampleRate, tmpInitialFilterFc, tmpModLfoToFilterFc, tmpModEnvToFilterFc; + + TSF_BOOL dynamicPitchRatio = (region->modLfoToPitch || region->modEnvToPitch || region->vibLfoToPitch); + double pitchRatio; + float tmpModLfoToPitch, tmpVibLfoToPitch, tmpModEnvToPitch; + + TSF_BOOL dynamicGain = (region->modLfoToVolume != 0); + float noteGain = 0, tmpModLfoToVolume; + + if (dynamicLowpass) tmpInitialFilterFc = (float)region->initialFilterFc, tmpModLfoToFilterFc = (float)region->modLfoToFilterFc, tmpModEnvToFilterFc = (float)region->modEnvToFilterFc; + else tmpInitialFilterFc = 0, tmpModLfoToFilterFc = 0, tmpModEnvToFilterFc = 0; + + if (dynamicPitchRatio) pitchRatio = 0, tmpModLfoToPitch = (float)region->modLfoToPitch, tmpVibLfoToPitch = (float)region->vibLfoToPitch, tmpModEnvToPitch = (float)region->modEnvToPitch; + else pitchRatio = tsf_timecents2Secsd(v->pitchInputTimecents) * v->pitchOutputFactor, tmpModLfoToPitch = 0, tmpVibLfoToPitch = 0, tmpModEnvToPitch = 0; + + if (dynamicGain) tmpModLfoToVolume = (float)region->modLfoToVolume * 0.1f; + else noteGain = tsf_decibelsToGain(v->noteGainDB), tmpModLfoToVolume = 0; + + while (numSamples) + { + float gainMono, gainLeft, gainRight; + int blockSamples = (numSamples > TSF_RENDER_EFFECTSAMPLEBLOCK ? TSF_RENDER_EFFECTSAMPLEBLOCK : numSamples); + numSamples -= blockSamples; + + if (dynamicLowpass) + { + float fres = tmpInitialFilterFc + v->modlfo.level * tmpModLfoToFilterFc + v->modenv.level * tmpModEnvToFilterFc; + float lowpassFc = (fres <= 13500 ? tsf_cents2Hertz(fres) / tmpSampleRate : 1.0f); + tmpLowpass.active = (lowpassFc < 0.499f); + if (tmpLowpass.active) tsf_voice_lowpass_setup(&tmpLowpass, lowpassFc); + } + + if (dynamicPitchRatio) + pitchRatio = tsf_timecents2Secsd(v->pitchInputTimecents + (v->modlfo.level * tmpModLfoToPitch + v->viblfo.level * tmpVibLfoToPitch + v->modenv.level * tmpModEnvToPitch)) * v->pitchOutputFactor; + + if (dynamicGain) + noteGain = tsf_decibelsToGain(v->noteGainDB + (v->modlfo.level * tmpModLfoToVolume)); + + gainMono = noteGain * v->ampenv.level; + + // Update EG. + tsf_voice_envelope_process(&v->ampenv, blockSamples, tmpSampleRate); + if (updateModEnv) tsf_voice_envelope_process(&v->modenv, blockSamples, tmpSampleRate); + + // Update LFOs. + if (updateModLFO) tsf_voice_lfo_process(&v->modlfo, blockSamples); + if (updateVibLFO) tsf_voice_lfo_process(&v->viblfo, blockSamples); + + switch (f->outputmode) + { + case TSF_STEREO_INTERLEAVED: + gainLeft = gainMono * v->panFactorLeft, gainRight = gainMono * v->panFactorRight; + while (blockSamples-- && tmpSourceSamplePosition < tmpSampleEndDbl) + { + unsigned int pos = (unsigned int)tmpSourceSamplePosition, nextPos = (pos >= tmpLoopEnd && isLooping ? tmpLoopStart : pos + 1); + + // Simple linear interpolation. + float alpha = (float)(tmpSourceSamplePosition - pos), val = (input[pos] * (1.0f - alpha) + input[nextPos] * alpha); + + // Low-pass filter. + if (tmpLowpass.active) val = tsf_voice_lowpass_process(&tmpLowpass, val); + + *outL++ += val * gainLeft; + *outL++ += val * gainRight; + + // Next sample. + tmpSourceSamplePosition += pitchRatio; + if (tmpSourceSamplePosition >= tmpLoopEndDbl && isLooping) tmpSourceSamplePosition -= (tmpLoopEnd - tmpLoopStart + 1.0); + } + break; + + case TSF_STEREO_UNWEAVED: + gainLeft = gainMono * v->panFactorLeft, gainRight = gainMono * v->panFactorRight; + while (blockSamples-- && tmpSourceSamplePosition < tmpSampleEndDbl) + { + unsigned int pos = (unsigned int)tmpSourceSamplePosition, nextPos = (pos >= tmpLoopEnd && isLooping ? tmpLoopStart : pos + 1); + + // Simple linear interpolation. + float alpha = (float)(tmpSourceSamplePosition - pos), val = (input[pos] * (1.0f - alpha) + input[nextPos] * alpha); + + // Low-pass filter. + if (tmpLowpass.active) val = tsf_voice_lowpass_process(&tmpLowpass, val); + + *outL++ += val * gainLeft; + *outR++ += val * gainRight; + + // Next sample. + tmpSourceSamplePosition += pitchRatio; + if (tmpSourceSamplePosition >= tmpLoopEndDbl && isLooping) tmpSourceSamplePosition -= (tmpLoopEnd - tmpLoopStart + 1.0); + } + break; + + case TSF_MONO: + while (blockSamples-- && tmpSourceSamplePosition < tmpSampleEndDbl) + { + unsigned int pos = (unsigned int)tmpSourceSamplePosition, nextPos = (pos >= tmpLoopEnd && isLooping ? tmpLoopStart : pos + 1); + + // Simple linear interpolation. + float alpha = (float)(tmpSourceSamplePosition - pos), val = (input[pos] * (1.0f - alpha) + input[nextPos] * alpha); + + // Low-pass filter. + if (tmpLowpass.active) val = tsf_voice_lowpass_process(&tmpLowpass, val); + + *outL++ += val * gainMono; + + // Next sample. + tmpSourceSamplePosition += pitchRatio; + if (tmpSourceSamplePosition >= tmpLoopEndDbl && isLooping) tmpSourceSamplePosition -= (tmpLoopEnd - tmpLoopStart + 1.0); + } + break; + } + + if (tmpSourceSamplePosition >= tmpSampleEndDbl || v->ampenv.segment == TSF_SEGMENT_DONE) + { + tsf_voice_kill(v); + return; + } + } + + v->sourceSamplePosition = tmpSourceSamplePosition; + if (tmpLowpass.active || dynamicLowpass) v->lowpass = tmpLowpass; +} + +TSFDEF tsf* tsf_load(struct tsf_stream* stream) +{ + tsf* res = TSF_NULL; + struct tsf_riffchunk chunkHead; + struct tsf_riffchunk chunkList; + struct tsf_hydra hydra; + void* rawBuffer = TSF_NULL; + float* floatBuffer = TSF_NULL; + tsf_u32 smplCount = 0; + + if (!tsf_riffchunk_read(TSF_NULL, &chunkHead, stream) || !TSF_FourCCEquals(chunkHead.id, "sfbk")) + { + //if (e) *e = TSF_INVALID_NOSF2HEADER; + return res; + } + + // Read hydra and locate sample data. + TSF_MEMSET(&hydra, 0, sizeof(hydra)); + while (tsf_riffchunk_read(&chunkHead, &chunkList, stream)) + { + struct tsf_riffchunk chunk; + if (TSF_FourCCEquals(chunkList.id, "pdta")) + { + while (tsf_riffchunk_read(&chunkList, &chunk, stream)) + { + #define HandleChunk(chunkName) (TSF_FourCCEquals(chunk.id, #chunkName) && !(chunk.size % chunkName##SizeInFile)) \ + { \ + int num = chunk.size / chunkName##SizeInFile, i; \ + hydra.chunkName##Num = num; \ + hydra.chunkName##s = (struct tsf_hydra_##chunkName*)TSF_MALLOC(num * sizeof(struct tsf_hydra_##chunkName)); \ + if (!hydra.chunkName##s) goto out_of_memory; \ + for (i = 0; i < num; ++i) tsf_hydra_read_##chunkName(&hydra.chunkName##s[i], stream); \ + } + enum + { + phdrSizeInFile = 38, pbagSizeInFile = 4, pmodSizeInFile = 10, + pgenSizeInFile = 4, instSizeInFile = 22, ibagSizeInFile = 4, + imodSizeInFile = 10, igenSizeInFile = 4, shdrSizeInFile = 46 + }; + if HandleChunk(phdr) else if HandleChunk(pbag) else if HandleChunk(pmod) + else if HandleChunk(pgen) else if HandleChunk(inst) else if HandleChunk(ibag) + else if HandleChunk(imod) else if HandleChunk(igen) else if HandleChunk(shdr) + else stream->skip(stream->data, chunk.size); + #undef HandleChunk + } + } + else if (TSF_FourCCEquals(chunkList.id, "sdta")) + { + while (tsf_riffchunk_read(&chunkList, &chunk, stream)) + { + if ((TSF_FourCCEquals(chunk.id, "smpl") + #ifdef STB_VORBIS_INCLUDE_STB_VORBIS_H + || TSF_FourCCEquals(chunk.id, "smpo") + #endif + ) && !rawBuffer && !floatBuffer && chunk.size >= sizeof(short)) + { + if (!tsf_load_samples(&rawBuffer, &floatBuffer, &smplCount, &chunk, stream)) goto out_of_memory; + } + else stream->skip(stream->data, chunk.size); + } + } + else stream->skip(stream->data, chunkList.size); + } + if (!hydra.phdrs || !hydra.pbags || !hydra.pmods || !hydra.pgens || !hydra.insts || !hydra.ibags || !hydra.imods || !hydra.igens || !hydra.shdrs) + { + //if (e) *e = TSF_INVALID_INCOMPLETE; + } + else if (!rawBuffer && !floatBuffer) + { + //if (e) *e = TSF_INVALID_NOSAMPLEDATA; + } + else + { + #ifdef STB_VORBIS_INCLUDE_STB_VORBIS_H + if (!floatBuffer && !tsf_decode_sf3_samples(rawBuffer, &floatBuffer, &smplCount, &hydra)) goto out_of_memory; + #endif + res = (tsf*)TSF_MALLOC(sizeof(tsf)); + if (res) TSF_MEMSET(res, 0, sizeof(tsf)); + if (!res || !tsf_load_presets(res, &hydra, smplCount)) goto out_of_memory; + res->outSampleRate = 44100.0f; + res->fontSamples = floatBuffer; + floatBuffer = TSF_NULL; // don't free below + } + if (0) + { + out_of_memory: + TSF_FREE(res); + res = TSF_NULL; + //if (e) *e = TSF_OUT_OF_MEMORY; + } + TSF_FREE(hydra.phdrs); TSF_FREE(hydra.pbags); TSF_FREE(hydra.pmods); + TSF_FREE(hydra.pgens); TSF_FREE(hydra.insts); TSF_FREE(hydra.ibags); + TSF_FREE(hydra.imods); TSF_FREE(hydra.igens); TSF_FREE(hydra.shdrs); + TSF_FREE(rawBuffer); TSF_FREE(floatBuffer); + return res; +} + +TSFDEF tsf* tsf_copy(tsf* f) +{ + tsf* res; + if (!f) return TSF_NULL; + if (!f->refCount) + { + f->refCount = (int*)TSF_MALLOC(sizeof(int)); + if (!f->refCount) return TSF_NULL; + *f->refCount = 1; + } + res = (tsf*)TSF_MALLOC(sizeof(tsf)); + if (!res) return TSF_NULL; + TSF_MEMCPY(res, f, sizeof(tsf)); + res->voices = TSF_NULL; + res->voiceNum = 0; + res->channels = TSF_NULL; + (*res->refCount)++; + return res; +} + +TSFDEF void tsf_close(tsf* f) +{ + if (!f) return; + if (!f->refCount || !--(*f->refCount)) + { + struct tsf_preset *preset = f->presets, *presetEnd = preset + f->presetNum; + for (; preset != presetEnd; preset++) TSF_FREE(preset->regions); + TSF_FREE(f->presets); + TSF_FREE(f->fontSamples); + TSF_FREE(f->refCount); + } + TSF_FREE(f->channels); + TSF_FREE(f->voices); + TSF_FREE(f); +} + +TSFDEF void tsf_reset(tsf* f) +{ + struct tsf_voice *v = f->voices, *vEnd = v + f->voiceNum; + for (; v != vEnd; v++) + if (v->playingPreset != -1 && (v->ampenv.segment < TSF_SEGMENT_RELEASE || v->ampenv.parameters.release)) + tsf_voice_endquick(f, v); + if (f->channels) { TSF_FREE(f->channels); f->channels = TSF_NULL; } +} + +TSFDEF int tsf_get_presetindex(const tsf* f, int bank, int preset_number) +{ + const struct tsf_preset *presets; + int i, iMax; + for (presets = f->presets, i = 0, iMax = f->presetNum; i < iMax; i++) + if (presets[i].preset == preset_number && presets[i].bank == bank) + return i; + return -1; +} + +TSFDEF int tsf_get_presetcount(const tsf* f) +{ + return f->presetNum; +} + +TSFDEF const char* tsf_get_presetname(const tsf* f, int preset) +{ + return (preset < 0 || preset >= f->presetNum ? TSF_NULL : f->presets[preset].presetName); +} + +TSFDEF const char* tsf_bank_get_presetname(const tsf* f, int bank, int preset_number) +{ + return tsf_get_presetname(f, tsf_get_presetindex(f, bank, preset_number)); +} + +TSFDEF void tsf_set_output(tsf* f, enum TSFOutputMode outputmode, int samplerate, float global_gain_db) +{ + f->outputmode = outputmode; + f->outSampleRate = (float)(samplerate >= 1 ? samplerate : 44100.0f); + f->globalGainDB = global_gain_db; +} + +TSFDEF void tsf_set_volume(tsf* f, float global_volume) +{ + f->globalGainDB = (global_volume == 1.0f ? 0 : -tsf_gainToDecibels(1.0f / global_volume)); +} + +TSFDEF int tsf_set_max_voices(tsf* f, int max_voices) +{ + int i = f->voiceNum; + int newVoiceNum = (f->voiceNum > max_voices ? f->voiceNum : max_voices); + struct tsf_voice *newVoices = (struct tsf_voice*)TSF_REALLOC(f->voices, newVoiceNum * sizeof(struct tsf_voice)); + if (!newVoices) return 0; + f->voices = newVoices; + f->voiceNum = f->maxVoiceNum = newVoiceNum; + for (; i < max_voices; i++) + f->voices[i].playingPreset = -1; + return 1; +} + +TSFDEF int tsf_note_on(tsf* f, int preset_index, int key, float vel) +{ + short midiVelocity = (short)(vel * 127); + unsigned int voicePlayIndex; + struct tsf_region *region, *regionEnd; + + if (preset_index < 0 || preset_index >= f->presetNum) return 1; + if (vel <= 0.0f) { tsf_note_off(f, preset_index, key); return 1; } + + // Play all matching regions. + voicePlayIndex = f->voicePlayIndex++; + for (region = f->presets[preset_index].regions, regionEnd = region + f->presets[preset_index].regionNum; region != regionEnd; region++) + { + struct tsf_voice *voice, *v, *vEnd; TSF_BOOL doLoop; float lowpassFilterQDB, lowpassFc; + if (key < region->lokey || key > region->hikey || midiVelocity < region->lovel || midiVelocity > region->hivel) continue; + + voice = TSF_NULL, v = f->voices, vEnd = v + f->voiceNum; + if (region->group) + { + for (; v != vEnd; v++) + if (v->playingPreset == preset_index && v->region->group == region->group) tsf_voice_endquick(f, v); + else if (v->playingPreset == -1 && !voice) voice = v; + } + else for (; v != vEnd; v++) if (v->playingPreset == -1) { voice = v; break; } + + if (!voice) + { + if (f->maxVoiceNum) + { + // Voices have been pre-allocated and limited to a maximum, try to kill a voice off in its release envelope + int bestKillReleaseSamplePos = -999999999; + for (v = f->voices; v != vEnd; v++) + { + if (v->ampenv.segment == TSF_SEGMENT_RELEASE) + { + // We're looking for the voice furthest into its release + int releaseSamplesDone = tsf_voice_envelope_release_samples(&v->ampenv, f->outSampleRate) - v->ampenv.samplesUntilNextSegment; + if (releaseSamplesDone > bestKillReleaseSamplePos) + { + bestKillReleaseSamplePos = releaseSamplesDone; + voice = v; + } + } + } + if (!voice) + continue; + tsf_voice_kill(voice); + } + else + { + // Allocate more voices so we don't need to kill one off. + struct tsf_voice* newVoices; + f->voiceNum += 4; + newVoices = (struct tsf_voice*)TSF_REALLOC(f->voices, f->voiceNum * sizeof(struct tsf_voice)); + if (!newVoices) return 0; + f->voices = newVoices; + voice = &f->voices[f->voiceNum - 4]; + voice[1].playingPreset = voice[2].playingPreset = voice[3].playingPreset = -1; + } + } + + voice->region = region; + voice->playingPreset = preset_index; + voice->playingKey = key; + voice->playIndex = voicePlayIndex; + voice->heldSustain = 0; + voice->noteGainDB = f->globalGainDB - region->attenuation - tsf_gainToDecibels(1.0f / vel); + + if (f->channels) + { + f->channels->setupVoice(f, voice); + } + else + { + tsf_voice_calcpitchratio(voice, 0, f->outSampleRate); + // The SFZ spec is silent about the pan curve, but a 3dB pan law seems common. This sqrt() curve matches what Dimension LE does; Alchemy Free seems closer to sin(adjustedPan * pi/2). + voice->panFactorLeft = TSF_SQRTF(0.5f - region->pan); + voice->panFactorRight = TSF_SQRTF(0.5f + region->pan); + } + + // Offset/end. + voice->sourceSamplePosition = region->offset; + + // Loop. + doLoop = (region->loop_mode != TSF_LOOPMODE_NONE && region->loop_start < region->loop_end); + voice->loopStart = (doLoop ? region->loop_start : 0); + voice->loopEnd = (doLoop ? region->loop_end : 0); + + // Setup envelopes. + tsf_voice_envelope_setup(&voice->ampenv, ®ion->ampenv, key, midiVelocity, TSF_TRUE, f->outSampleRate); + tsf_voice_envelope_setup(&voice->modenv, ®ion->modenv, key, midiVelocity, TSF_FALSE, f->outSampleRate); + + // Setup lowpass filter. + lowpassFc = (region->initialFilterFc <= 13500 ? tsf_cents2Hertz((float)region->initialFilterFc) / f->outSampleRate : 1.0f); + lowpassFilterQDB = region->initialFilterQ / 10.0f; + voice->lowpass.QInv = 1.0 / TSF_POW(10.0, (lowpassFilterQDB / 20.0)); + voice->lowpass.z1 = voice->lowpass.z2 = 0; + voice->lowpass.active = (lowpassFc < 0.499f); + if (voice->lowpass.active) tsf_voice_lowpass_setup(&voice->lowpass, lowpassFc); + + // Setup LFO filters. + tsf_voice_lfo_setup(&voice->modlfo, region->delayModLFO, region->freqModLFO, f->outSampleRate); + tsf_voice_lfo_setup(&voice->viblfo, region->delayVibLFO, region->freqVibLFO, f->outSampleRate); + } + return 1; +} + +TSFDEF int tsf_bank_note_on(tsf* f, int bank, int preset_number, int key, float vel) +{ + int preset_index = tsf_get_presetindex(f, bank, preset_number); + if (preset_index == -1) return 0; + return tsf_note_on(f, preset_index, key, vel); +} + +TSFDEF void tsf_note_off(tsf* f, int preset_index, int key) +{ + struct tsf_voice *v = f->voices, *vEnd = v + f->voiceNum, *vMatchFirst = TSF_NULL, *vMatchLast = TSF_NULL; + for (; v != vEnd; v++) + { + //Find the first and last entry in the voices list with matching preset, key and look up the smallest play index + if (v->playingPreset != preset_index || v->playingKey != key || v->ampenv.segment >= TSF_SEGMENT_RELEASE) continue; + else if (!vMatchFirst || v->playIndex < vMatchFirst->playIndex) vMatchFirst = vMatchLast = v; + else if (v->playIndex == vMatchFirst->playIndex) vMatchLast = v; + } + if (!vMatchFirst) return; + for (v = vMatchFirst; v <= vMatchLast; v++) + { + //Stop all voices with matching preset, key and the smallest play index which was enumerated above + if (v != vMatchFirst && v != vMatchLast && + (v->playIndex != vMatchFirst->playIndex || v->playingPreset != preset_index || v->playingKey != key || v->ampenv.segment >= TSF_SEGMENT_RELEASE)) continue; + tsf_voice_end(f, v); + } +} + +TSFDEF int tsf_bank_note_off(tsf* f, int bank, int preset_number, int key) +{ + int preset_index = tsf_get_presetindex(f, bank, preset_number); + if (preset_index == -1) return 0; + tsf_note_off(f, preset_index, key); + return 1; +} + +TSFDEF void tsf_note_off_all(tsf* f) +{ + struct tsf_voice *v = f->voices, *vEnd = v + f->voiceNum; + for (; v != vEnd; v++) if (v->playingPreset != -1 && v->ampenv.segment < TSF_SEGMENT_RELEASE) + tsf_voice_end(f, v); +} + +TSFDEF int tsf_active_voice_count(tsf* f) +{ + int count = 0; + struct tsf_voice *v = f->voices, *vEnd = v + f->voiceNum; + for (; v != vEnd; v++) if (v->playingPreset != -1) count++; + return count; +} + +TSFDEF void tsf_render_short(tsf* f, short* buffer, int samples, int flag_mixing) +{ + float outputSamples[TSF_RENDER_SHORTBUFFERBLOCK]; + int channels = (f->outputmode == TSF_MONO ? 1 : 2), maxChannelSamples = TSF_RENDER_SHORTBUFFERBLOCK / channels; + while (samples > 0) + { + int channelSamples = (samples > maxChannelSamples ? maxChannelSamples : samples); + short* bufferEnd = buffer + channelSamples * channels; + float *floatSamples = outputSamples; + tsf_render_float(f, floatSamples, channelSamples, TSF_FALSE); + samples -= channelSamples; + + if (flag_mixing) + while (buffer != bufferEnd) + { + float v = *floatSamples++; + int vi = *buffer + (v < -1.00004566f ? (int)-32768 : (v > 1.00001514f ? (int)32767 : (int)(v * 32767.5f))); + *buffer++ = (vi < -32768 ? (short)-32768 : (vi > 32767 ? (short)32767 : (short)vi)); + } + else + while (buffer != bufferEnd) + { + float v = *floatSamples++; + *buffer++ = (v < -1.00004566f ? (short)-32768 : (v > 1.00001514f ? (short)32767 : (short)(v * 32767.5f))); + } + } +} + +TSFDEF void tsf_render_float(tsf* f, float* buffer, int samples, int flag_mixing) +{ + struct tsf_voice *v = f->voices, *vEnd = v + f->voiceNum; + if (!flag_mixing) TSF_MEMSET(buffer, 0, (f->outputmode == TSF_MONO ? 1 : 2) * sizeof(float) * samples); + for (; v != vEnd; v++) + if (v->playingPreset != -1) + tsf_voice_render(f, v, buffer, samples); +} + +static void tsf_channel_setup_voice(tsf* f, struct tsf_voice* v) +{ + struct tsf_channel* c = &f->channels->channels[f->channels->activeChannel]; + float newpan = v->region->pan + c->panOffset; + v->playingChannel = f->channels->activeChannel; + v->noteGainDB += c->gainDB; + tsf_voice_calcpitchratio(v, (c->pitchWheel == 8192 ? c->tuning : ((c->pitchWheel / 16383.0f * c->pitchRange * 2.0f) - c->pitchRange + c->tuning)), f->outSampleRate); + if (newpan <= -0.5f) { v->panFactorLeft = 1.0f; v->panFactorRight = 0.0f; } + else if (newpan >= 0.5f) { v->panFactorLeft = 0.0f; v->panFactorRight = 1.0f; } + else { v->panFactorLeft = TSF_SQRTF(0.5f - newpan); v->panFactorRight = TSF_SQRTF(0.5f + newpan); } +} + +static struct tsf_channel* tsf_channel_init(tsf* f, int channel) +{ + int i; + if (f->channels && channel < f->channels->channelNum) return &f->channels->channels[channel]; + if (!f->channels) + { + f->channels = (struct tsf_channels*)TSF_MALLOC(sizeof(struct tsf_channels) + sizeof(struct tsf_channel) * channel); + if (!f->channels) return TSF_NULL; + f->channels->setupVoice = &tsf_channel_setup_voice; + f->channels->channelNum = 0; + f->channels->activeChannel = 0; + } + else + { + struct tsf_channels *newChannels = (struct tsf_channels*)TSF_REALLOC(f->channels, sizeof(struct tsf_channels) + sizeof(struct tsf_channel) * channel); + if (!newChannels) return TSF_NULL; + f->channels = newChannels; + } + i = f->channels->channelNum; + f->channels->channelNum = channel + 1; + for (; i <= channel; i++) + { + struct tsf_channel* c = &f->channels->channels[i]; + c->presetIndex = c->bank = 0; + c->pitchWheel = c->midiPan = 8192; + c->midiVolume = c->midiExpression = 16383; + c->midiRPN = 0xFFFF; + c->midiData = c->sustain = 0; + c->panOffset = 0.0f; + c->gainDB = 0.0f; + c->pitchRange = 2.0f; + c->tuning = 0.0f; + } + return &f->channels->channels[channel]; +} + +static void tsf_channel_applypitch(tsf* f, int channel, struct tsf_channel* c) +{ + struct tsf_voice *v, *vEnd; + float pitchShift = (c->pitchWheel == 8192 ? c->tuning : ((c->pitchWheel / 16383.0f * c->pitchRange * 2.0f) - c->pitchRange + c->tuning)); + for (v = f->voices, vEnd = v + f->voiceNum; v != vEnd; v++) + if (v->playingPreset != -1 && v->playingChannel == channel) + tsf_voice_calcpitchratio(v, pitchShift, f->outSampleRate); +} + +TSFDEF int tsf_channel_set_presetindex(tsf* f, int channel, int preset_index) +{ + struct tsf_channel *c = tsf_channel_init(f, channel); + if (!c) return 0; + c->presetIndex = (unsigned short)preset_index; + return 1; +} + +TSFDEF int tsf_channel_set_presetnumber(tsf* f, int channel, int preset_number, int flag_mididrums) +{ + int preset_index; + struct tsf_channel *c = tsf_channel_init(f, channel); + if (!c) return 0; + if (flag_mididrums) + { + preset_index = tsf_get_presetindex(f, 128 | (c->bank & 0x7FFF), preset_number); + if (preset_index == -1) preset_index = tsf_get_presetindex(f, 128, preset_number); + if (preset_index == -1) preset_index = tsf_get_presetindex(f, 128, 0); + if (preset_index == -1) preset_index = tsf_get_presetindex(f, (c->bank & 0x7FFF), preset_number); + } + else preset_index = tsf_get_presetindex(f, (c->bank & 0x7FFF), preset_number); + if (preset_index == -1) preset_index = tsf_get_presetindex(f, 0, preset_number); + if (preset_index != -1) + { + c->presetIndex = (unsigned short)preset_index; + return 1; + } + return 0; +} + +TSFDEF int tsf_channel_set_bank(tsf* f, int channel, int bank) +{ + struct tsf_channel *c = tsf_channel_init(f, channel); + if (!c) return 0; + c->bank = (unsigned short)bank; + return 1; +} + +TSFDEF int tsf_channel_set_bank_preset(tsf* f, int channel, int bank, int preset_number) +{ + int preset_index; + struct tsf_channel *c = tsf_channel_init(f, channel); + if (!c) return 0; + preset_index = tsf_get_presetindex(f, bank, preset_number); + if (preset_index == -1) return 0; + c->presetIndex = (unsigned short)preset_index; + c->bank = (unsigned short)bank; + return 1; +} + +TSFDEF int tsf_channel_set_pan(tsf* f, int channel, float pan) +{ + struct tsf_voice *v, *vEnd; + struct tsf_channel *c = tsf_channel_init(f, channel); + if (!c) return 0; + for (v = f->voices, vEnd = v + f->voiceNum; v != vEnd; v++) + if (v->playingPreset != -1 && v->playingChannel == channel) + { + float newpan = v->region->pan + pan - 0.5f; + if (newpan <= -0.5f) { v->panFactorLeft = 1.0f; v->panFactorRight = 0.0f; } + else if (newpan >= 0.5f) { v->panFactorLeft = 0.0f; v->panFactorRight = 1.0f; } + else { v->panFactorLeft = TSF_SQRTF(0.5f - newpan); v->panFactorRight = TSF_SQRTF(0.5f + newpan); } + } + c->panOffset = pan - 0.5f; + return 1; +} + +TSFDEF int tsf_channel_set_volume(tsf* f, int channel, float volume) +{ + float gainDB = tsf_gainToDecibels(volume), gainDBChange; + struct tsf_voice *v, *vEnd; + struct tsf_channel *c = tsf_channel_init(f, channel); + if (!c) return 0; + if (gainDB == c->gainDB) return 1; + for (v = f->voices, vEnd = v + f->voiceNum, gainDBChange = gainDB - c->gainDB; v != vEnd; v++) + if (v->playingPreset != -1 && v->playingChannel == channel) + v->noteGainDB += gainDBChange; + c->gainDB = gainDB; + return 1; +} + +TSFDEF int tsf_channel_set_pitchwheel(tsf* f, int channel, int pitch_wheel) +{ + struct tsf_channel *c = tsf_channel_init(f, channel); + if (!c) return 0; + if (c->pitchWheel == pitch_wheel) return 1; + c->pitchWheel = (unsigned short)pitch_wheel; + tsf_channel_applypitch(f, channel, c); + return 1; +} + +TSFDEF int tsf_channel_set_pitchrange(tsf* f, int channel, float pitch_range) +{ + struct tsf_channel *c = tsf_channel_init(f, channel); + if (!c) return 0; + if (c->pitchRange == pitch_range) return 1; + c->pitchRange = pitch_range; + if (c->pitchWheel != 8192) tsf_channel_applypitch(f, channel, c); + return 1; +} + +TSFDEF int tsf_channel_set_tuning(tsf* f, int channel, float tuning) +{ + struct tsf_channel *c = tsf_channel_init(f, channel); + if (!c) return 0; + if (c->tuning == tuning) return 1; + c->tuning = tuning; + tsf_channel_applypitch(f, channel, c); + return 1; +} + +TSFDEF int tsf_channel_set_sustain(tsf* f, int channel, int flag_sustain) +{ + struct tsf_channel *c = tsf_channel_init(f, channel); + if (!c) return 0; + if (!c->sustain == !flag_sustain) return 1; + c->sustain = (unsigned short)(flag_sustain != 0); + //Turning on sustain does no action now, just starts note_off behaving differently + if (flag_sustain) return 1; + //Turning off sustain, actually end voices that got a note_off and were set to heldSustain status + struct tsf_voice *v = f->voices, *vEnd = v + f->voiceNum; + for (; v != vEnd; v++) + if (v->playingPreset != -1 && v->playingChannel == channel && v->ampenv.segment < TSF_SEGMENT_RELEASE && v->heldSustain) + tsf_voice_end(f, v); + return 1; +} + +TSFDEF int tsf_channel_note_on(tsf* f, int channel, int key, float vel) +{ + if (!f->channels || channel >= f->channels->channelNum) return 1; + f->channels->activeChannel = channel; + if (!vel) + { + tsf_channel_note_off(f, channel, key); + return 1; + } + return tsf_note_on(f, f->channels->channels[channel].presetIndex, key, vel); +} + +TSFDEF void tsf_channel_note_off(tsf* f, int channel, int key) +{ + unsigned sustain; + struct tsf_voice *v = f->voices, *vEnd = v + f->voiceNum, *vMatchFirst = TSF_NULL, *vMatchLast = TSF_NULL; + for (; v != vEnd; v++) + { + //Find the first and last entry in the voices list with matching channel, key and look up the smallest play index + if (v->playingPreset == -1 || v->playingChannel != channel || v->playingKey != key || v->ampenv.segment >= TSF_SEGMENT_RELEASE || v->heldSustain) continue; + else if (!vMatchFirst || v->playIndex < vMatchFirst->playIndex) vMatchFirst = vMatchLast = v; + else if (v->playIndex == vMatchFirst->playIndex) vMatchLast = v; + } + if (!vMatchFirst) return; + for (sustain = f->channels->channels[channel].sustain, v = vMatchFirst; v <= vMatchLast; v++) + { + //Stop all voices with matching channel, key and the smallest play index which was enumerated above + if (v != vMatchFirst && v != vMatchLast && + (v->playIndex != vMatchFirst->playIndex || v->playingPreset == -1 || v->playingChannel != channel || v->playingKey != key || v->ampenv.segment >= TSF_SEGMENT_RELEASE)) continue; + //Don't turn off if sustain is active, just mark as held by sustain so we don't forget it + if (sustain) + v->heldSustain = 1; + else + tsf_voice_end(f, v); + } +} + +TSFDEF void tsf_channel_note_off_all(tsf* f, int channel) +{ + //Ignore sustain channel settings, note_off_all overrides + struct tsf_voice *v = f->voices, *vEnd = v + f->voiceNum; + for (; v != vEnd; v++) + if (v->playingPreset != -1 && v->playingChannel == channel && v->ampenv.segment < TSF_SEGMENT_RELEASE) + tsf_voice_end(f, v); +} + +TSFDEF void tsf_channel_sounds_off_all(tsf* f, int channel) +{ + struct tsf_voice *v = f->voices, *vEnd = v + f->voiceNum; + for (; v != vEnd; v++) + if (v->playingPreset != -1 && v->playingChannel == channel && (v->ampenv.segment < TSF_SEGMENT_RELEASE || v->ampenv.parameters.release)) + tsf_voice_endquick(f, v); +} + +TSFDEF int tsf_channel_midi_control(tsf* f, int channel, int controller, int control_value) +{ + struct tsf_channel* c = tsf_channel_init(f, channel); + if (!c) return 0; + switch (controller) + { + case 7 /*VOLUME_MSB*/ : c->midiVolume = (unsigned short)((c->midiVolume & 0x7F ) | (control_value << 7)); goto TCMC_SET_VOLUME; + case 39 /*VOLUME_LSB*/ : c->midiVolume = (unsigned short)((c->midiVolume & 0x3F80) | control_value); goto TCMC_SET_VOLUME; + case 11 /*EXPRESSION_MSB*/ : c->midiExpression = (unsigned short)((c->midiExpression & 0x7F ) | (control_value << 7)); goto TCMC_SET_VOLUME; + case 43 /*EXPRESSION_LSB*/ : c->midiExpression = (unsigned short)((c->midiExpression & 0x3F80) | control_value); goto TCMC_SET_VOLUME; + case 10 /*PAN_MSB*/ : c->midiPan = (unsigned short)((c->midiPan & 0x7F ) | (control_value << 7)); goto TCMC_SET_PAN; + case 42 /*PAN_LSB*/ : c->midiPan = (unsigned short)((c->midiPan & 0x3F80) | control_value); goto TCMC_SET_PAN; + case 6 /*DATA_ENTRY_MSB*/ : c->midiData = (unsigned short)((c->midiData & 0x7F) | (control_value << 7)); goto TCMC_SET_DATA; + case 38 /*DATA_ENTRY_LSB*/ : c->midiData = (unsigned short)((c->midiData & 0x3F80) | control_value); goto TCMC_SET_DATA; + case 0 /*BANK_SELECT_MSB*/ : c->bank = (unsigned short)(0x8000 | control_value); return 1; //bank select MSB alone acts like LSB + case 32 /*BANK_SELECT_LSB*/ : c->bank = (unsigned short)((c->bank & 0x8000 ? ((c->bank & 0x7F) << 7) : 0) | control_value); return 1; + case 101 /*RPN_MSB*/ : c->midiRPN = (unsigned short)(((c->midiRPN == 0xFFFF ? 0 : c->midiRPN) & 0x7F ) | (control_value << 7)); return 1; + case 100 /*RPN_LSB*/ : c->midiRPN = (unsigned short)(((c->midiRPN == 0xFFFF ? 0 : c->midiRPN) & 0x3F80) | control_value); return 1; + case 98 /*NRPN_LSB*/ : c->midiRPN = 0xFFFF; return 1; + case 99 /*NRPN_MSB*/ : c->midiRPN = 0xFFFF; return 1; + case 64 /*SUSTAIN*/ : tsf_channel_set_sustain(f, channel, (int)(control_value >= 64)); return 1; + case 120 /*ALL_SOUND_OFF*/ : tsf_channel_sounds_off_all(f, channel); return 1; + case 123 /*ALL_NOTES_OFF*/ : tsf_channel_note_off_all(f, channel); return 1; + case 121 /*ALL_CTRL_OFF*/ : + c->midiVolume = c->midiExpression = 16383; + c->midiPan = 8192; + c->bank = 0; + c->midiRPN = 0xFFFF; + c->midiData = 0; + tsf_channel_set_volume(f, channel, 1.0f); + tsf_channel_set_pan(f, channel, 0.5f); + tsf_channel_set_pitchrange(f, channel, 2.0f); + tsf_channel_set_tuning(f, channel, 0); + return 1; + } + return 1; +TCMC_SET_VOLUME: + //Raising to the power of 3 seems to result in a decent sounding volume curve for MIDI + tsf_channel_set_volume(f, channel, TSF_POWF((c->midiVolume / 16383.0f) * (c->midiExpression / 16383.0f), 3.0f)); + return 1; +TCMC_SET_PAN: + tsf_channel_set_pan(f, channel, c->midiPan / 16383.0f); + return 1; +TCMC_SET_DATA: + if (c->midiRPN == 0) tsf_channel_set_pitchrange(f, channel, (c->midiData >> 7) + 0.01f * (c->midiData & 0x7F)); + else if (c->midiRPN == 1) tsf_channel_set_tuning(f, channel, (int)c->tuning + ((float)c->midiData - 8192.0f) / 8192.0f); //fine tune + else if (c->midiRPN == 2 && controller == 6) tsf_channel_set_tuning(f, channel, ((float)control_value - 64.0f) + (c->tuning - (int)c->tuning)); //coarse tune + return 1; +} + +TSFDEF int tsf_channel_get_preset_index(tsf* f, int channel) +{ + return (f->channels && channel < f->channels->channelNum ? f->channels->channels[channel].presetIndex : 0); +} + +TSFDEF int tsf_channel_get_preset_bank(tsf* f, int channel) +{ + return (f->channels && channel < f->channels->channelNum ? (f->channels->channels[channel].bank & 0x7FFF) : 0); +} + +TSFDEF int tsf_channel_get_preset_number(tsf* f, int channel) +{ + return (f->channels && channel < f->channels->channelNum ? f->presets[f->channels->channels[channel].presetIndex].preset : 0); +} + +TSFDEF float tsf_channel_get_pan(tsf* f, int channel) +{ + return (f->channels && channel < f->channels->channelNum ? f->channels->channels[channel].panOffset - 0.5f : 0.5f); +} + +TSFDEF float tsf_channel_get_volume(tsf* f, int channel) +{ + return (f->channels && channel < f->channels->channelNum ? tsf_decibelsToGain(f->channels->channels[channel].gainDB) : 1.0f); +} + +TSFDEF int tsf_channel_get_pitchwheel(tsf* f, int channel) +{ + return (f->channels && channel < f->channels->channelNum ? f->channels->channels[channel].pitchWheel : 8192); +} + +TSFDEF float tsf_channel_get_pitchrange(tsf* f, int channel) +{ + return (f->channels && channel < f->channels->channelNum ? f->channels->channels[channel].pitchRange : 2.0f); +} + +TSFDEF float tsf_channel_get_tuning(tsf* f, int channel) +{ + return (f->channels && channel < f->channels->channelNum ? f->channels->channels[channel].tuning : 0.0f); +} + +#ifdef __cplusplus +} +#endif + +#endif //TSF_IMPLEMENTATION diff --git a/tools/audioconv64/vadpcm/LICENSE.txt b/tools/audioconv64/vadpcm/LICENSE.txt new file mode 100644 index 000000000..ee6256cdb --- /dev/null +++ b/tools/audioconv64/vadpcm/LICENSE.txt @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/tools/audioconv64/vadpcm/README.md b/tools/audioconv64/vadpcm/README.md new file mode 100644 index 000000000..77da52b58 --- /dev/null +++ b/tools/audioconv64/vadpcm/README.md @@ -0,0 +1,14 @@ +This directory vendors the VADPCM codec used by `audioconv64`. + +Upstream repository: https://github.com/depp/vadpcm +Vendored from commit: `3c462832e84294166b2034b8c420de4150753d12` + +Only the upstream `codec/` subtree is kept here (under `codec/`). + +## Updating + +Use `tools/audioconv64/vadpcm/update-vadcpm.sh`. + +The script fetches upstream, computes a split of `codec/`, pulls only that +subtree into `codec/`, refreshes `LICENSE.txt`, and then undoes the +autogenerated subtree commit so changes are left staged for build/test. diff --git a/tools/audioconv64/vadpcm/binary.c b/tools/audioconv64/vadpcm/binary.c deleted file mode 100644 index 505480a52..000000000 --- a/tools/audioconv64/vadpcm/binary.c +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2022 Dietrich Epp. -// This file is part of Skelly 64. Skelly 64 is licensed under the terms of the -// Mozilla Public License, version 2.0. See LICENSE.txt for details. -#include "binary.h" - -// Instantiate inline functions. -uint16_t vadpcm_read16(const void *ptr); -uint32_t vadpcm_read32(const void *ptr); diff --git a/tools/audioconv64/vadpcm/binary.h b/tools/audioconv64/vadpcm/binary.h deleted file mode 100644 index 07121e28a..000000000 --- a/tools/audioconv64/vadpcm/binary.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2022 Dietrich Epp. -// This file is part of Skelly 64. Skelly 64 is licensed under the terms of the -// Mozilla Public License, version 2.0. See LICENSE.txt for details. -#pragma once -// Binary encoding and decodeng functions. Internal header. - -#include - -// Read a big-endian 16-bit integer. -inline uint16_t vadpcm_read16(const void *ptr) { - const uint8_t *d = ptr; - return (d[0] << 8) | d[1]; -} - -// Read a big-endian 32-bit integer. -inline uint32_t vadpcm_read32(const void *ptr) { - const uint8_t *d = ptr; - return ((uint32_t)d[0] << 24) | ((uint32_t)d[1] << 16) | - ((uint32_t)d[2] << 8) | (uint32_t)d[3]; -} diff --git a/tools/audioconv64/vadpcm/codebook.c b/tools/audioconv64/vadpcm/codebook.c deleted file mode 100644 index 88144dccd..000000000 --- a/tools/audioconv64/vadpcm/codebook.c +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2022 Dietrich Epp. -// This file is part of Skelly 64. Skelly 64 is licensed under the terms of the -// Mozilla Public License, version 2.0. See LICENSE.txt for details. -#include "binary.h" -#include "vadpcm.h" - -enum { - // The only supported version number for VADPCM data. This is also the only - // known version number. - kVADPCMVersion = 1, - - // Header size for AIFC codebook, not counting the predictor data. - kVADPCMAIFCHeaderSize = 6, -}; - -vadpcm_error vadpcm_read_codebook_aifc( - struct vadpcm_codebook_spec *restrict spec, size_t *restrict data_offset, - const void *VADPCM_RESTRICT data, size_t size) { - const uint8_t *restrict p = data; - - // Read the header: - // u16 version (equals 1) - // u16 order - // u16 predictor count - if (size < 2) { - return kVADPCMErrInvalidData; - } - int version = vadpcm_read16(p); - if (version != kVADPCMVersion) { - return kVADPCMErrUnknownVersion; - } - int order = vadpcm_read16(p + 2); - int predictor_count = vadpcm_read16(p + 4); - if (order == 0 || predictor_count == 0) { - return kVADPCMErrInvalidData; - } - if (order > kVADPCMMaxOrder) { - return kVADPCMErrLargeOrder; - } - if (predictor_count > kVADPCMMaxPredictorCount) { - return kVADPCMErrLargePredictorCount; - } - - // Check that there's enough space for the predictor data. - size_t vsize = (size_t)16 * predictor_count * order; - if (size < vsize + kVADPCMAIFCHeaderSize) { - return kVADPCMErrInvalidData; - } - - spec->predictor_count = predictor_count; - spec->order = order; - *data_offset = kVADPCMAIFCHeaderSize; - return 0; -} - -void vadpcm_read_vectors(int count, const void *VADPCM_RESTRICT data, - struct vadpcm_vector *VADPCM_RESTRICT vectors) { - const uint8_t *ptr = data; - for (int i = 0; i < count; i++) { - for (int j = 0; j < 8; j++) { - vectors[i].v[j] = vadpcm_read16(ptr + 16 * i + 2 * j); - } - } -} diff --git a/tools/audioconv64/vadpcm/codec/README.md b/tools/audioconv64/vadpcm/codec/README.md new file mode 100644 index 000000000..40d5dd974 --- /dev/null +++ b/tools/audioconv64/vadpcm/codec/README.md @@ -0,0 +1,5 @@ +# VADPCM Library + +This contains the core VADPCM encoder and decoder. The interface is defined in `vadpcm.h`. + +General VADPCM documentation is available in project documentation: https://depp.github.io/skelly64/vadpcm/ diff --git a/tools/audioconv64/vadpcm/codec/autocorr.c b/tools/audioconv64/vadpcm/codec/autocorr.c new file mode 100644 index 000000000..f25f2a7ad --- /dev/null +++ b/tools/audioconv64/vadpcm/codec/autocorr.c @@ -0,0 +1,33 @@ +// Copyright 2023 Dietrich Epp. +// This file is part of VADPCM. VADPCM is licensed under the terms of the +// Mozilla Public License, version 2.0. See LICENSE.txt for details. +#include "codec/autocorr.h" + +#include "codec/vadpcm.h" + +void vadpcm_autocorr(size_t frame_count, float (*restrict corr)[6], + const int16_t *restrict src) { + float x0 = 0.0f, x1 = 0.0f, x2 = 0.0f, m[6]; + size_t frame; + int i; + + for (frame = 0; frame < frame_count; frame++) { + for (i = 0; i < 6; i++) { + m[i] = 0.0f; + } + for (i = 0; i < kVADPCMFrameSampleCount; i++) { + x2 = x1; + x1 = x0; + x0 = src[frame * kVADPCMFrameSampleCount + i] * (1.0f / 32768.0f); + m[0] += x0 * x0; + m[1] += x1 * x0; + m[2] += x1 * x1; + m[3] += x2 * x0; + m[4] += x2 * x1; + m[5] += x2 * x2; + } + for (int i = 0; i < 6; i++) { + corr[frame][i] = m[i]; + } + } +} diff --git a/tools/audioconv64/vadpcm/codec/autocorr.h b/tools/audioconv64/vadpcm/codec/autocorr.h new file mode 100644 index 000000000..8996a20ab --- /dev/null +++ b/tools/audioconv64/vadpcm/codec/autocorr.h @@ -0,0 +1,20 @@ +// Copyright 2023 Dietrich Epp. +// This file is part of VADPCM. VADPCM is licensed under the terms of the +// Mozilla Public License, version 2.0. See LICENSE.txt for details. +#pragma once + +// Autocorrelation matrixes for blocks of audio +// +// Autocorrelation is a symmetric 3x3 matrix. The upper triangle is stored. +// Indexes: +// +// [0 1 3] +// [_ 2 4] +// [_ _ 5] + +#include +#include + +// Calculate the autocorrelation matrix for each frame. +void vadpcm_autocorr(size_t frame_count, float (*restrict corr)[6], + const int16_t *restrict src); diff --git a/tools/audioconv64/vadpcm/decode.c b/tools/audioconv64/vadpcm/codec/decode.c similarity index 64% rename from tools/audioconv64/vadpcm/decode.c rename to tools/audioconv64/vadpcm/codec/decode.c index 297d05b65..8f57ec296 100644 --- a/tools/audioconv64/vadpcm/decode.c +++ b/tools/audioconv64/vadpcm/codec/decode.c @@ -1,12 +1,10 @@ // Copyright 2022 Dietrich Epp. -// This file is part of Skelly 64. Skelly 64 is licensed under the terms of the +// This file is part of VADPCM. VADPCM is licensed under the terms of the // Mozilla Public License, version 2.0. See LICENSE.txt for details. -#include "vadpcm.h" +#include "codec/vadpcm.h" #include -#include - // Extend the sign bit of a 4-bit integer. static int vadpcm_ext4(int x) { return x > 7 ? x - 16 : x; @@ -65,8 +63,8 @@ vadpcm_error vadpcm_decode(int predictor_count, int order, // Accumulate the residual and predicted values. const struct vadpcm_vector *v = &predictor[order - 1]; for (int k = 0; k < 8; k++) { - int residual = residuals[k] << scaling; - accumulator[k] += residual << 11; + int residual = residuals[k] * (1 << scaling); + accumulator[k] += residual * (1 << 11); for (int i = 0; i < 7 - k; i++) { accumulator[k + 1 + i] += residual * v->v[i]; } @@ -82,42 +80,3 @@ vadpcm_error vadpcm_decode(int predictor_count, int order, } return 0; } - -#if TEST -#include "lib/vadpcm/test.h" - -#include -#include - -void test_decode(const char *name, int predictor_count, int order, - struct vadpcm_vector *codebook, size_t frame_count, - const void *vadpcm, const int16_t *pcm) { - size_t sample_count = frame_count * kVADPCMFrameSampleCount; - int16_t *out_pcm = xmalloc(sizeof(*out_pcm) * sample_count); - struct vadpcm_vector state = {{0}}; - vadpcm_error err = vadpcm_decode(predictor_count, order, codebook, &state, - frame_count, out_pcm, vadpcm); - if (err != 0) { - fprintf(stderr, "error: test_decode %s: %s", name, - vadpcm_error_name2(err)); - test_failure_count++; - goto done; - } - for (size_t i = 0; i < sample_count; i++) { - if (pcm[i] != out_pcm[i]) { - fprintf(stderr, - "error: test_decode %s: output does not match, " - "index = %zu\n", - name, i); - size_t frame = i / kVADPCMFrameSampleCount; - show_pcm_diff(pcm + frame * kVADPCMFrameSampleCount, - out_pcm + frame * kVADPCMFrameSampleCount); - test_failure_count++; - goto done; - } - } -done: - free(out_pcm); -} - -#endif diff --git a/tools/audioconv64/vadpcm/codec/encode.c b/tools/audioconv64/vadpcm/codec/encode.c new file mode 100644 index 000000000..f031b4f06 --- /dev/null +++ b/tools/audioconv64/vadpcm/codec/encode.c @@ -0,0 +1,294 @@ +// Copyright 2022 Dietrich Epp. +// This file is part of VADPCM. VADPCM is licensed under the terms of the +// Mozilla Public License, version 2.0. See LICENSE.txt for details. +#include "codec/encode.h" + +#include "codec/autocorr.h" +#include "codec/predictor.h" +#include "codec/random.h" +#include "codec/vadpcm.h" + +#include +#include +#include +#include + +enum { + // Order of predictor to use. Other orders are not supported. + kVADPCMOrder = 2, + + // Number of predictors to use, by default. + kVADPCMDefaultPredictorCount = 4, +}; + +void vadpcm_make_vectors(const double *restrict coeff, + struct vadpcm_vector *restrict vectors) { + double scale = (double)(1 << 11); + for (int i = 0; i < 2; i++) { + double x1 = 0.0, x2 = 0.0; + if (i == 0) { + x2 = scale; + } else { + x1 = scale; + } + for (int j = 0; j < 8; j++) { + double x = coeff[0] * x1 + coeff[1] * x2; + int value; + if (x > 0x7fff) { + value = 0x7fff; + } else if (x < -0x8000) { + value = -0x8000; + } else { + value = lrint(x); + } + vectors[i].v[j] = value; + x2 = x1; + x1 = x; + } + } +} + +void vadpcm_make_codebook(size_t frame_count, int predictor_count, + const float (*restrict corr)[6], + const uint8_t *restrict predictors, + struct vadpcm_vector *restrict codebook) { + double pcorr[kVADPCMMaxPredictorCount][6]; + int count[kVADPCMMaxPredictorCount]; + vadpcm_meancorrs(frame_count, predictor_count, corr, predictors, pcorr, + count); + for (int i = 0; i < predictor_count; i++) { + if (count[i] > 0) { + double coeff[2]; + vadpcm_solve_stable(pcorr[i], coeff); + vadpcm_make_vectors(coeff, codebook + 2 * i); + } else { + memset(codebook + 2 * i, 0, sizeof(struct vadpcm_vector) * 2); + } + } +} + +static int vadpcm_getshift(int min, int max, int min_residual, int max_residual) { + int shift = 0; + while (shift < 12 && (min < min_residual || max_residual < max)) { + min >>= 1; + max >>= 1; + shift++; + } + return shift; +} + +// Encode audio as VADPCM, given the assignment of each frame to a predictor. +void vadpcm_encode_data(size_t frame_count, void *restrict dest, + const int16_t *restrict src, + const uint8_t *restrict predictors, + const struct vadpcm_vector *restrict codebook, + vadpcm_dither dither, + int min_residual, int max_residual, + struct vadpcm_stats *restrict stats, + struct vadpcm_encoder_state *restrict encoder_state) { + uint32_t rng_state = encoder_state->rng; + uint8_t *destptr = dest; + int state[4]; + state[0] = encoder_state->data[0]; + state[1] = encoder_state->data[1]; + stats->signal_mean_square = 0.0; + stats->error_mean_square = 0.0; + for (size_t frame = 0; frame < frame_count; frame++) { + unsigned predictor = predictors[frame]; + const struct vadpcm_vector *restrict pvec = codebook + 2 * predictor; + int accumulator[8], s0, s1, s, a, r, min, max; + + double sum_square = 0.0; + for (int i = 0; i < kVADPCMFrameSampleCount; i++) { + double value = (double)src[frame * 16 + i]; + sum_square += value * value; + } + stats->signal_mean_square += sum_square; + + // Calculate the residual with full precision, and figure out the + // scaling factor necessary to encode it. + state[2] = src[frame * 16 + 6]; + state[3] = src[frame * 16 + 7]; + min = 0; + max = 0; + for (int vector = 0; vector < 2; vector++) { + s0 = state[vector * 2]; + s1 = state[vector * 2 + 1]; + for (int i = 0; i < 8; i++) { + accumulator[i] = + (src[frame * 16 + vector * 8 + i] * (1 << 11)) - + s0 * pvec[0].v[i] - s1 * pvec[1].v[i]; + } + for (int i = 0; i < 8; i++) { + s = accumulator[i] >> 11; + if (s < min) { + min = s; + } + if (s > max) { + max = s; + } + for (int j = 0; j < 7 - i; j++) { + accumulator[i + 1 + j] -= s * pvec[1].v[j]; + } + } + } + int shift = vadpcm_getshift(min, max, min_residual, max_residual); + + // Try a range of 3 shift values, and use the shift value that produces + // the lowest error. + double best_error = 0.0; + int min_shift = shift > 0 ? shift - 1 : 0; + int max_shift = shift < 12 ? shift + 1 : 12; + uint32_t init_state = rng_state; + for (shift = min_shift; shift <= max_shift; shift++) { + rng_state = init_state; + uint8_t fout[8]; + double error = 0.0; + // Half a quantization step, which turns the truncating shift below + // into a round to nearest. Dither replaces this with a random + // value covering the same range. + int round_bias = shift > 0 ? 1 << (shift - 1) : 0; + s0 = state[0]; + s1 = state[1]; + for (int vector = 0; vector < 2; vector++) { + for (int i = 0; i < 8; i++) { + accumulator[i] = s0 * pvec[0].v[i] + s1 * pvec[1].v[i]; + } + for (int i = 0; i < 8; i++) { + s = src[frame * 16 + vector * 8 + i]; + a = accumulator[i] >> 11; + // Calculate the residual, encode as 4 bits. + int bias; + if (dither == kVADPCMDitherRectangular) { + // Uniform over [0, 2^shift), which makes the + // truncating shift below round up with a probability + // equal to how far the residual sits between its two + // representable neighbors. + bias = (rng_state >> 16) >> (16 - shift); + rng_state = vadpcm_rng(rng_state); + } else { + bias = round_bias; + } + r = (s - a + bias) >> shift; + if (r > max_residual) { + r = max_residual; + } else if (r < min_residual) { + r = min_residual; + } + accumulator[i] = r; + // Update state to match decoder. The accumulator has + // 32-bit precision, but the state carried from vector to + // vector is just the 16-bit output values. + int sout = r * (1 << shift); + for (int j = 0; j < 7 - i; j++) { + accumulator[i + 1 + j] += sout * pvec[1].v[j]; + } + sout += a; + if (sout > 0x7fff) { + sout = 0x7fff; + } else if (sout < -0x8000) { + sout = -0x8000; + } + s0 = s1; + s1 = sout; + // Track encoding error. + double serror = s - sout; + error += serror * serror; + } + for (int i = 0; i < 4; i++) { + fout[vector * 4 + i] = ((accumulator[2 * i] & 15) << 4) | + (accumulator[2 * i + 1] & 15); + } + } + if (shift == min_shift || error < best_error) { + destptr[frame * 9] = (shift << 4) | predictor; + memcpy(destptr + frame * 9 + 1, fout, 8); + state[2] = s0; + state[3] = s1; + best_error = error; + } + } + state[0] = state[2]; + state[1] = state[3]; + stats->error_mean_square += best_error; + } + *encoder_state = (struct vadpcm_encoder_state){ + .data = {state[0], state[1]}, + .rng = rng_state, + }; + double factor = 1.0 / ((double)(frame_count * kVADPCMFrameSampleCount) * + (32768.0 * 32768.0)); + stats->signal_mean_square *= factor; + stats->error_mean_square *= factor; +} + +vadpcm_error vadpcm_encode(const struct vadpcm_params *restrict params, + struct vadpcm_vector *restrict codebook, + size_t frame_count, void *restrict dest, + const int16_t *restrict src, + struct vadpcm_stats *stats) { + int predictor_count = params->predictor_count; + int min_residual = params->min_residual; + int max_residual = params->max_residual; + if (predictor_count < 1 || kVADPCMMaxPredictorCount < predictor_count) { + return kVADPCMErrInvalidParams; + } + vadpcm_dither dither = params->dither; + if (dither != kVADPCMDitherNone && dither != kVADPCMDitherRectangular) { + return kVADPCMErrInvalidParams; + } + if (min_residual < -8 || max_residual > 7 || min_residual > max_residual) { + return kVADPCMErrInvalidParams; + } + + // Early exit if there is no data to encode. + if (frame_count == 0) { + memset(codebook, 0, + sizeof(*codebook) * kVADPCMEncodeOrder * predictor_count); + if (stats != NULL) { + stats->signal_mean_square = 0.0; + stats->error_mean_square = 0.0; + } + return 0; + } + + // Scratch memory buffers. + float (*corr)[6] = NULL; + uint8_t *predictors = NULL; + if (frame_count > ((size_t)-1) / (sizeof(*corr))) { + return kVADPCMErrMemory; + } + + // Get autocorrelation matrix for each frame. + corr = malloc(frame_count * sizeof(*corr)); + if (corr == NULL) { + return kVADPCMErrMemory; + } + predictors = malloc(frame_count); + if (predictors == NULL) { + free(corr); + return kVADPCMErrMemory; + } + vadpcm_autocorr(frame_count, corr, src); + + // Assign predictors to each frame. + vadpcm_error err = vadpcm_assign_predictors(frame_count, predictor_count, + corr, predictors); + + if (err == 0) { + // Create optimal codebook, given predictor assignments. + vadpcm_make_codebook(frame_count, predictor_count, corr, predictors, + codebook); + + // Encode. + struct vadpcm_stats stats_buf; + struct vadpcm_encoder_state encoder_state = {{0, 0}, 0}; + vadpcm_encode_data(frame_count, dest, src, predictors, codebook, dither, + min_residual, max_residual, + stats != NULL ? stats : &stats_buf, &encoder_state); + } + + free(corr); + free(predictors); + return err; +} diff --git a/tools/audioconv64/vadpcm/codec/encode.h b/tools/audioconv64/vadpcm/codec/encode.h new file mode 100644 index 000000000..909188207 --- /dev/null +++ b/tools/audioconv64/vadpcm/codec/encode.h @@ -0,0 +1,40 @@ +// Copyright 2023 Dietrich Epp. +// This file is part of VADPCM. VADPCM is licensed under the terms of the +// Mozilla Public License, version 2.0. See LICENSE.txt for details. +#pragma once + +#include "codec/vadpcm.h" + +#include +#include + +struct vadpcm_vector; +struct vadpcm_stats; + +// Calculate codebook vectors for one predictor, given the predictor +// coefficients. +void vadpcm_make_vectors(const double *restrict coeff, + struct vadpcm_vector *restrict vectors); + +// Create a codebook, given the frame autocorrelation matrixes and the +// assignment from frames to predictors. +void vadpcm_make_codebook(size_t frame_count, int predictor_count, + const float (*restrict corr)[6], + const uint8_t *restrict predictors, + struct vadpcm_vector *restrict codebook); + +// Current state of the encoder. The state can be initialized to zero. +struct vadpcm_encoder_state { + int16_t data[2]; + uint32_t rng; +}; + +// Encode audio as VADPCM, given the assignment of each frame to a predictor. +void vadpcm_encode_data(size_t frame_count, void *restrict dest, + const int16_t *restrict src, + const uint8_t *restrict predictors, + const struct vadpcm_vector *restrict codebook, + vadpcm_dither dither, + int min_residual, int max_residual, + struct vadpcm_stats *restrict stats, + struct vadpcm_encoder_state *restrict encoder_state); diff --git a/tools/audioconv64/vadpcm/error.c b/tools/audioconv64/vadpcm/codec/error.c similarity index 53% rename from tools/audioconv64/vadpcm/error.c rename to tools/audioconv64/vadpcm/codec/error.c index 6e8cc643c..5582cd358 100644 --- a/tools/audioconv64/vadpcm/error.c +++ b/tools/audioconv64/vadpcm/codec/error.c @@ -1,7 +1,7 @@ // Copyright 2022 Dietrich Epp. -// This file is part of Skelly 64. Skelly 64 is licensed under the terms of the +// This file is part of VADPCM. VADPCM is licensed under the terms of the // Mozilla Public License, version 2.0. See LICENSE.txt for details. -#include "vadpcm.h" +#include "codec/vadpcm.h" const char *vadpcm_error_name(vadpcm_error err) { switch (err) { @@ -9,14 +9,10 @@ const char *vadpcm_error_name(vadpcm_error err) { return "no error"; case kVADPCMErrInvalidData: return "invalid data"; - case kVADPCMErrLargeOrder: - return "predictor order too large"; - case kVADPCMErrLargePredictorCount: - return "predictor count too large"; - case kVADPCMErrUnknownVersion: - return "unknown VADPCM version"; case kVADPCMErrInvalidParams: return "invalid encoding parameters"; + case kVADPCMErrMemory: + return "memory allocation failed"; } return 0; } diff --git a/tools/audioconv64/vadpcm/codec/predictor.c b/tools/audioconv64/vadpcm/codec/predictor.c new file mode 100644 index 000000000..a93845f36 --- /dev/null +++ b/tools/audioconv64/vadpcm/codec/predictor.c @@ -0,0 +1,395 @@ +// Copyright 2023 Dietrich Epp. +// This file is part of VADPCM. VADPCM is licensed under the terms of the +// Mozilla Public License, version 2.0. See LICENSE.txt for details. +#include "codec/predictor.h" + +#include "codec/vadpcm.h" + +#include +#include +#include +#include +#include +#include + +// A record of the predictor error if a frame is encoded with a given predictor. +struct vadpcm_error_record { + int predictor; + float error; +}; + +// Scratch data for a frame when assigning predictors. +struct vadpcm_scratch { + struct vadpcm_error_record p[2]; +}; + +enum { + // Iterations for predictor assignment. + kVADPCMIterations = 20, +}; + +float vadpcm_eval(const float *restrict corr, const float *restrict coeff); + +double vadpcm_eval_solved(const double *restrict corr, + const double *restrict coeff); + +void vadpcm_best_error(size_t frame_count, const float (*restrict corr)[6], + float *restrict best_error) { + for (size_t frame = 0; frame < frame_count; frame++) { + double fcorr[6]; + for (int i = 0; i < 6; i++) { + fcorr[i] = (double)corr[frame][i]; + } + double coeff[2]; + float error; + if (vadpcm_solve_stable(fcorr, coeff) == 0) { + error = (float)vadpcm_eval_solved(fcorr, coeff); + } else { + float fcoeff[2] = {(float)coeff[0], (float)coeff[1]}; + error = (float)vadpcm_eval(corr[frame], fcoeff); + } + best_error[frame] = error; + } +} + +// Get the mean autocorrelation matrix for each predictor. If the predictor for +// a frame is out of range, that frame is ignored. +void vadpcm_meancorrs(size_t frame_count, int predictor_count, + const float (*restrict corr)[6], + const uint8_t *restrict predictors, + double (*restrict pcorr)[6], int *restrict count) { + for (int i = 0; i < predictor_count; i++) { + count[i] = 0; + for (int j = 0; j < 6; j++) { + pcorr[i][j] = 0.0; + } + } + for (size_t frame = 0; frame < frame_count; frame++) { + int predictor = predictors[frame]; + if (predictor < predictor_count) { + count[predictor]++; + // REVIEW: This is naive summation. Is that good enough? + for (int j = 0; j < 6; j++) { + pcorr[predictor][j] += (double)corr[frame][j]; + } + } + } + for (int i = 0; i < predictor_count; i++) { + if (count[i] > 0) { + double a = 1.0 / count[i]; + for (int j = 0; j < 6; j++) { + pcorr[i][j] *= a; + } + } + } +} + +void vadpcm_solve(const double *restrict corr, double *restrict coeff) { + // For the autocorrelation matrix A, we want vector v which minimizes the + // residual \epsilon, + // + // \epsilon = [1|v]^T A [1|v] + // + // We can rewrite this as: + // + // \epsilon = B + 2 C v + v^T D v + // + // Where B, C, and D are submatrixes of A. The minimum value, v, satisfies: + // + // D v + C = 0. + + // The autocorrelation matrix is accumulated in single precision, so its + // entries carry roughly 1e-7 relative accuracy, and a pivot smaller than + // that is noise. This solve runs in double precision, so there is no + // reason to give up any earlier than that. Bailing out sooner is not + // conservative: a smooth signal is an ill-conditioned system precisely + // because consecutive samples are correlated, which is when second-order + // prediction is worth the most. + double rel_epsilon = 1.0e-5; + coeff[0] = 0.0; + coeff[1] = 0.0; + + // The element with maximum absolute value is on the diagonal, by the + // Cauchy-Schwarz inequality. + double max = corr[0]; + if (corr[2] > max) { + max = corr[2]; + } + if (corr[5] > max) { + max = corr[5]; + } + double epsilon = max * rel_epsilon; + + // Solve using Gaussian elimination. + // + // [a b | x] + // [b c | y] + double a = corr[2]; + double b = corr[4]; + double c = corr[5]; + double x = corr[1]; + double y = corr[3]; + + // Partial pivoting. Note that a, c are non-negative. + int pivot = c > a; + if (pivot) { + double t; + t = a; + a = c; + c = t; + t = x; + x = y; + y = t; + } + + // Multiply first row by 1/a: [1 b/a | x/a] + if (a <= epsilon) { + // Matrix is close to zero. Just use zero for the predictor + // coefficients. + return; + } + double a1 = 1.0 / a; + double b1 = b * a1; + double x1 = x * a1; + + // Subtract first row * b from second row: [0 c-b1*b | y - x1*b] + double c2 = c - b1 * b; + double y2 = y - x1 * b; + + // Multiply second row by 1/c. [0 1 | y2/c2] + if (fabs(c2) <= epsilon) { + // Matrix is poorly conditioned or singular. Solve as a first-order + // system. + coeff[pivot] = x1; + return; + } + double y3 = y2 / c2; + + // Backsubstitute. + double x4 = x1 - y3 * b1; + + coeff[pivot] = x4; + coeff[!pivot] = y3; +} + +int vadpcm_stabilize(double *restrict coeff) { + // TODO: The new coefficient calculations have not been checked closely. + if (coeff[1] < -1.0) { + coeff[1] = -1.0; + if (coeff[0] < -1.0) { + coeff[0] = -1.0; + } else if (coeff[0] > 1.0) { + coeff[0] = 1.0; + } + return 1; + } + if (coeff[0] > 0.0) { + if (coeff[1] + coeff[0] > 1.0) { + double d = coeff[1] - coeff[0]; + if (d < -3.0) { + d = -3.0; + } else if (d > 1.0) { + d = 1.0; + } + coeff[0] = 0.5 - 0.5 * d; + coeff[1] = 0.5 + 0.5 * d; + return 1; + } + } else { + if (coeff[1] - coeff[0] > 1.0) { + double d = coeff[1] + coeff[0]; + if (d < -3.0) { + d = -3.0; + } else if (d > 1.0) { + d = 1.0; + } + coeff[0] = 0.5 * d - 0.5; + coeff[1] = 0.5 * d + 0.5; + return 1; + } + } + return 0; +} + +// Calculate the square error for a pair of coefficients. This is vadpcm_eval, +// in double precision. +static double vadpcm_eval_at(const double *restrict corr, double c0, + double c1) { + return corr[0] + corr[2] * c0 * c0 + corr[5] * c1 * c1 + + 2.0 * (corr[4] * c0 * c1 - corr[1] * c0 - corr[3] * c1); +} + +// Minimize the square error over the segment from (x0,y0) to (x1,y1). If the +// minimum is lower than *best_error, record it there and in coeff. +static void vadpcm_solve_edge(const double *restrict corr, double x0, double y0, + double x1, double y1, + double *restrict best_error, + double *restrict coeff) { + // Writing the matrix as [k B^T; B A], the error along p + t v is a + // quadratic in t, + // + // g(t) = g(0) + 2 t (v^T A p - B^T v) + t^2 v^T A v + // + // which is minimized at t = v^T (B - A p) / (v^T A v). + double vx = x1 - x0, vy = y1 - y0; + double apx = corr[2] * x0 + corr[4] * y0; + double apy = corr[4] * x0 + corr[5] * y0; + double num = vx * (corr[1] - apx) + vy * (corr[3] - apy); + double den = vx * (corr[2] * vx + corr[4] * vy) + + vy * (corr[4] * vx + corr[5] * vy); + double t; + if (den > 0.0) { + t = num / den; + if (t < 0.0) { + t = 0.0; + } else if (t > 1.0) { + t = 1.0; + } + } else { + // The matrix is positive semidefinite, so v^T A v is never negative, + // and zero means the error is linear along this edge. Take whichever + // endpoint the gradient points toward. + t = num > 0.0 ? 1.0 : 0.0; + } + double c0 = x0 + t * vx, c1 = y0 + t * vy; + double error = vadpcm_eval_at(corr, c0, c1); + if (error < *best_error) { + *best_error = error; + coeff[0] = c0; + coeff[1] = c1; + } +} + +int vadpcm_solve_stable(const double *restrict corr, double *restrict coeff) { + vadpcm_solve(corr, coeff); + // A second-order predictor is stable exactly when its coefficients lie + // within the triangle with vertices (2,-1), (-2,-1), and (0,1). + if (coeff[1] >= -1.0 && coeff[0] + coeff[1] <= 1.0 && + coeff[1] - coeff[0] <= 1.0) { + return 0; + } + // The error is a convex quadratic and the triangle is convex, so the + // constrained minimum lies on an edge. Minimize along each of the three + // edges and keep the best result. + // + // Note that the coefficients land on the boundary of the triangle, which + // is marginally stable rather than strictly stable. Contracting the + // triangle to leave a margin is not worthwhile: vadpcm_make_vectors + // rounds the coefficients onto a 1/2048 grid, and near the (2,-1) vertex + // the poles move as the square root of that perturbation, so no margin + // survives quantization until it is large enough to cost real accuracy on + // low-frequency signals. + double best_error = DBL_MAX; + coeff[0] = 0.0; + coeff[1] = 0.0; + vadpcm_solve_edge(corr, -2.0, -1.0, 2.0, -1.0, &best_error, coeff); + vadpcm_solve_edge(corr, 2.0, -1.0, 0.0, 1.0, &best_error, coeff); + vadpcm_solve_edge(corr, 0.0, 1.0, -2.0, -1.0, &best_error, coeff); + return 1; +} + +// Refine (improve) the existing predictor assignments. Does not assign +// unassigned predictors. Record the amount of error, squared, for each frame. +// Returns the index of an unassigned predictor, or predictor_count, if no +// predictor is unassigned. +static int vadpcm_refine_predictors(size_t frame_count, int predictor_count, + const float (*restrict corr)[6], + float *restrict error, + uint8_t *restrict predictors) { + // Calculate optimal predictor coefficients for each predictor. + double pcorr[kVADPCMMaxPredictorCount][6]; + int count[kVADPCMMaxPredictorCount]; + vadpcm_meancorrs(frame_count, predictor_count, corr, predictors, pcorr, + count); + + float coeff[kVADPCMMaxPredictorCount][2]; + int active_count = 0; + for (int i = 0; i < predictor_count; i++) { + if (count[i] > 0) { + double dcoeff[2]; + vadpcm_solve_stable(pcorr[i], dcoeff); + for (int j = 0; j < 2; j++) { + coeff[active_count][j] = (float)dcoeff[j]; + } + active_count++; + } + } + + // Assign frames to the best predictor for each frame, and record the amount + // of error. + int count2[kVADPCMMaxPredictorCount]; + for (int i = 0; i < active_count; i++) { + count2[i] = 0; + } + for (size_t frame = 0; frame < frame_count; frame++) { + int fpredictor = 0; + float ferror = 0.0f; + for (int i = 0; i < active_count; i++) { + float e = vadpcm_eval(corr[frame], coeff[i]); + if (i == 0 || e < ferror) { + fpredictor = i; + ferror = e; + } + } + predictors[frame] = fpredictor; + error[frame] = ferror; + count2[fpredictor]++; + } + for (int i = 0; i < active_count; i++) { + if (count2[i] == 0) { + return i; + } + } + return active_count; +} + +// Find the frame where the error is highest, relative to the best case. +static size_t vadpcm_worst_frame(size_t frame_count, + const float *restrict best_error, + const float *restrict error) { + float best_improvement = error[0] - best_error[0]; + size_t best_index = 0; + for (size_t frame = 1; frame < frame_count; frame++) { + float improvement = error[frame] - best_error[frame]; + if (improvement > best_improvement) { + best_improvement = improvement; + best_index = frame; + } + } + return best_index; +} + +vadpcm_error vadpcm_assign_predictors(size_t frame_count, int predictor_count, + const float (*restrict corr)[6], + uint8_t *restrict predictors) { + memset(predictors, 0, frame_count); + if (predictor_count <= 1) { + return 0; + } + float *best_error = malloc(frame_count * sizeof(*best_error)); + if (best_error == NULL) { + return kVADPCMErrMemory; + } + vadpcm_best_error(frame_count, corr, best_error); + float *error = malloc(frame_count * sizeof(*error)); + if (error == NULL) { + free(best_error); + return kVADPCMErrMemory; + } + int unassigned = predictor_count; + int active_count = 1; + for (int iter = 0; iter < kVADPCMIterations; iter++) { + if (unassigned < predictor_count) { + size_t worst = vadpcm_worst_frame(frame_count, best_error, error); + predictors[worst] = unassigned; + if (unassigned >= active_count) { + active_count = unassigned + 1; + } + } + unassigned = vadpcm_refine_predictors(frame_count, active_count, corr, + error, predictors); + } + free(best_error); + free(error); + return 0; +} diff --git a/tools/audioconv64/vadpcm/codec/predictor.h b/tools/audioconv64/vadpcm/codec/predictor.h new file mode 100644 index 000000000..2c94ebdb7 --- /dev/null +++ b/tools/audioconv64/vadpcm/codec/predictor.h @@ -0,0 +1,92 @@ +// Copyright 2023 Dietrich Epp. +// This file is part of VADPCM. VADPCM is licensed under the terms of the +// Mozilla Public License, version 2.0. See LICENSE.txt for details. +#pragma once + +// Linear predictor selection. +// +// This module generates a codebook of second-order linear predictors and +// assigns a predictor to each block of audio. This is the only difficult part +// of the encoder. The encoder does not operate on audio data, but instead +// operates on the autocorrelation matrix for each frame of 16 audio samples. + +#include "codec/vadpcm.h" + +#include +#include + +// Calculate the square error, given an autocorrelation matrix and predictor +// coefficients. +inline float vadpcm_eval(const float *restrict corr, + const float *restrict coeff) { + return corr[0] + // + corr[2] * coeff[0] * coeff[0] + // + corr[5] * coeff[1] * coeff[1] + // + 2.0f * (corr[4] * coeff[0] * coeff[1] - // + corr[1] * coeff[0] - // + corr[3] * coeff[1]); +} + +// Calculate the best-case error from a frame, given its solved coefficients. +inline double vadpcm_eval_solved(const double *restrict corr, + const double *restrict coeff) { + // Equivalent to vadpcm_eval(), for the case where coeff are optimal for + // this autocorrelation matrix. + // + // matrix = [k B^T] + // [B A ] + // + // solve(A, B) = A^-1 B + // eval(k, A, B, x) = k - 2B^T x + x^T A x + // eval(k, A, B, solve(A, B)) + // = eval(k, A, B, A^-1 B) + // = k - 2B^T (A^-1 B) + (A^-1 B)^T A (A^-1 B) + // = k - 2B^T A^-1 B + B^T A^-1^T A A^-1 B + // = k - 2B^T A^-1 B + B^T A^-1^T B + // = k - B^T A^-1 B + // = k - B^T solve(A, B) + return corr[0] - corr[1] * coeff[0] - corr[3] * coeff[1]; +} + +// Calculate the best-case error for each frame, given the autocorrelation +// matrixes. +void vadpcm_best_error(size_t frame_count, const float (*restrict corr)[6], + float *restrict best_error); + +// Get the mean autocorrelation matrix for each predictor. If the predictor for +// a frame is out of range, that frame is ignored. +void vadpcm_meancorrs(size_t frame_count, int predictor_count, + const float (*restrict corr)[6], + const uint8_t *restrict predictors, + double (*restrict pcorr)[6], int *restrict count); + +// Calculate the predictor coefficients, given an autocorrelation matrix. The +// coefficients are chosen to minimize vadpcm_eval. The result may describe an +// unstable filter, so prefer vadpcm_solve_stable unless the unconstrained +// solution is specifically what is wanted. +void vadpcm_solve(const double *restrict corr, double *restrict coeff); + +// Adjust predictor coefficients to make them stable. Return 0 if the input +// coefficients are stable and 1 if the input coefficients are unstable and +// were modified. +// +// This moves the coefficients to the nearest stable point as measured in the +// coefficient plane, which is not the nearest stable point as measured in +// prediction error. The two can differ by an order of magnitude. Prefer +// vadpcm_solve_stable, which minimizes the error directly. +int vadpcm_stabilize(double *restrict coeff); + +// Calculate the predictor coefficients which minimize vadpcm_eval among all +// stable predictors, given an autocorrelation matrix. Return 0 if the +// unconstrained solution was already stable, and 1 if the stability constraint +// was active. +// +// Every predictor which reaches a codebook should come from this function. An +// unstable predictor makes the codebook vectors grow without bound, and the +// decoder has no way to recover from one. +int vadpcm_solve_stable(const double *restrict corr, double *restrict coeff); + +// Assign a predictor to each frame. +vadpcm_error vadpcm_assign_predictors(size_t frame_count, int predictor_count, + const float (*restrict corr)[6], + uint8_t *restrict predictors); diff --git a/tools/audioconv64/vadpcm/codec/random.c b/tools/audioconv64/vadpcm/codec/random.c new file mode 100644 index 000000000..cf1ebaa80 --- /dev/null +++ b/tools/audioconv64/vadpcm/codec/random.c @@ -0,0 +1,6 @@ +// Copyright 2023 Dietrich Epp. +// This file is part of VADPCM. VADPCM is licensed under the terms of the +// Mozilla Public License, version 2.0. See LICENSE.txt for details. +#include "codec/random.h" // IWYU pragma: keep + +uint32_t vadpcm_rng(uint32_t state); diff --git a/tools/audioconv64/vadpcm/codec/random.h b/tools/audioconv64/vadpcm/codec/random.h new file mode 100644 index 000000000..6d0982cf9 --- /dev/null +++ b/tools/audioconv64/vadpcm/codec/random.h @@ -0,0 +1,17 @@ +// Copyright 2023 Dietrich Epp. +// This file is part of VADPCM. VADPCM is licensed under the terms of the +// Mozilla Public License, version 2.0. See LICENSE.txt for details. +#pragma once + +// Random number generator. Internal header. + +#include + +inline uint32_t vadpcm_rng(uint32_t state) { + // 0xd9f5: Computationally Easy, Spectrally Good Multipliers for + // Congruential Pseudorandom Number Generators, Steele and Vigna, Table 7, + // p.18 + // + // 0x6487ed51: pi << 29, relatively prime. + return state * 0xd9f5 + 0x6487ed51; +} diff --git a/tools/audioconv64/vadpcm/vadpcm.h b/tools/audioconv64/vadpcm/codec/vadpcm.h similarity index 69% rename from tools/audioconv64/vadpcm/vadpcm.h rename to tools/audioconv64/vadpcm/codec/vadpcm.h index 041b822cc..da01e2d63 100644 --- a/tools/audioconv64/vadpcm/vadpcm.h +++ b/tools/audioconv64/vadpcm/codec/vadpcm.h @@ -1,5 +1,5 @@ // Copyright 2022 Dietrich Epp. -// This file is part of Skelly 64. Skelly 64 is licensed under the terms of the +// This file is part of VADPCM. VADPCM is licensed under the terms of the // Mozilla Public License, version 2.0. See LICENSE.txt for details. #pragma once // VADPCM encoding and decoding. @@ -23,17 +23,11 @@ typedef enum { // Invalid data. kVADPCMErrInvalidData, - // Predictor order is too large. - kVADPCMErrLargeOrder, - - // Predictor count is too large. - kVADPCMErrLargePredictorCount, - - // Data uses an unsupported / unknown version of VADPCM. - kVADPCMErrUnknownVersion, - // Invalid encoding parameters. kVADPCMErrInvalidParams, + + // Memory allocation failed. + kVADPCMErrMemory, } vadpcm_error; // Return the short name of the VADPCM error code. Returns NULL for unknown @@ -85,29 +79,6 @@ struct vadpcm_codebook_spec { int order; }; -// Parse a codebook spec, as it appears in an AIFC file. On success, fills in -// 'spec' and stores the offset to the vector data in data_offset. -// -// The data is taken from an AIFC 'APPL' (application-specific) chunk with the -// name "VADPCMCODES". The chunk header, APPL header, and chunk name should not -// be included in the data passed to this function. -// -// Error codes: -// kVADPCMErrInvalidData: Order or predictor count is zero, or the data is -// incomplete (unexpected EOF). -// kVADPCMErrLargeOrder: Order is larger than largest supported order. -// kVADPCMErrLargePredictorCount: Predictor count is larger than the largest -// supported predictor count. -// kVADPCMErrUnknownVersion: Data uses an unknown version of VADPCM. -vadpcm_error vadpcm_read_codebook_aifc( - struct vadpcm_codebook_spec *VADPCM_RESTRICT spec, - size_t *VADPCM_RESTRICT data_offset, const void *VADPCM_RESTRICT data, - size_t size); - -// Parse codebook vectors. -void vadpcm_read_vectors(int count, const void *VADPCM_RESTRICT data, - struct vadpcm_vector *VADPCM_RESTRICT vectors); - // Decode VADPCM-encoded audio. // // Arguments: @@ -127,15 +98,47 @@ vadpcm_error vadpcm_decode(int predictor_count, int order, size_t frame_count, int16_t *VADPCM_RESTRICT dest, const void *VADPCM_RESTRICT src); +// Dither applied to the residual before it is quantized to four bits. +typedef enum { + // No dither. The residual is rounded to the nearest representable value, + // which gives the lowest error energy. The error is correlated with the + // signal, so what remains is heard as distortion rather than as noise. + kVADPCMDitherNone, + + // Rectangular dither. A random value, uniformly distributed over one + // quantization step, is added to the residual before it is rounded. This + // decorrelates the error from the signal, so the error is heard as noise, + // and costs about 3 dB of signal-to-noise ratio: the error variance is + // d^2/6 rather than d^2/12, for a quantization step of d. + kVADPCMDitherRectangular, +} vadpcm_dither; + // Parameters for VADPCM encoding. struct vadpcm_params { // The number of predictors to put in the codebook. int predictor_count; + + // The dither to apply when quantizing residuals. Note that the zero value + // is kVADPCMDitherNone; the vadpcm command-line tool defaults to + // kVADPCMDitherRectangular instead. + vadpcm_dither dither; + + // Optional clamp range for encoded residual values. Valid range is [-8, 7]. + // Use the default full-range values (-8, 7) to match classic VADPCM. + // Libdragon extension: supports encoding with fewer residual bits. + int min_residual; + int max_residual; }; -// Return the amount of scratch space needed to encode a file with the given -// number of frames. -size_t vadpcm_encode_scratch_size(size_t frame_count); +// Statistics about the VADPCM encoding. +struct vadpcm_stats { + // The mean of the square of the original input signal. + double signal_mean_square; + + // The mean of the square of the encoding error (the difference between the + // original signal and the encoded signal). + double error_mean_square; +}; // Encode PCM as VADPCM. The predictor order is kVADPCMEncodeOrder (2) and // cannot be changed. @@ -150,14 +153,15 @@ size_t vadpcm_encode_scratch_size(size_t frame_count); // frame_count: Number of frames of VADPCM to encode // dest: Output array of frame_count * kVADPCMFrameByteSize bytes // src: Input array of frame_count * kVADPCMFrameSampleCount elements -// scratch: Scratch space with size vadpcm_encode_scratch_size(frame_count) +// stats: If not NULL, this will be filled with stats about the encoding // // Error codes: // kVADPCMErrInvalidParams: Invalid encoding parameters. vadpcm_error vadpcm_encode(const struct vadpcm_params *VADPCM_RESTRICT params, struct vadpcm_vector *VADPCM_RESTRICT codebook, size_t frame_count, void *VADPCM_RESTRICT dest, - const int16_t *VADPCM_RESTRICT src, void *scratch); + const int16_t *VADPCM_RESTRICT src, + struct vadpcm_stats *stats); #ifdef __cplusplus } diff --git a/tools/audioconv64/vadpcm/encode.c b/tools/audioconv64/vadpcm/encode.c deleted file mode 100644 index fb6982023..000000000 --- a/tools/audioconv64/vadpcm/encode.c +++ /dev/null @@ -1,762 +0,0 @@ -// Copyright 2022 Dietrich Epp. -// This file is part of Skelly 64. Skelly 64 is licensed under the terms of the -// Mozilla Public License, version 2.0. See LICENSE.txt for details. -#include "vadpcm.h" - -#include -#include -#include - -enum { - // Order of predictor to use. Other orders are not supported. - kVADPCMOrder = 2, - - // Number of predictors to use, by default. - kVADPCMDefaultPredictorCount = 4, - - // Iterations for predictor assignment. - kVADPCMIterations = 20, -}; - -// Autocorrelation is a symmetric 3x3 matrix. -// -// The upper triangle is stored. Indexes: -// -// [0 1 3] -// [_ 2 4] -// [_ _ 5] - -// Calculate the autocorrelation matrix for each frame. -static void vadpcm_autocorr(size_t frame_count, float (*restrict corr)[6], - const int16_t *restrict src) { - float x0 = 0.0f, x1 = 0.0f, x2 = 0.0f, m[6]; - size_t frame; - int i; - - for (frame = 0; frame < frame_count; frame++) { - for (i = 0; i < 6; i++) { - m[i] = 0.0f; - } - for (i = 0; i < kVADPCMFrameSampleCount; i++) { - x2 = x1; - x1 = x0; - x0 = src[frame * kVADPCMFrameSampleCount + i] * (1.0f / 32768.0f); - m[0] += x0 * x0; - m[1] += x1 * x0; - m[2] += x1 * x1; - m[3] += x2 * x0; - m[4] += x2 * x1; - m[5] += x2 * x2; - } - for (int i = 0; i < 6; i++) { - corr[frame][i] = m[i]; - } - } -} - -// Get the mean autocorrelation matrix for each predictor. If the predictor for -// a frame is out of range, that frame is ignored. -static void vadpcm_meancorrs(size_t frame_count, int predictor_count, - const float (*restrict corr)[6], - const uint8_t *restrict predictors, - double (*restrict pcorr)[6], int *restrict count) { - for (int i = 0; i < predictor_count; i++) { - count[i] = 0; - for (int j = 0; j < 6; j++) { - pcorr[i][j] = 0.0; - } - } - for (size_t frame = 0; frame < frame_count; frame++) { - int predictor = predictors[frame]; - if (predictor < predictor_count) { - count[predictor]++; - // REVIEW: This is naive summation. Is that good enough? - for (int j = 0; j < 6; j++) { - pcorr[predictor][j] += (double)corr[frame][j]; - } - } - } - for (int i = 0; i < predictor_count; i++) { - if (count[i] > 0) { - double a = 1.0 / count[i]; - for (int j = 0; j < 6; j++) { - pcorr[i][j] *= a; - } - } - } -} - -// Calculate the square error, given an autocorrelation matrix and predictor -// coefficients. -static float vadpcm_eval(const float corr[restrict static 6], - const float coeff[restrict static 2]) { - return corr[0] + // - corr[2] * coeff[0] * coeff[0] + // - corr[5] * coeff[1] * coeff[1] + // - 2.0f * (corr[4] * coeff[0] * coeff[1] - // - corr[1] * coeff[0] - // - corr[3] * coeff[1]); -} - -// Calculate the predictor coefficients, given an autocorrelation matrix. The -// coefficients are chosen to minimize vadpcm_eval. -static void vadpcm_solve(const double corr[restrict static 6], - double coeff[restrict static 2]) { - // For the autocorrelation matrix A, we want vector v which minimizes the - // residual \epsilon, - // - // \epsilon = [1|v]^T A [1|v] - // - // We can rewrite this as: - // - // \epsilon = B + 2 C v + v^T D v - // - // Where B, C, and D are submatrixes of A. The minimum value, v, satisfies: - // - // D v + C = 0. - - double rel_epsilon = 1.0 / 4096.0; - coeff[0] = 0.0; - coeff[1] = 0.0; - - // The element with maximum absolute value is on the diagonal, by the - // Cauchy-Schwarz inequality. - double max = corr[0]; - if (corr[2] > max) { - max = corr[2]; - } - if (corr[5] > max) { - max = corr[5]; - } - double epsilon = max * rel_epsilon; - - // Solve using Gaussian elimination. - // - // [a b | x] - // [b c | y] - double a = corr[2]; - double b = corr[4]; - double c = corr[5]; - double x = corr[1]; - double y = corr[3]; - - // Partial pivoting. Note that a, c are non-negative. - int pivot = c > a; - if (pivot) { - double t; - t = a; - a = c; - c = t; - t = x; - x = y; - y = t; - } - - // Multiply first row by 1/a: [1 b/a | x/a] - if (a <= epsilon) { - // Matrix is close to zero. Just use zero for the predictor - // coefficients. - return; - } - double a1 = 1.0 / a; - double b1 = b * a1; - double x1 = x * a1; - - // Subtract first row * b from second row: [0 c-b1*b | y - x1*b] - double c2 = c - b1 * b; - double y2 = y - x1 * b; - - // Multiply second row by 1/c. [0 1 | y2/c2] - if (fabs(c2) <= epsilon) { - // Matrix is poorly conditioned or singular. Solve as a first-order - // system. - coeff[pivot] = x1; - return; - } - double y3 = y2 / c2; - - // Backsubstitute. - double x4 = x1 - y3 * b1; - - coeff[pivot] = x4; - coeff[!pivot] = y3; -} - -// Calculate the best-case error from a frame, given its solved coefficients. -static double vadpcm_eval_solved(const double corr[restrict static 6], - const double coeff[restrict static 2]) { - // Equivalent to vadpcm_eval(), for the case where coeff are optimal for - // this autocorrelation matrix. - // - // matrix = [k B^T] - // [B A ] - // - // solve(A, B) = A^-1 B - // eval(k, A, B, x) = k - 2B^T x + x^T A x - // eval(k, A, B, solve(A, B)) - // = eval(k, A, B, A^-1 B) - // = k - 2B^T (A^-1 B) + (A^-1 B)^T A (A^-1 B) - // = k - 2B^T A^-1 B + B^T A^-1^T A A^-1 B - // = k - 2B^T A^-1 B + B^T A^-1^T B - // = k - B^T A^-1 B - // = k - B^T solve(A, B) - return corr[0] - corr[1] * coeff[0] - corr[3] * coeff[1]; -} - -// Calculate the best-case error for each frame, given the autocorrelation -// matrixes. -static void vadpcm_best_error(size_t frame_count, - const float (*restrict corr)[6], - float *restrict best_error) { - for (size_t frame = 0; frame < frame_count; frame++) { - double fcorr[6]; - for (int i = 0; i < 6; i++) { - fcorr[i] = (double)corr[frame][i]; - } - double coeff[2]; - vadpcm_solve(fcorr, coeff); - best_error[frame] = (float)vadpcm_eval_solved(fcorr, coeff); - } -} - -// Refine (improve) the existing predictor assignments. Does not assign -// unassigned predictors. Record the amount of error, squared, for each frame. -// Returns the index of an unassigned predictor, or predictor_count, if no -// predictor is unassigned. -static int vadpcm_refine_predictors(size_t frame_count, int predictor_count, - const float (*restrict corr)[6], - float *restrict error, - uint8_t *restrict predictors) { - // Calculate optimal predictor coefficients for each predictor. - double pcorr[kVADPCMMaxPredictorCount][6]; - int count[kVADPCMMaxPredictorCount]; - vadpcm_meancorrs(frame_count, predictor_count, corr, predictors, pcorr, - count); - - float coeff[kVADPCMMaxPredictorCount][2]; - int active_count = 0; - for (int i = 0; i < predictor_count; i++) { - if (count[i] > 0) { - double dcoeff[2]; - vadpcm_solve(pcorr[i], dcoeff); - for (int j = 0; j < 2; j++) { - coeff[active_count][j] = dcoeff[j]; - } - active_count++; - } - } - - // Assign frames to the best predictor for each frame, and record the amount - // of error. - int count2[kVADPCMMaxPredictorCount]; - for (int i = 0; i < active_count; i++) { - count2[i] = 0; - } - for (size_t frame = 0; frame < frame_count; frame++) { - int fpredictor = 0; - float ferror = 0.0f; - for (int i = 0; i < active_count; i++) { - float e = vadpcm_eval(corr[frame], coeff[i]); - if (i == 0 || e < ferror) { - fpredictor = i; - ferror = e; - } - } - predictors[frame] = fpredictor; - error[frame] = ferror; - count2[fpredictor]++; - } - for (int i = 0; i < active_count; i++) { - if (count2[i] == 0) { - return i; - } - } - return active_count; -} - -// Find the frame where the error is highest, relative to the best case. -static size_t vadpcm_worst_frame(size_t frame_count, - const float *restrict best_error, - const float *restrict error) { - float best_improvement = error[0] - best_error[0]; - size_t best_index = 0; - for (size_t frame = 1; frame < frame_count; frame++) { - float improvement = error[frame] - best_error[frame]; - if (improvement > best_improvement) { - best_improvement = improvement; - best_index = frame; - } - } - return best_index; -} - -// Assign a predictor to each frame. The predictors array should be initialized -// to zero. -static void vadpcm_assign_predictors(size_t frame_count, int predictor_count, - const float (*restrict corr)[6], - const float *restrict best_error, - float *restrict error, - uint8_t *restrict predictors) { - int unassigned = predictor_count; - int active_count = 1; - for (int iter = 0; iter < kVADPCMIterations; iter++) { - if (unassigned < predictor_count) { - size_t worst = vadpcm_worst_frame(frame_count, best_error, error); - predictors[worst] = unassigned; - if (unassigned >= active_count) { - active_count = unassigned + 1; - } - } - unassigned = vadpcm_refine_predictors(frame_count, active_count, corr, - error, predictors); - } -} - -// Calculate codebook vectors for one predictor, given the predictor -// coefficients. -static void vadpcm_make_vectors( - const double coeff[restrict static 2], - struct vadpcm_vector vectors[restrict static 2]) { - double scale = (double)(1 << 11); - for (int i = 0; i < 2; i++) { - double x1 = 0.0, x2 = 0.0; - if (i == 0) { - x2 = scale; - } else { - x1 = scale; - } - for (int j = 0; j < 8; j++) { - double x = coeff[0] * x1 + coeff[1] * x2; - int value; - if (x > 0x7fff) { - value = 0x7fff; - } else if (x < -0x8000) { - value = -0x8000; - } else { - value = lrint(x); - } - vectors[i].v[j] = value; - x2 = x1; - x1 = x; - } - } -} - -// Create a codebook, given the frame autocorrelation matrixes and the -// assignment from frames to predictors. -static void vadpcm_make_codebook(size_t frame_count, int predictor_count, - const float (*restrict corr)[6], - const uint8_t *restrict predictors, - struct vadpcm_vector *restrict codebook) { - double pcorr[kVADPCMMaxPredictorCount][6]; - int count[kVADPCMMaxPredictorCount]; - vadpcm_meancorrs(frame_count, predictor_count, corr, predictors, pcorr, - count); - for (int i = 0; i < predictor_count; i++) { - if (count[i] > 0) { - double coeff[2]; - vadpcm_solve(pcorr[i], coeff); - vadpcm_make_vectors(coeff, codebook + 2 * i); - } else { - memset(codebook + 2 * i, 0, sizeof(struct vadpcm_vector) * 2); - } - } -} - -static int vadpcm_getshift(int min, int max) { - int shift = 0; - while (shift < 12 && (min < -8 || 7 < max)) { - min >>= 1; - max >>= 1; - shift++; - } - return shift; -} - -size_t vadpcm_encode_scratch_size(size_t frame_count) { - return frame_count * (sizeof(float) * 8 + 1); -} - -static uint32_t vadpcm_rng(uint32_t state) { - // 0xd9f5: Computationally Easy, Spectrally Good Multipliers for - // Congruential Pseudorandom Number Generators, Steele and Vigna, Table 7, - // p.18 - // - // 0x6487ed51: pi << 29, relatively prime. - return state * 0xd9f5 + 0x6487ed51; -} - -// Encode audio as VADPCM, given the assignment of each frame to a predictor. -static void vadpcm_encode_data(size_t frame_count, void *restrict dest, - const int16_t *restrict src, - const uint8_t *restrict predictors, - const struct vadpcm_vector *restrict codebook) { - uint32_t rng_state = 0; - uint8_t *destptr = dest; - int state[4]; - state[0] = 0; - state[1] = 0; - for (size_t frame = 0; frame < frame_count; frame++) { - unsigned predictor = predictors[frame]; - const struct vadpcm_vector *restrict pvec = codebook + 2 * predictor; - int accumulator[8], s0, s1, s, a, r, min, max; - - // Calculate the residual with full precision, and figure out the - // scaling factor necessary to encode it. - state[2] = src[frame * 16 + 6]; - state[3] = src[frame * 16 + 7]; - min = 0; - max = 0; - for (int vector = 0; vector < 2; vector++) { - s0 = state[vector * 2]; - s1 = state[vector * 2 + 1]; - for (int i = 0; i < 8; i++) { - accumulator[i] = (src[frame * 16 + vector * 8 + i] << 11) - - s0 * pvec[0].v[i] - s1 * pvec[1].v[i]; - } - for (int i = 0; i < 8; i++) { - s = accumulator[i] >> 11; - if (s < min) { - min = s; - } - if (s > max) { - max = s; - } - for (int j = 0; j < 7 - i; j++) { - accumulator[i + 1 + j] -= s * pvec[1].v[j]; - } - } - } - int shift = vadpcm_getshift(min, max); - - // Try a range of 3 shift values, and use the shift value that produces - // the lowest error. - double best_error = 0.0; - int min_shift = shift > 0 ? shift - 1 : 0; - int max_shift = shift < 12 ? shift + 1 : 12; - uint32_t init_state = rng_state; - for (shift = min_shift; shift <= max_shift; shift++) { - rng_state = init_state; - uint8_t fout[8]; - double error = 0.0; - s0 = state[0]; - s1 = state[1]; - for (int vector = 0; vector < 2; vector++) { - for (int i = 0; i < 8; i++) { - accumulator[i] = s0 * pvec[0].v[i] + s1 * pvec[1].v[i]; - } - for (int i = 0; i < 8; i++) { - s = src[frame * 16 + vector * 8 + i]; - a = accumulator[i] >> 11; - // Calculate the residual, encode as 4 bits. - int bias = (rng_state >> 16) >> (16 - shift); - rng_state = vadpcm_rng(rng_state); - r = (s - a + bias) >> shift; - if (r > 7) { - r = 7; - } else if (r < -8) { - r = -8; - } - accumulator[i] = r; - // Update state to match decoder. - int sout = r << shift; - for (int j = 0; j < 7 - i; j++) { - accumulator[i + 1 + j] += sout * pvec[1].v[j]; - } - sout += a; - s0 = s1; - s1 = sout; - // Track encoding error. - double serror = s - sout; - error += serror * serror; - } - for (int i = 0; i < 4; i++) { - fout[vector * 4 + i] = ((accumulator[2 * i] & 15) << 4) | - (accumulator[2 * i + 1] & 15); - } - } - if (shift == min_shift || error < best_error) { - destptr[frame * 9] = (shift << 4) | predictor; - memcpy(destptr + frame * 9 + 1, fout, 8); - state[2] = s0; - state[3] = s1; - best_error = error; - } - } - state[0] = state[2]; - state[1] = state[3]; - } -} - -vadpcm_error vadpcm_encode(const struct vadpcm_params *restrict params, - struct vadpcm_vector *restrict codebook, - size_t frame_count, void *restrict dest, - const int16_t *restrict src, void *scratch) { - int predictor_count = params->predictor_count; - if (predictor_count < 1 || kVADPCMMaxPredictorCount < predictor_count) { - return kVADPCMErrInvalidParams; - } - - // Early exit if there is no data to encode. - if (frame_count == 0) { - memset(codebook, 0, - sizeof(*codebook) * kVADPCMEncodeOrder * predictor_count); - } - - // Divide up scratch memory. - float(*restrict corr)[6]; - float *restrict best_error; - float *restrict error; - uint8_t *restrict predictors; - { - char *ptr = scratch; - corr = (void *)ptr; - ptr += sizeof(*corr) * frame_count; - best_error = (void *)ptr; - ptr += sizeof(*best_error) * frame_count; - error = (void *)ptr; - ptr += sizeof(*error) * frame_count; - predictors = (void *)ptr; - } - - vadpcm_autocorr(frame_count, corr, src); - for (size_t i = 0; i < frame_count; i++) { - predictors[i] = 0; - } - if (predictor_count > 1) { - vadpcm_best_error(frame_count, corr, best_error); - vadpcm_assign_predictors(frame_count, predictor_count, corr, best_error, - error, predictors); - } - vadpcm_make_codebook(frame_count, predictor_count, corr, predictors, - codebook); - vadpcm_encode_data(frame_count, dest, src, predictors, codebook); - return 0; -} - -#if TEST -#include "lib/vadpcm/test.h" - -#include -#include -#include - -#pragma GCC diagnostic ignored "-Wdouble-promotion" - -static void test_autocorr(void) { - // Check that the error for a set of coefficients can be calculated using - // the autocorrelation matrix. - static const float coeff[2] = {0.5f, 0.25f}; - - uint32_t state = 1; - int failures = 0; - for (int test = 0; test < 10; test++) { - // Generate some random data. - int16_t data[kVADPCMFrameSampleCount * 2]; - for (int i = 0; i < kVADPCMFrameSampleCount * 2; i++) { - data[i] = 0; - } - for (int i = 0; i <= 4; i++) { - int n = (kVADPCMFrameSampleCount * 2) >> i; - int m = 1 << i; - for (int j = 0; j < n; j++) { - // Random number in -2^13..2^13-1. - int s = (int)(state >> 19) - (1 << 12); - state = vadpcm_rng(state); - for (int k = 0; k < m; k++) { - data[j * m + k] += s; - } - } - } - - // Get the autocorrelation. - float corr[2][6]; - vadpcm_autocorr(2, corr, data); - - // Calculate error directly. - float s1 = (float)data[kVADPCMFrameSampleCount - 2] * (1.0f / 32768.0f); - float s2 = (float)data[kVADPCMFrameSampleCount - 1] * (1.0f / 32768.0f); - float error = 0; - for (int i = 0; i < kVADPCMFrameSampleCount; i++) { - float s = data[kVADPCMFrameSampleCount + i] * (1.0 / 32768.0); - float d = s - coeff[1] * s1 - coeff[0] * s2; - error += d * d; - s1 = s2; - s2 = s; - } - - // Calculate error from autocorrelation matrix. - float eval = vadpcm_eval(corr[1], coeff); - - if (fabsf(error - eval) > (error + eval) * 1.0e-4f) { - fprintf(stderr, - "test_autororr case %d: " - "error = %f, eval = %f, relative error = %f\n", - test, error, eval, fabsf(error - eval) / (error + eval)); - failures++; - } - } - if (failures > 0) { - fprintf(stderr, "test_autocorr failures: %d\n", failures); - test_failure_count++; - } -} - -static void test_solve(void) { - // Check that vadpcm_solve minimizes vadpcm_eval. - static const double dcorr[][6] = { - // Simple positive definite matrixes. - {4.0, 1.0, 5.0, 2.0, 3.0, 6.0}, - {4.0, -1.0, 5.0, -2.0, -3.0, 6.0}, - {4.0, 1.0, 6.0, 2.0, 3.0, 5.0}, - // Singular matrixes. - {1.0, 0.5, 1.0, 0.0, 0.0, 0.0}, - {1.0, 0.0, 0.0, 0.5, 0.0, 1.0}, - {1.0, 0.25, 2.0, 0.25, 2.0, 2.0}, - // Zero submatrix. - {1.0, 0.0, 0.0, 0.0, 0.0, 0.0}, - // Zero. - {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, - }; - static const float offset[][2] = { - {1.0f, 0.0f}, - {0.0f, 1.0f}, - {-1.0f, 0.0f}, - {0.0f, -1.0f}, - }; - static const float offset_amt = 0.01f; - int failures = 0; - for (size_t test = 0; test < sizeof(dcorr) / sizeof(*dcorr); test++) { - double dcoeff[2]; - vadpcm_solve(dcorr[test], dcoeff); - float corr[6]; - float coeff[2]; - for (int i = 0; i < 6; i++) { - corr[i] = (float)dcorr[test][i]; - } - for (int i = 0; i < 2; i++) { - coeff[i] = (float)dcoeff[i]; - } - // Test that this is a local minimum. - float error = vadpcm_eval(corr, coeff); - if (error < 0.0f) { - fprintf(stderr, "test_solve case %zu: negative error\n", test); - failures++; - continue; - } - float min_error = error - error * (1.0f / 65536.0f); - for (size_t off = 0; off < sizeof(offset) / sizeof(*offset); off++) { - float ocoeff[2]; - for (int i = 0; i < 2; i++) { - ocoeff[i] = coeff[i] + offset[off][i] * offset_amt; - } - float oerror = vadpcm_eval(corr, ocoeff); - if (oerror < min_error) { - fprintf(stderr, "test_solve case %zu: not a local minimum\n", - test); - failures++; - } - } - // Check that eval_solved() gives us the same result as eval(). - double error2 = vadpcm_eval_solved(dcorr[test], dcoeff); - if (fabs(error2 - (double)error) > (double)error * (1.0 / 65536.0)) { - fprintf(stderr, - "test_solve case %zu: eval_solved() is incorrect\n" - "\teval_solved = %f\n" - "\teval = %f\n", - test, error2, (double)error); - failures++; - continue; - } - } - if (failures > 0) { - fprintf(stderr, "test_solve failures: %d\n", failures); - test_failure_count++; - } -} - -void test_encoder(void) { - test_autocorr(); - test_solve(); -} - -static int vadpcm_ext4(int x) { - return x > 7 ? x - 16 : x; -} - -static void show_raw(const char *name, const uint8_t *data) { - fprintf(stderr, "%s: scale = %d, predictor = %d\n%s:", name, data[0] >> 4, - data[0] & 15, name); - for (int i = 1; i < kVADPCMFrameByteSize; i++) { - fprintf(stderr, "%8d%8d", vadpcm_ext4(data[i] >> 4), - vadpcm_ext4(data[i] & 15)); - } - fputc('\n', stderr); -} - -void test_reencode(const char *name, int predictor_count, int order, - struct vadpcm_vector *codebook, size_t frame_count, - const void *vadpcm) { - // Take a VADPCM file. Decode it, then reencode it with the same codebook, - // and then decode it again. The decoded PCM data should match. - int16_t *pcm1 = NULL, *pcm2 = NULL; - uint8_t *adpcm2 = NULL; - uint8_t *predictors = NULL; - vadpcm_error err; - size_t sample_count = frame_count * kVADPCMFrameSampleCount; - - struct vadpcm_vector state; - memset(&state, 0, sizeof(state)); - pcm1 = xmalloc(sizeof(*pcm1) * sample_count); - err = vadpcm_decode(predictor_count, order, codebook, &state, frame_count, - pcm1, vadpcm); - if (err != 0) { - fprintf(stderr, "error: test_reencode %s: decode (first): %s", name, - vadpcm_error_name2(err)); - test_failure_count++; - goto done; - } - predictors = xmalloc(frame_count); - for (size_t frame = 0; frame < frame_count; frame++) { - predictors[frame] = - ((const uint8_t *)vadpcm)[kVADPCMFrameByteSize * frame] & 15; - } - adpcm2 = xmalloc(kVADPCMFrameByteSize * frame_count); - vadpcm_encode_data(frame_count, adpcm2, pcm1, predictors, codebook); - pcm2 = xmalloc(kVADPCMFrameSampleCount * sizeof(int16_t) * frame_count); - memset(&state, 0, sizeof(state)); - err = vadpcm_decode(predictor_count, order, codebook, &state, frame_count, - pcm2, adpcm2); - if (err != 0) { - fprintf(stderr, "error: test_reencode %s: decode (second): %s", name, - vadpcm_error_name2(err)); - test_failure_count++; - goto done; - } - for (size_t i = 0; i < frame_count * kVADPCMFrameSampleCount; i++) { - if (pcm1[i] != pcm2[i]) { - fprintf(stderr, - "error: reencode %s: output does not match, " - "index = %zu\n", - name, i); - size_t frame = i / kVADPCMFrameSampleCount; - fputs("vadpcm:\n", stderr); - show_raw("raw", - (const uint8_t *)vadpcm + kVADPCMFrameByteSize * frame); - show_raw("out", adpcm2 + kVADPCMFrameByteSize * frame); - fputs("pcm:\n", stderr); - show_pcm_diff(pcm1 + frame * kVADPCMFrameSampleCount, - pcm2 + frame * kVADPCMFrameSampleCount); - test_failure_count++; - goto done; - } - } - -done: - free(pcm1); - free(pcm2); - free(adpcm2); - free(predictors); -} - -#endif // TEST diff --git a/tools/audioconv64/vadpcm/update-vadcpm.sh b/tools/audioconv64/vadpcm/update-vadcpm.sh new file mode 100644 index 000000000..ada79afc8 --- /dev/null +++ b/tools/audioconv64/vadpcm/update-vadcpm.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +PREFIX="tools/audioconv64/vadpcm" +REMOTE_URL="https://github.com/depp/vadpcm.git" +BRANCH="main" + +repo_root="$(git rev-parse --show-toplevel)" +cd "$repo_root" + +git fetch "$REMOTE_URL" "$BRANCH" +split_commit="$(git subtree split --prefix=codec FETCH_HEAD)" +git subtree pull --prefix "$PREFIX/codec" . "$split_commit" --squash +echo "Update applied." diff --git a/tools/audioconv64/vadpcm_pack.h b/tools/audioconv64/vadpcm_pack.h new file mode 100644 index 000000000..7dbadc9be --- /dev/null +++ b/tools/audioconv64/vadpcm_pack.h @@ -0,0 +1,69 @@ +// Native sub-nibble packing for VADPCM frames. +// +// libvadpcm always emits the classic 9-byte frame: one control byte followed +// by 16 residuals packed as nibbles. When the encoder is restricted to a +// smaller residual range (audioconv64 "bits=" option), those nibbles waste +// 1 or 2 bits each, so the frames are repacked to the dense layout the mixer +// ucode understands: +// +// residual i occupies payload bits [i*bits, i*bits + bits) +// +// counting payload bit 0 as the most significant bit of payload byte 0. For +// bits==4 this is exactly the input layout, so the repack is the identity. + +#pragma once + +#include +#include +#include + +// Bytes of a VADPCM frame with `bits`-wide residuals (16 samples per frame). +static inline int vadpcm_frame_bytes(int bits) { return 2*bits + 1; } + +// Read residual `i` of a 9-byte frame, sign-extended. +static inline int vadpcm_get_nibble(const uint8_t *frame, int i) { + int r = (frame[1 + i/2] >> ((i & 1) ? 0 : 4)) & 0xF; + return (r ^ 8) - 8; +} + +// Repack one 9-byte frame into `vadpcm_frame_bytes(bits)` bytes. +static inline void vadpcm_pack_frame(uint8_t *dst, const uint8_t *src, int bits) { + assert(bits >= 2 && bits <= 4); + dst[0] = src[0]; + memset(dst + 1, 0, 2*bits); + for (int i = 0; i < 16; i++) { + int r = vadpcm_get_nibble(src, i); + assert(r >= -(1 << (bits-1)) && r < (1 << (bits-1))); + for (int b = 0; b < bits; b++) { + int p = i*bits + b; + dst[1 + p/8] |= ((r >> (bits-1-b)) & 1) << (7 - (p & 7)); + } + } +} + +// Inverse of vadpcm_pack_frame, used to validate the repack. +static inline void vadpcm_unpack_frame(uint8_t *dst, const uint8_t *src, int bits) { + dst[0] = src[0]; + memset(dst + 1, 0, 8); + for (int i = 0; i < 16; i++) { + int r = 0; + for (int b = 0; b < bits; b++) { + int p = i*bits + b; + r = (r << 1) | ((src[1 + p/8] >> (7 - (p & 7))) & 1); + } + r = (r ^ (1 << (bits-1))) - (1 << (bits-1)); + dst[1 + i/2] |= (r & 0xF) << ((i & 1) ? 0 : 4); + } +} + +// Repack a whole buffer of `nframes` frames. Returns the output size. +static inline int vadpcm_pack_frames(uint8_t *dst, const uint8_t *src, int nframes, int bits) { + int fb = vadpcm_frame_bytes(bits); + for (int i = 0; i < nframes; i++) { + vadpcm_pack_frame(dst + i*fb, src + i*9, bits); + uint8_t check[9]; + vadpcm_unpack_frame(check, dst + i*fb, bits); + assert(memcmp(check, src + i*9, 9) == 0); + } + return nframes * fb; +} diff --git a/tools/build-gdb.sh b/tools/build-gdb.sh index 109eab502..63f53538d 100755 --- a/tools/build-gdb.sh +++ b/tools/build-gdb.sh @@ -5,6 +5,7 @@ # Bash strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/ set -euo pipefail +set -x IFS=$'\n\t' # Check that N64_INST is defined @@ -14,6 +15,14 @@ if [ -z "${N64_INST-}" ]; then exit 1 fi +# Path where the toolchain will be built. +BUILD_PATH="${BUILD_PATH:-toolchain}" +DOWNLOAD_PATH="${DOWNLOAD_PATH:-$BUILD_PATH}" + +# Redirect output to a log file +exec > >(tee "$BUILD_PATH/build-gdb.log") 2>&1 +echo "Build started at: $(date)" + # Dependency source libs (Versions) GDB_V=16.2 @@ -23,9 +32,6 @@ N64_TARGET=${N64_TARGET:-mips64-elf} # Set N64_INST before calling the script to change the default installation directory path INSTALL_PATH="${N64_INST}" -# Path where the toolchain will be built. -BUILD_PATH="${BUILD_PATH:-toolchain}" -DOWNLOAD_PATH="${DOWNLOAD_PATH:-$BUILD_PATH}" # Determine how many parallel Make jobs to run based on CPU count JOBS="${JOBS:-$(getconf _NPROCESSORS_ONLN)}" @@ -49,11 +55,15 @@ command_exists () { # Download the file URL using wget or curl (depending on which is installed) download () { + local n_retries=5 + local retry_delay=30 local url="$1" local file="$DOWNLOAD_PATH/$(basename "$url")" local tmpfile="$file.part" - if command_exists wget ; then wget --continue --output-document "$tmpfile" "$url" - elif command_exists curl ; then curl --location --output "$tmpfile" "$url" + if command_exists wget ; then + wget --tries=$n_retries --wait=$retry_delay --continue --output-document "$tmpfile" "$url" + elif command_exists curl ; then + curl --retry $n_retries --retry-all-errors --retry-delay $retry_delay --location --output "$tmpfile" "$url" else echo "Install wget or curl to download toolchain sources" 1>&2 return 1 @@ -61,20 +71,24 @@ download () { mv "$tmpfile" "$file" } +download_gnu () { + download "https://mirrors.kernel.org/gnu/$1" +} + # Dependency downloads and unpack -test -f "$DOWNLOAD_PATH/gdb-$GDB_V.tar.gz" || download "https://ftp.gnu.org/gnu/gdb/gdb-$GDB_V.tar.gz" +test -f "$DOWNLOAD_PATH/gdb-$GDB_V.tar.gz" || download_gnu "gdb/gdb-$GDB_V.tar.gz" test -d "$BUILD_PATH/gdb-$GDB_V" || tar -xzf "$DOWNLOAD_PATH/gdb-$GDB_V.tar.gz" -C "$BUILD_PATH" # Resolve dependencies on macOS via homebrew if [[ $OSTYPE == 'darwin'* ]]; then - # Tell GDB configure to use Homebrew's GMP, MPFR, MPC, and Zlib. + # Tell GDB configure to use Homebrew's Python, GMP, MPFR, MPC, and Zlib. # These should have already been installed by build-toolchain.sh GDB_CONFIGURE_ARGS=( "--with-gmp=$(brew --prefix gmp)" "--with-mpfr=$(brew --prefix mpfr)" "--with-mpc=$(brew --prefix libmpc)" "--with-isl=$(brew --prefix isl)" - "--with-python=python3" + "--with-python=$(brew --prefix python3)/bin/python3" "--with-system-zlib" ) elif [ "$N64_HOST" == "x86_64-w64-mingw32" ]; then @@ -113,6 +127,7 @@ make install-strip || sudo make install-strip || su -c "make install-strip" popd # Final message +set +x echo echo "***********************************************" echo "GDB correctly built and installed to LibDragon toolchain" diff --git a/tools/build-toolchain.sh b/tools/build-toolchain.sh index e1e504696..08c8b90cb 100755 --- a/tools/build-toolchain.sh +++ b/tools/build-toolchain.sh @@ -21,6 +21,7 @@ DOWNLOAD_PATH="${DOWNLOAD_PATH:-$BUILD_PATH}" # Redirect output to a log file exec > >(tee "$BUILD_PATH/build-toolchain.log") 2>&1 +echo "Build started at: $(date)" # Additional directories SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -69,11 +70,15 @@ command_exists () { # Download the file URL using wget or curl (depending on which is installed) download () { + local n_retries=5 + local retry_delay=30 local url="$1" local file="$DOWNLOAD_PATH/$(basename "$url")" local tmpfile="$file.part" - if command_exists wget ; then wget --continue --output-document "$tmpfile" "$url" - elif command_exists curl ; then curl --location --output "$tmpfile" "$url" + if command_exists wget ; then + wget --tries=$n_retries --wait=$retry_delay --continue --output-document "$tmpfile" "$url" + elif command_exists curl ; then + curl --retry $n_retries --retry-all-errors --retry-delay $retry_delay --location --output "$tmpfile" "$url" else echo "Install wget or curl to download toolchain sources" 1>&2 return 1 @@ -81,6 +86,10 @@ download () { mv "$tmpfile" "$file" } +download_gnu () { + download "https://mirrors.kernel.org/gnu/$1" +} + # Compilation on macOS via homebrew if [[ $OSTYPE == 'darwin'* ]]; then if ! command_exists brew; then @@ -127,17 +136,17 @@ else fi # Dependency downloads and unpack -test -f "$DOWNLOAD_PATH/binutils-$BINUTILS_V.tar.gz" || download "https://ftp.gnu.org/gnu/binutils/binutils-$BINUTILS_V.tar.gz" +test -f "$DOWNLOAD_PATH/binutils-$BINUTILS_V.tar.gz" || download_gnu "binutils/binutils-$BINUTILS_V.tar.gz" test -d "$BUILD_PATH/binutils-$BINUTILS_V" || tar -xzf "$DOWNLOAD_PATH/binutils-$BINUTILS_V.tar.gz" -C "$BUILD_PATH" -test -f "$DOWNLOAD_PATH/gcc-$GCC_V.tar.gz" || download "https://ftp.gnu.org/gnu/gcc/gcc-$GCC_V/gcc-$GCC_V.tar.gz" +test -f "$DOWNLOAD_PATH/gcc-$GCC_V.tar.gz" || download_gnu "gcc/gcc-$GCC_V/gcc-$GCC_V.tar.gz" test -d "$BUILD_PATH/gcc-$GCC_V" || tar -xzf "$DOWNLOAD_PATH/gcc-$GCC_V.tar.gz" -C "$BUILD_PATH" test -f "$DOWNLOAD_PATH/newlib-$NEWLIB_V.tar.gz" || download "https://sourceware.org/pub/newlib/newlib-$NEWLIB_V.tar.gz" test -d "$BUILD_PATH/newlib-$NEWLIB_V" || tar -xzf "$DOWNLOAD_PATH/newlib-$NEWLIB_V.tar.gz" -C "$BUILD_PATH" if [ "$GMP_V" != "" ]; then - test -f "$DOWNLOAD_PATH/gmp-$GMP_V.tar.bz2" || download "https://ftp.gnu.org/gnu/gmp/gmp-$GMP_V.tar.bz2" + test -f "$DOWNLOAD_PATH/gmp-$GMP_V.tar.bz2" || download_gnu "gmp/gmp-$GMP_V.tar.bz2" test -d "$BUILD_PATH/gmp-$GMP_V" || tar -xf "$DOWNLOAD_PATH/gmp-$GMP_V.tar.bz2" -C "$BUILD_PATH" # note: no .gz download file currently available pushd "$BUILD_PATH/gcc-$GCC_V" ln -sf ../"gmp-$GMP_V" "gmp" @@ -145,7 +154,7 @@ if [ "$GMP_V" != "" ]; then fi if [ "$MPC_V" != "" ]; then - test -f "$DOWNLOAD_PATH/mpc-$MPC_V.tar.gz" || download "https://ftp.gnu.org/gnu/mpc/mpc-$MPC_V.tar.gz" + test -f "$DOWNLOAD_PATH/mpc-$MPC_V.tar.gz" || download_gnu "mpc/mpc-$MPC_V.tar.gz" test -d "$BUILD_PATH/mpc-$MPC_V" || tar -xzf "$DOWNLOAD_PATH/mpc-$MPC_V.tar.gz" -C "$BUILD_PATH" pushd "$BUILD_PATH/gcc-$GCC_V" ln -sf ../"mpc-$MPC_V" "mpc" @@ -153,7 +162,7 @@ if [ "$MPC_V" != "" ]; then fi if [ "$MPFR_V" != "" ]; then - test -f "$DOWNLOAD_PATH/mpfr-$MPFR_V.tar.gz" || download "https://ftp.gnu.org/gnu/mpfr/mpfr-$MPFR_V.tar.gz" + test -f "$DOWNLOAD_PATH/mpfr-$MPFR_V.tar.gz" || download_gnu "mpfr/mpfr-$MPFR_V.tar.gz" test -d "$BUILD_PATH/mpfr-$MPFR_V" || tar -xzf "$DOWNLOAD_PATH/mpfr-$MPFR_V.tar.gz" -C "$BUILD_PATH" pushd "$BUILD_PATH/gcc-$GCC_V" ln -sf ../"mpfr-$MPFR_V" "mpfr" @@ -161,7 +170,7 @@ if [ "$MPFR_V" != "" ]; then fi if [ "$MAKE_V" != "" ]; then - test -f "$DOWNLOAD_PATH/make-$MAKE_V.tar.gz" || download "https://ftp.gnu.org/gnu/make/make-$MAKE_V.tar.gz" + test -f "$DOWNLOAD_PATH/make-$MAKE_V.tar.gz" || download_gnu "make/make-$MAKE_V.tar.gz" test -d "$BUILD_PATH/make-$MAKE_V" || tar -xzf "$DOWNLOAD_PATH/make-$MAKE_V.tar.gz" -C "$BUILD_PATH" fi @@ -324,6 +333,7 @@ CFLAGS_FOR_TARGET="-DHAVE_ASSERT_FUNC -O2 -fpermissive" \ --with-cpu=mips64vr4300 \ --disable-libssp \ --disable-werror \ + --enable-newlib-io-c99-formats \ --enable-newlib-multithread \ --enable-newlib-retargetable-locking make -j "$JOBS" @@ -393,6 +403,7 @@ else --with-cpu=mips64vr4300 \ --disable-libssp \ --disable-werror \ + --enable-newlib-io-c99-formats \ --enable-newlib-multithread \ --enable-newlib-retargetable-locking make -j "$JOBS" @@ -421,6 +432,21 @@ if [ "$MAKE_V" != "" ]; then popd fi +# Create a toolchain.version file in JSON format to identify the toolchain version. +# It contains: GCC version, Binutils versions and Newlib/Picolibc version. +TOOLCHAIN_VERSION_FILE="$INSTALL_PATH/$N64_TARGET/include/toolchain.version" + +VERSION_CONTENT="{ + \"host\": \"$N64_HOST\", + \"binutils\": \"$BINUTILS_V\", + \"gcc\": \"$GCC_V\", + \"newlib\": \"$NEWLIB_V\" +}" + +printf '%s\n' "$VERSION_CONTENT" > "$TOOLCHAIN_VERSION_FILE" || \ + sudo sh -c "printf '%s\\n' \"$VERSION_CONTENT\" > \"$TOOLCHAIN_VERSION_FILE\"" || \ + su -c "printf '%s\\n' \"$VERSION_CONTENT\" > \"$TOOLCHAIN_VERSION_FILE\"" + # Final message set +x echo diff --git a/tools/common/aplib_compress.c b/tools/common/aplib_compress.c index 66420dd5f..c6d39764d 100644 --- a/tools/common/aplib_compress.c +++ b/tools/common/aplib_compress.c @@ -1,3 +1,4 @@ +#include "../common/polyfill.h" // apultra #include "apultra/matchfinder.c" #include "apultra/shrink.c" diff --git a/tools/common/apultra/shrink.c b/tools/common/apultra/shrink.c index ac9ba096e..f11957284 100644 --- a/tools/common/apultra/shrink.c +++ b/tools/common/apultra/shrink.c @@ -1058,7 +1058,7 @@ static int apultra_reduce_commands(apultra_compressor *pCompressor, const unsign * * @return size of compressed data in output buffer, or -1 if the data is uncompressible */ -static int apultra_write_block(apultra_compressor *pCompressor, const apultra_final_match *pBestMatch, const unsigned char *pInWindow, const int nStartOffset, const int nEndOffset, unsigned char *pOutData, const int nMaxOutDataSize, int *nCurBitsOffset, int *nCurBitShift, int *nFollowsLiteral, int *nCurRepMatchOffset, const int nBlockFlags) { +static int apultra_write_block(apultra_compressor *pCompressor, const apultra_final_match *pBestMatch, const unsigned char *pInWindow, const int nStartOffset, const int nEndOffset, unsigned char *pOutData, const int nMaxOutDataSize, int *nCurBitsOffset, int *nCurBitShift, int *nFollowsLiteral, int *nCurRepMatchOffset, const int nBlockFlags, const int nPriorDelta) { int i; int nRepMatchOffset = *nCurRepMatchOffset; int nCurFollowsLiteral = *nFollowsLiteral; @@ -1203,7 +1203,7 @@ static int apultra_write_block(apultra_compressor *pCompressor, const apultra_fi nCurFollowsLiteral = 1; } - const int nCurSafeDist = (i - nStartOffset) - nOutOffset; + const int nCurSafeDist = nPriorDelta + (i - nStartOffset) - nOutOffset; if (nCurSafeDist >= 0 && pCompressor->stats.safe_dist < nCurSafeDist) pCompressor->stats.safe_dist = nCurSafeDist; } @@ -1219,7 +1219,7 @@ static int apultra_write_block(apultra_compressor *pCompressor, const apultra_fi pCompressor->stats.num_eod++; pCompressor->stats.commands_divisor++; - const int nCurSafeDist = (i - nStartOffset) - nOutOffset; + const int nCurSafeDist = nPriorDelta + (i - nStartOffset) - nOutOffset; if (nCurSafeDist >= 0 && pCompressor->stats.safe_dist < nCurSafeDist) pCompressor->stats.safe_dist = nCurSafeDist; } @@ -1247,7 +1247,7 @@ static int apultra_write_block(apultra_compressor *pCompressor, const apultra_fi * * @return size of compressed data in output buffer, or -1 if the data is uncompressible */ -static int apultra_optimize_and_write_block(apultra_compressor *pCompressor, const unsigned char *pInWindow, const int nPreviousBlockSize, const int nInDataSize, unsigned char *pOutData, const int nMaxOutDataSize, int *nCurBitsOffset, int *nCurBitShift, int *nCurFollowsLiteral, int *nCurRepMatchOffset, const int nBlockFlags) { +static int apultra_optimize_and_write_block(apultra_compressor *pCompressor, const unsigned char *pInWindow, const int nPreviousBlockSize, const int nInDataSize, unsigned char *pOutData, const int nMaxOutDataSize, int *nCurBitsOffset, int *nCurBitShift, int *nCurFollowsLiteral, int *nCurRepMatchOffset, const int nBlockFlags, const int nPriorDelta) { const int nEndOffset = nPreviousBlockSize + nInDataSize; const int nArrivalsPerPosition = pCompressor->max_arrivals; int *rle_len = (int*)pCompressor->intervals /* reuse */; @@ -1441,7 +1441,7 @@ static int apultra_optimize_and_write_block(apultra_compressor *pCompressor, con /* Write compressed block */ - return apultra_write_block(pCompressor, pCompressor->best_match - nPreviousBlockSize, pInWindow, nPreviousBlockSize, nEndOffset, pOutData, nMaxOutDataSize, nCurBitsOffset, nCurBitShift, nCurFollowsLiteral, nCurRepMatchOffset, nBlockFlags); + return apultra_write_block(pCompressor, pCompressor->best_match - nPreviousBlockSize, pInWindow, nPreviousBlockSize, nEndOffset, pOutData, nMaxOutDataSize, nCurBitsOffset, nCurBitShift, nCurFollowsLiteral, nCurRepMatchOffset, nBlockFlags, nPriorDelta); } /* Forward declaration */ @@ -1615,7 +1615,7 @@ static void apultra_compressor_destroy(apultra_compressor *pCompressor) { * * @return size of compressed data in output buffer, or -1 if the data is uncompressible */ -static int apultra_compressor_shrink_block(apultra_compressor *pCompressor, const unsigned char *pInWindow, const int nPreviousBlockSize, const int nInDataSize, unsigned char *pOutData, const int nMaxOutDataSize, int *nCurBitsOffset, int *nCurBitShift, int *nCurFollowsLiteral, int *nCurRepMatchOffset, const int nBlockFlags) { +static int apultra_compressor_shrink_block(apultra_compressor *pCompressor, const unsigned char *pInWindow, const int nPreviousBlockSize, const int nInDataSize, unsigned char *pOutData, const int nMaxOutDataSize, int *nCurBitsOffset, int *nCurBitShift, int *nCurFollowsLiteral, int *nCurRepMatchOffset, const int nBlockFlags, const int nPriorDelta) { int nCompressedSize; if (apultra_build_suffix_array(pCompressor, pInWindow, nPreviousBlockSize + nInDataSize)) @@ -1626,7 +1626,7 @@ static int apultra_compressor_shrink_block(apultra_compressor *pCompressor, cons } apultra_find_all_matches(pCompressor, NMATCHES_PER_INDEX, nPreviousBlockSize, nPreviousBlockSize + nInDataSize, nBlockFlags); - nCompressedSize = apultra_optimize_and_write_block(pCompressor, pInWindow, nPreviousBlockSize, nInDataSize, pOutData, nMaxOutDataSize, nCurBitsOffset, nCurBitShift, nCurFollowsLiteral, nCurRepMatchOffset, nBlockFlags); + nCompressedSize = apultra_optimize_and_write_block(pCompressor, pInWindow, nPreviousBlockSize, nInDataSize, pOutData, nMaxOutDataSize, nCurBitsOffset, nCurBitShift, nCurFollowsLiteral, nCurRepMatchOffset, nBlockFlags, nPriorDelta); } return nCompressedSize; @@ -1716,8 +1716,14 @@ size_t apultra_compress(const unsigned char *pInputData, unsigned char *pOutBuff if ((nOriginalSize + nInDataSize) >= nInputSize) nBlockFlags |= 2; + /* Prior cumulative (decoder-output − decoder-input) at the start + * of this block. Used by safe_dist accounting so it sees the + * GLOBAL output-vs-input lead, not just the per-block local delta. + * Dictionary bytes are not emitted by the decoder, so they're + * excluded from the input side. */ + const int nPriorDelta = (nOriginalSize - (int)nDictionarySize) - nCompressedSize; nOutDataSize = apultra_compressor_shrink_block(&compressor, pInputData + nOriginalSize - nPreviousBlockSize, nPreviousBlockSize, nInDataSize, pOutBuffer + nCompressedSize, nOutDataEnd, - &nCurBitsOffset, &nCurBitShift, &nCurFollowsLiteral, &nCurRepMatchOffset, nBlockFlags); + &nCurBitsOffset, &nCurBitShift, &nCurFollowsLiteral, &nCurRepMatchOffset, nBlockFlags, nPriorDelta); nBlockFlags &= (~1); if (nOutDataSize >= 0) { diff --git a/tools/common/assetcomp.c b/tools/common/assetcomp.c index 4d1d7786a..ba23197c4 100644 --- a/tools/common/assetcomp.c +++ b/tools/common/assetcomp.c @@ -22,6 +22,7 @@ #undef MIN #undef MAX #include "../../src/asset.c" +#include "../../src/utils.c" #include "../../src/compress/aplib_dec.c" #include "../../src/compress/shrinkler_dec.c" #include "../../src/compress/lz4_dec.c" @@ -98,9 +99,13 @@ void asset_compress_mem_raw(int compression, const uint8_t *data, int sz, uint8_ *margin = stats.safe_dist + *cmp_size - sz; } break; case 3: { // shrinkler - *winsize = 256*1024; // FIXME + if (*winsize == 0) { + *winsize = 256*1024; + while (sz < *winsize && *winsize > 2*1024) + *winsize /= 2; + } int inplace_margin; - *output = shrinkler_compress(data, sz, 3, cmp_size, &inplace_margin); + *output = shrinkler_compress(data, sz, 3, *winsize, cmp_size, &inplace_margin); // Shrinkler seems to return negative margin values because we asked to // verify using 4 byte reads. Just clamp to zero. *margin = inplace_margin > 0 ? inplace_margin : 0; @@ -112,6 +117,8 @@ void asset_compress_mem_raw(int compression, const uint8_t *data, int sz, uint8_ /** * @brief Compress or recompress a file in the libdragon asset format. + * + * NOTE: infn and outfn can be the same file, this is explicitly allowed. * * @param infn Input file to (re-)compress * @param outfn Output file @@ -161,6 +168,8 @@ int asset_compress_mem(void *data, int sz, FILE *out, int compression, int winsi winsize /= 2; } + int start_pos = ftell(out); + // FIXME: use asset_compress_mem_raw() instead of duplicating the code here switch (compression) { case 0: { // none @@ -169,23 +178,27 @@ int asset_compress_mem(void *data, int sz, FILE *out, int compression, int winsi return sz; } break; case 3: { // shrinkler - winsize = 256*1024; // FIXME + if (winsize == 0) { + winsize = 256*1024; + while (sz < winsize && winsize > 2*1024) + winsize /= 2; + } int cmp_size; int inplace_margin; - uint8_t *output = shrinkler_compress(data, sz, 3, &cmp_size, &inplace_margin); + uint8_t *output = shrinkler_compress(data, sz, 3, winsize, &cmp_size, &inplace_margin); // Shrinkler seems to return negative margin values because we asked to // verify using 4 byte reads. Just clamp to zero. inplace_margin = inplace_margin > 0 ? inplace_margin : 0; - fwrite("DCA3", 1, 4, out); - w16(out, 3); // algo - w16(out, asset_winsize_to_flags(winsize) | ASSET_FLAG_INPLACE); // flags - w32(out, cmp_size); // cmp_size - w32(out, sz); // dec_size - w32(out, inplace_margin); // inplace margin + fwrite("DCA5", 1, 4, out); + w8(out, asset_winsize_to_flags(winsize) | (3 << ASSET_FLAG_ALGO_SHIFT)); // flags + wleb128u(out, cmp_size); // cmp_size + wleb128u(out, sz); // dec_size + wleb128u(out, inplace_margin); // inplace margin + walign(out, 2); fwrite(output, 1, cmp_size, out); free(output); if (margin) *margin = inplace_margin; - return cmp_size + 20; + return ftell(out) - start_pos; } break; case 2: { // aplib if (winsize == 0) { @@ -203,18 +216,22 @@ int asset_compress_mem(void *data, int sz, FILE *out, int compression, int winsi 0, // dictionary size NULL, // progress callback &stats); + assert(cmp_size <= max_cmp_size); int inplace_margin = stats.safe_dist + cmp_size - sz; - fwrite("DCA3", 1, 4, out); - w16(out, 2); // algo - w16(out, asset_winsize_to_flags(winsize) | ASSET_FLAG_INPLACE); // flags - w32(out, cmp_size); // cmp_size - w32(out, sz); // dec_size - w32(out, inplace_margin); // inplace margin + // FIXME: in rare cases, apultra returns a negative margin... + inplace_margin = inplace_margin > 0 ? inplace_margin : 0; + + fwrite("DCA5", 1, 4, out); + w8(out, asset_winsize_to_flags(winsize) | (2 << ASSET_FLAG_ALGO_SHIFT)); // flags + wleb128u(out, cmp_size); // cmp_size + wleb128u(out, sz); // dec_size + wleb128u(out, inplace_margin); // inplace margin + walign(out, 2); fwrite(output, 1, cmp_size, out); free(output); if (margin) *margin = inplace_margin; - return cmp_size + 20; + return ftell(out) - start_pos; } break; case 1: { // lz4hc // Default for LZ4HC is 8 KiB, which makes sense given the little @@ -245,16 +262,16 @@ int asset_compress_mem(void *data, int sz, FILE *out, int compression, int winsi assert(cmp_size <= cmp_max_size); int inplace_margin = LZ4_DECOMPRESS_INPLACE_MARGIN(cmp_size); - fwrite("DCA3", 1, 4, out); - w16(out, 1); // algo - w16(out, asset_winsize_to_flags(winsize) | ASSET_FLAG_INPLACE); // flags - w32(out, cmp_size); // cmp_size - w32(out, sz); // dec_size - w32(out, inplace_margin); // inplace margin + fwrite("DCA5", 1, 4, out); + w8(out, asset_winsize_to_flags(winsize) | (1 << ASSET_FLAG_ALGO_SHIFT)); // flags + wleb128u(out, cmp_size); // cmp_size + wleb128u(out, sz); // dec_size + wleb128u(out, inplace_margin); // inplace margin + walign(out, 2); fwrite(output, 1, cmp_size, out); free(output); if (margin) *margin = inplace_margin; - return cmp_size + 20; + return ftell(out) - start_pos; } break; default: assert(0); diff --git a/tools/common/binout.c b/tools/common/binout.c index 7fe9779e5..215d7402e 100644 --- a/tools/common/binout.c +++ b/tools/common/binout.c @@ -8,6 +8,9 @@ For more information, please refer to */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif #include #include #include @@ -21,9 +24,9 @@ struct placeholder_data { int64_t offset; uint64_t *pending_offsets_64; - uint32_t *pending_offsets_32; - uint16_t *pending_offsets_16; - uint8_t *pending_offsets_8; + uint64_t *pending_offsets_32; + uint64_t *pending_offsets_16; + uint64_t *pending_offsets_8; }; struct { @@ -54,6 +57,21 @@ void _w64(FILE *f, uint64_t v) w32(f, v & 0xffffffff); } +void wleb128u(FILE *f, uint64_t v) +{ + while (v >= 0x80) { + w8(f, (v & 0x7F) | 0x80); + v >>= 7; + } + w8(f, v & 0x7F); +} + +void wleb128s(FILE *f, int64_t v) +{ + uint64_t uv = (v << 1) ^ (v >> 63); + wleb128u(f, uv); +} + int w32_placeholder(FILE *f) { int pos = ftell(f); diff --git a/tools/common/binout.h b/tools/common/binout.h index 5019721c0..ca008e3b5 100644 --- a/tools/common/binout.h +++ b/tools/common/binout.h @@ -57,6 +57,8 @@ void _w64(FILE *f, uint64_t v); #define wf32(f, v) _w32(f, BITCAST_F2I(v)) #define wf32approx(f, v, prec) wf32(f, roundf((v)/(prec))*(prec)) #define wa(f, v, s) fwrite(v, s, 1, f) +void wleb128u(FILE *f, uint64_t v); +void wleb128s(FILE *f, int64_t v); int w64_placeholder(FILE *f); void w64_placeholdervf(FILE *file, const char *format, va_list arg); diff --git a/tools/common/crc32.c b/tools/common/crc32.c index 8cd2d4dd0..698fc664c 100644 --- a/tools/common/crc32.c +++ b/tools/common/crc32.c @@ -54,6 +54,14 @@ static const unsigned int crc_table[256] = { 0x2d02ef8dL }; +unsigned int crc32_update(unsigned int crc, const unsigned char *buffer, unsigned int len) +{ + for (unsigned int i = 0; i < len; i++) { + crc = crc_table[(crc ^ buffer[i]) & 0xff] ^ (crc >> 8); + } + return crc; +} + unsigned int crc32(const unsigned char *buffer, unsigned int len) { unsigned int crc = 0xffffffffL; diff --git a/tools/mkfont/crc64.c b/tools/common/crc64.c similarity index 100% rename from tools/mkfont/crc64.c rename to tools/common/crc64.c diff --git a/tools/audioconv64/dr_mp3.h b/tools/common/dr_mp3.h similarity index 100% rename from tools/audioconv64/dr_mp3.h rename to tools/common/dr_mp3.h diff --git a/tools/audioconv64/dr_wav.h b/tools/common/dr_wav.h similarity index 93% rename from tools/audioconv64/dr_wav.h rename to tools/common/dr_wav.h index 3e8618e37..a55d4095f 100644 --- a/tools/audioconv64/dr_wav.h +++ b/tools/common/dr_wav.h @@ -1,6 +1,6 @@ /* WAV audio loader and writer. Choice of public domain or MIT-0. See license statements at the end of this file. -dr_wav - v0.13.11 - 07-07-2023 +dr_wav - v0.14.3 - 2025-12-14 David Reid - mackron@gmail.com @@ -146,8 +146,8 @@ extern "C" { #define DRWAV_XSTRINGIFY(x) DRWAV_STRINGIFY(x) #define DRWAV_VERSION_MAJOR 0 -#define DRWAV_VERSION_MINOR 13 -#define DRWAV_VERSION_REVISION 11 +#define DRWAV_VERSION_MINOR 14 +#define DRWAV_VERSION_REVISION 3 #define DRWAV_VERSION_STRING DRWAV_XSTRINGIFY(DRWAV_VERSION_MAJOR) "." DRWAV_XSTRINGIFY(DRWAV_VERSION_MINOR) "." DRWAV_XSTRINGIFY(DRWAV_VERSION_REVISION) #include /* For size_t. */ @@ -176,7 +176,7 @@ typedef unsigned int drwav_uint32; #pragma GCC diagnostic pop #endif #endif -#if defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) && !defined(__ILP32__)) || defined(_M_X64) || defined(__ia64) || defined (_M_IA64) || defined(__aarch64__) || defined(_M_ARM64) || defined(__powerpc64__) +#if defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) && !defined(__ILP32__)) || defined(_M_X64) || defined(__ia64) || defined (_M_IA64) || defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC) || defined(__powerpc64__) typedef drwav_uint64 drwav_uintptr; #else typedef drwav_uint32 drwav_uintptr; @@ -305,8 +305,9 @@ typedef struct typedef enum { - drwav_seek_origin_start, - drwav_seek_origin_current + DRWAV_SEEK_SET, + DRWAV_SEEK_CUR, + DRWAV_SEEK_END } drwav_seek_origin; typedef enum @@ -415,11 +416,21 @@ origin [in] The origin of the seek - the current position or the start of the Returns whether or not the seek was successful. -Whether or not it is relative to the beginning or current position is determined by the "origin" parameter which will be either drwav_seek_origin_start or -drwav_seek_origin_current. +Whether or not it is relative to the beginning or current position is determined by the "origin" parameter which will be either DRWAV_SEEK_SET or +DRWAV_SEEK_CUR. */ typedef drwav_bool32 (* drwav_seek_proc)(void* pUserData, int offset, drwav_seek_origin origin); +/* +Callback for when the current position in the stream needs to be retrieved. + +pUserData [in] The user data that was passed to drwav_init() and family. +pCursor [out] A pointer to a variable to receive the current position in the stream. + +Returns whether or not the operation was successful. +*/ +typedef drwav_bool32 (* drwav_tell_proc)(void* pUserData, drwav_int64* pCursor); + /* Callback for when drwav_init_ex() finds a chunk. @@ -514,6 +525,11 @@ typedef enum drwav_metadata_type_list_info_genre = 1 << 15, drwav_metadata_type_list_info_album = 1 << 16, drwav_metadata_type_list_info_tracknumber = 1 << 17, + drwav_metadata_type_list_info_location = 1 << 18, + drwav_metadata_type_list_info_organization = 1 << 19, + drwav_metadata_type_list_info_keywords = 1 << 20, + drwav_metadata_type_list_info_medium = 1 << 21, + drwav_metadata_type_list_info_description = 1 << 22, /* Other type constants for convenience. */ drwav_metadata_type_list_all_info_strings = drwav_metadata_type_list_info_software @@ -524,7 +540,12 @@ typedef enum | drwav_metadata_type_list_info_date | drwav_metadata_type_list_info_genre | drwav_metadata_type_list_info_album - | drwav_metadata_type_list_info_tracknumber, + | drwav_metadata_type_list_info_tracknumber + | drwav_metadata_type_list_info_location + | drwav_metadata_type_list_info_organization + | drwav_metadata_type_list_info_keywords + | drwav_metadata_type_list_info_medium + | drwav_metadata_type_list_info_description, drwav_metadata_type_list_all_adtl = drwav_metadata_type_list_label | drwav_metadata_type_list_note @@ -555,11 +576,11 @@ typedef struct /* See drwav_smpl_loop_type. */ drwav_uint32 type; - /* The byte offset of the first sample to be played in the loop. */ - drwav_uint32 firstSampleByteOffset; + /* The offset of the first sample to be played in the loop. */ + drwav_uint32 firstSampleOffset; - /* The byte offset into the audio data of the last sample to be played in the loop. */ - drwav_uint32 lastSampleByteOffset; + /* The offset into the audio data of the last sample to be played in the loop. */ + drwav_uint32 lastSampleOffset; /* A value to represent that playback should occur at a point between samples. This value ranges from 0 to UINT32_MAX. Where a value of 0 means no fraction, and a value of (UINT32_MAX / 2) would mean half a sample. */ drwav_uint32 sampleFraction; @@ -637,8 +658,8 @@ typedef struct /* Set to 0 for uncompressed formats. Else the last byte in compressed wave data where decompression can begin to find the value of the corresponding sample value. */ drwav_uint32 blockStart; - /* For uncompressed formats this is the byte offset of the cue point into the audio data. For compressed formats this is relative to the block specified with blockStart. */ - drwav_uint32 sampleByteOffset; + /* For uncompressed formats this is the offset of the cue point into the audio data. For compressed formats this is relative to the block specified with blockStart. */ + drwav_uint32 sampleOffset; } drwav_cue_point; typedef struct @@ -846,6 +867,9 @@ typedef struct /* A pointer to the function to call when the wav file needs to be seeked. */ drwav_seek_proc onSeek; + /* A pointer to the function to call when the position of the stream needs to be retrieved. */ + drwav_tell_proc onTell; + /* The user data to pass to callbacks. */ void* pUserData; @@ -968,9 +992,9 @@ after the function returns. See also: drwav_init_file(), drwav_init_memory(), drwav_uninit() */ -DRWAV_API drwav_bool32 drwav_init(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, void* pUserData, const drwav_allocation_callbacks* pAllocationCallbacks); -DRWAV_API drwav_bool32 drwav_init_ex(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_chunk_proc onChunk, void* pReadSeekUserData, void* pChunkUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks); -DRWAV_API drwav_bool32 drwav_init_with_metadata(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, void* pUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_ex(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, drwav_chunk_proc onChunk, void* pReadSeekTellUserData, void* pChunkUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_with_metadata(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks); /* Initializes a pre-allocated drwav object for writing. @@ -1273,9 +1297,9 @@ Opens and reads an entire wav file in a single operation. The return value is a heap-allocated buffer containing the audio data. Use drwav_free() to free the buffer. */ -DRWAV_API drwav_int16* drwav_open_and_read_pcm_frames_s16(drwav_read_proc onRead, drwav_seek_proc onSeek, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); -DRWAV_API float* drwav_open_and_read_pcm_frames_f32(drwav_read_proc onRead, drwav_seek_proc onSeek, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); -DRWAV_API drwav_int32* drwav_open_and_read_pcm_frames_s32(drwav_read_proc onRead, drwav_seek_proc onSeek, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_int16* drwav_open_and_read_pcm_frames_s16(drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API float* drwav_open_and_read_pcm_frames_f32(drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_int32* drwav_open_and_read_pcm_frames_s32(drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); #ifndef DR_WAV_NO_STDIO /* Opens and decodes an entire wav file in a single operation. @@ -1384,7 +1408,7 @@ DRWAV_API drwav_bool32 drwav_fourcc_equal(const drwav_uint8* a, const char* b); #define DRWAV_MAX_SIMD_VECTOR_SIZE 32 /* Architecture Detection */ -#if defined(__x86_64__) || defined(_M_X64) +#if defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC)) #define DRWAV_X64 #elif defined(__i386) || defined(_M_IX86) #define DRWAV_X86 @@ -1435,8 +1459,8 @@ DRWAV_API drwav_bool32 drwav_fourcc_equal(const drwav_uint8* a, const char* b); /* End SIZE_MAX */ /* Weird bit manipulation is for C89 compatibility (no direct support for 64-bit integers). */ -#define DRWAV_INT64_MIN ((drwav_int64)0x80000000 << 32) -#define DRWAV_INT64_MAX ((((drwav_int64)0x7FFFFFFF) << 32) | 0xFFFFFFFF) +#define DRWAV_INT64_MIN ((drwav_int64) ((drwav_uint64)0x80000000 << 32)) +#define DRWAV_INT64_MAX ((drwav_int64)(((drwav_uint64)0x7FFFFFFF << 32) | 0xFFFFFFFF)) #if defined(_MSC_VER) && _MSC_VER >= 1400 #define DRWAV_HAS_BYTESWAP16_INTRINSIC @@ -1787,7 +1811,7 @@ DRWAV_PRIVATE drwav_int64 drwav_aiff_extented_to_s64(const drwav_uint8* data) exponent -= 16383; if (exponent > 63) { - return sign ? DRWAV_INT64_MIN : DRWAV_INT64_MAX; /* Too bit for a 64-bit integer. */ + return sign ? DRWAV_INT64_MIN : DRWAV_INT64_MAX; /* Too big for a 64-bit integer. */ } else if (exponent < 1) { return 0; /* Number is less than 1, so rounds down to 0. */ } @@ -1962,12 +1986,12 @@ DRWAV_PRIVATE drwav_bool32 drwav__seek_forward(drwav_seek_proc onSeek, drwav_uin drwav_uint64 bytesRemainingToSeek = offset; while (bytesRemainingToSeek > 0) { if (bytesRemainingToSeek > 0x7FFFFFFF) { - if (!onSeek(pUserData, 0x7FFFFFFF, drwav_seek_origin_current)) { + if (!onSeek(pUserData, 0x7FFFFFFF, DRWAV_SEEK_CUR)) { return DRWAV_FALSE; } bytesRemainingToSeek -= 0x7FFFFFFF; } else { - if (!onSeek(pUserData, (int)bytesRemainingToSeek, drwav_seek_origin_current)) { + if (!onSeek(pUserData, (int)bytesRemainingToSeek, DRWAV_SEEK_CUR)) { return DRWAV_FALSE; } bytesRemainingToSeek = 0; @@ -1980,21 +2004,21 @@ DRWAV_PRIVATE drwav_bool32 drwav__seek_forward(drwav_seek_proc onSeek, drwav_uin DRWAV_PRIVATE drwav_bool32 drwav__seek_from_start(drwav_seek_proc onSeek, drwav_uint64 offset, void* pUserData) { if (offset <= 0x7FFFFFFF) { - return onSeek(pUserData, (int)offset, drwav_seek_origin_start); + return onSeek(pUserData, (int)offset, DRWAV_SEEK_SET); } /* Larger than 32-bit seek. */ - if (!onSeek(pUserData, 0x7FFFFFFF, drwav_seek_origin_start)) { + if (!onSeek(pUserData, 0x7FFFFFFF, DRWAV_SEEK_SET)) { return DRWAV_FALSE; } offset -= 0x7FFFFFFF; for (;;) { if (offset <= 0x7FFFFFFF) { - return onSeek(pUserData, (int)offset, drwav_seek_origin_current); + return onSeek(pUserData, (int)offset, DRWAV_SEEK_CUR); } - if (!onSeek(pUserData, 0x7FFFFFFF, drwav_seek_origin_current)) { + if (!onSeek(pUserData, 0x7FFFFFFF, DRWAV_SEEK_CUR)) { return DRWAV_FALSE; } offset -= 0x7FFFFFFF; @@ -2028,7 +2052,7 @@ DRWAV_PRIVATE drwav_bool32 drwav__on_seek(drwav_seek_proc onSeek, void* pUserDat return DRWAV_FALSE; } - if (origin == drwav_seek_origin_start) { + if (origin == DRWAV_SEEK_SET) { *pCursor = offset; } else { *pCursor += offset; @@ -2096,7 +2120,7 @@ DRWAV_PRIVATE drwav_uint8* drwav__metadata_get_memory(drwav__metadata_parser* pP pParser->pDataCursor += align - modulo; } } - + pResult = pParser->pDataCursor; /* @@ -2189,12 +2213,12 @@ DRWAV_PRIVATE drwav_uint64 drwav__read_smpl_to_metadata_obj(drwav__metadata_pars bytesJustRead = drwav__metadata_parser_read(pParser, smplLoopData, sizeof(smplLoopData), &totalBytesRead); if (bytesJustRead == sizeof(smplLoopData)) { - pMetadata->data.smpl.pLoops[iSampleLoop].cuePointId = drwav_bytes_to_u32(smplLoopData + 0); - pMetadata->data.smpl.pLoops[iSampleLoop].type = drwav_bytes_to_u32(smplLoopData + 4); - pMetadata->data.smpl.pLoops[iSampleLoop].firstSampleByteOffset = drwav_bytes_to_u32(smplLoopData + 8); - pMetadata->data.smpl.pLoops[iSampleLoop].lastSampleByteOffset = drwav_bytes_to_u32(smplLoopData + 12); - pMetadata->data.smpl.pLoops[iSampleLoop].sampleFraction = drwav_bytes_to_u32(smplLoopData + 16); - pMetadata->data.smpl.pLoops[iSampleLoop].playCount = drwav_bytes_to_u32(smplLoopData + 20); + pMetadata->data.smpl.pLoops[iSampleLoop].cuePointId = drwav_bytes_to_u32(smplLoopData + 0); + pMetadata->data.smpl.pLoops[iSampleLoop].type = drwav_bytes_to_u32(smplLoopData + 4); + pMetadata->data.smpl.pLoops[iSampleLoop].firstSampleOffset = drwav_bytes_to_u32(smplLoopData + 8); + pMetadata->data.smpl.pLoops[iSampleLoop].lastSampleOffset = drwav_bytes_to_u32(smplLoopData + 12); + pMetadata->data.smpl.pLoops[iSampleLoop].sampleFraction = drwav_bytes_to_u32(smplLoopData + 16); + pMetadata->data.smpl.pLoops[iSampleLoop].playCount = drwav_bytes_to_u32(smplLoopData + 20); } else { break; } @@ -2254,7 +2278,7 @@ DRWAV_PRIVATE drwav_uint64 drwav__read_cue_to_metadata_obj(drwav__metadata_parse pMetadata->data.cue.pCuePoints[iCuePoint].dataChunkId[3] = cuePointData[11]; pMetadata->data.cue.pCuePoints[iCuePoint].chunkStart = drwav_bytes_to_u32(cuePointData + 12); pMetadata->data.cue.pCuePoints[iCuePoint].blockStart = drwav_bytes_to_u32(cuePointData + 16); - pMetadata->data.cue.pCuePoints[iCuePoint].sampleByteOffset = drwav_bytes_to_u32(cuePointData + 20); + pMetadata->data.cue.pCuePoints[iCuePoint].sampleOffset = drwav_bytes_to_u32(cuePointData + 20); } else { break; } @@ -2407,7 +2431,7 @@ DRWAV_PRIVATE drwav_result drwav_buffer_reader_read(drwav_buffer_reader* pReader size_t bytesRemaining; DRWAV_ASSERT(pReader != NULL); - + if (pBytesRead != NULL) { *pBytesRead = 0; } @@ -2487,7 +2511,7 @@ DRWAV_PRIVATE drwav_uint64 drwav__read_bext_to_metadata_obj(drwav__metadata_pars size_t bytesRead = drwav__metadata_parser_read(pParser, bextData, sizeof(bextData), NULL); DRWAV_ASSERT(pParser->stage == drwav__metadata_parser_stage_read); - + if (bytesRead == sizeof(bextData)) { drwav_buffer_reader reader; drwav_uint32 timeReferenceLow; @@ -2549,7 +2573,7 @@ DRWAV_PRIVATE drwav_uint64 drwav__read_list_label_or_note_to_metadata_obj(drwav_ drwav_uint64 totalBytesRead = 0; size_t bytesJustRead = drwav__metadata_parser_read(pParser, cueIDBuffer, sizeof(cueIDBuffer), &totalBytesRead); - DRWAV_ASSERT(pParser->stage == drwav__metadata_parser_stage_read); + DRWAV_ASSERT(pParser->stage == drwav__metadata_parser_stage_read); if (bytesJustRead == sizeof(cueIDBuffer)) { drwav_uint32 sizeIncludingNullTerminator; @@ -2698,7 +2722,7 @@ DRWAV_PRIVATE drwav_uint64 drwav__metadata_process_chunk(drwav__metadata_parser* drwav_uint8 buffer[4]; size_t bytesJustRead; - if (!pParser->onSeek(pParser->pReadSeekUserData, 28, drwav_seek_origin_current)) { + if (!pParser->onSeek(pParser->pReadSeekUserData, 28, DRWAV_SEEK_CUR)) { return bytesRead; } bytesRead += 28; @@ -2721,7 +2745,7 @@ DRWAV_PRIVATE drwav_uint64 drwav__metadata_process_chunk(drwav__metadata_parser* } } else { /* Loop count in header does not match the size of the chunk. */ - } + } } } else { bytesRead = drwav__read_smpl_to_metadata_obj(pParser, pChunkHeader, &pParser->pMetadata[pParser->metadataCursor]); @@ -2811,7 +2835,7 @@ DRWAV_PRIVATE drwav_uint64 drwav__metadata_process_chunk(drwav__metadata_parser* return bytesRead; } allocSizeNeeded += drwav__strlen(buffer) + 1; - allocSizeNeeded += (size_t)pChunkHeader->sizeInBytes - DRWAV_BEXT_BYTES; /* Coding history. */ + allocSizeNeeded += (size_t)pChunkHeader->sizeInBytes - DRWAV_BEXT_BYTES + 1; /* Coding history. */ drwav__metadata_request_extra_memory_for_stage_2(pParser, allocSizeNeeded, 1); @@ -2916,6 +2940,16 @@ DRWAV_PRIVATE drwav_uint64 drwav__metadata_process_chunk(drwav__metadata_parser* subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_album); } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_tracknumber, "ITRK")) { subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_tracknumber); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_location, "IARL")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_location); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_organization, "ICMS")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_organization); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_keywords, "IKEY")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_keywords); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_medium, "IMED")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_medium); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_description, "ISBJ")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_description); } else if ((allowedMetadataTypes & drwav_metadata_type_unknown) != 0) { subchunkBytesRead = drwav__metadata_process_unknown_chunk(pParser, subchunkId, subchunkDataSize, listType); } @@ -2926,14 +2960,14 @@ DRWAV_PRIVATE drwav_uint64 drwav__metadata_process_chunk(drwav__metadata_parser* if (subchunkBytesRead < subchunkDataSize) { drwav_uint64 bytesToSeek = subchunkDataSize - subchunkBytesRead; - if (!pParser->onSeek(pParser->pReadSeekUserData, (int)bytesToSeek, drwav_seek_origin_current)) { + if (!pParser->onSeek(pParser->pReadSeekUserData, (int)bytesToSeek, DRWAV_SEEK_CUR)) { break; } bytesRead += bytesToSeek; } if ((subchunkDataSize % 2) == 1) { - if (!pParser->onSeek(pParser->pReadSeekUserData, 1, drwav_seek_origin_current)) { + if (!pParser->onSeek(pParser->pReadSeekUserData, 1, DRWAV_SEEK_CUR)) { break; } bytesRead += 1; @@ -2985,16 +3019,17 @@ DRWAV_API drwav_uint16 drwav_fmt_get_format(const drwav_fmt* pFMT) } } -DRWAV_PRIVATE drwav_bool32 drwav_preinit(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, void* pReadSeekUserData, const drwav_allocation_callbacks* pAllocationCallbacks) +DRWAV_PRIVATE drwav_bool32 drwav_preinit(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pReadSeekTellUserData, const drwav_allocation_callbacks* pAllocationCallbacks) { - if (pWav == NULL || onRead == NULL || onSeek == NULL) { + if (pWav == NULL || onRead == NULL || onSeek == NULL) { /* <-- onTell is optional. */ return DRWAV_FALSE; } DRWAV_ZERO_MEMORY(pWav, sizeof(*pWav)); pWav->onRead = onRead; pWav->onSeek = onSeek; - pWav->pUserData = pReadSeekUserData; + pWav->onTell = onTell; + pWav->pUserData = pReadSeekTellUserData; pWav->allocationCallbacks = drwav_copy_allocation_callbacks_or_defaults(pAllocationCallbacks); if (pWav->allocationCallbacks.onFree == NULL || (pWav->allocationCallbacks.onMalloc == NULL && pWav->allocationCallbacks.onRealloc == NULL)) { @@ -3075,7 +3110,13 @@ DRWAV_PRIVATE drwav_bool32 drwav_init__internal(drwav* pWav, drwav_chunk_proc on if (pWav->container == drwav_container_riff || pWav->container == drwav_container_rifx) { if (drwav_bytes_to_u32_ex(chunkSizeBytes, pWav->container) < 36) { - return DRWAV_FALSE; /* Chunk size should always be at least 36 bytes. */ + /* + I've had a report of a WAV file failing to load when the size of the WAVE chunk is not encoded + and is instead just set to 0. I'm going to relax the validation here to allow these files to + load. Considering the chunk size isn't actually used this should be safe. With this change my + test suite still passes. + */ + /*return DRWAV_FALSE;*/ /* Chunk size should always be at least 36 bytes. */ } } else if (pWav->container == drwav_container_rf64) { if (drwav_bytes_to_u32_le(chunkSizeBytes) != 0xFFFFFFFF) { @@ -3305,7 +3346,7 @@ DRWAV_PRIVATE drwav_bool32 drwav_init__internal(drwav* pWav, drwav_chunk_proc on fmt.channelMask = drwav_bytes_to_u32_ex(fmtext + 2, pWav->container); drwav_bytes_to_guid(fmtext + 6, fmt.subFormat); } else { - if (pWav->onSeek(pWav->pUserData, fmt.extendedSize, drwav_seek_origin_current) == DRWAV_FALSE) { + if (pWav->onSeek(pWav->pUserData, fmt.extendedSize, DRWAV_SEEK_CUR) == DRWAV_FALSE) { return DRWAV_FALSE; } } @@ -3315,7 +3356,7 @@ DRWAV_PRIVATE drwav_bool32 drwav_init__internal(drwav* pWav, drwav_chunk_proc on } /* Seek past any leftover bytes. For w64 the leftover will be defined based on the chunk size. */ - if (pWav->onSeek(pWav->pUserData, (int)(header.sizeInBytes - bytesReadSoFar), drwav_seek_origin_current) == DRWAV_FALSE) { + if (pWav->onSeek(pWav->pUserData, (int)(header.sizeInBytes - bytesReadSoFar), DRWAV_SEEK_CUR) == DRWAV_FALSE) { return DRWAV_FALSE; } cursor += (header.sizeInBytes - bytesReadSoFar); @@ -3336,7 +3377,7 @@ DRWAV_PRIVATE drwav_bool32 drwav_init__internal(drwav* pWav, drwav_chunk_proc on if (((pWav->container == drwav_container_riff || pWav->container == drwav_container_rifx || pWav->container == drwav_container_rf64) && drwav_fourcc_equal(header.id.fourcc, "data")) || ((pWav->container == drwav_container_w64) && drwav_guid_equal(header.id.guid, drwavGUID_W64_DATA))) { foundChunk_data = DRWAV_TRUE; - + pWav->dataChunkDataPos = cursor; if (pWav->container != drwav_container_rf64) { /* The data chunk size for RF64 will always be set to 0xFFFFFFFF here. It was set to it's true value earlier. */ @@ -3426,7 +3467,7 @@ DRWAV_PRIVATE drwav_bool32 drwav_init__internal(drwav* pWav, drwav_chunk_proc on return DRWAV_FALSE; } - + channels = drwav_bytes_to_u16_ex (commData + 0, pWav->container); frameCount = drwav_bytes_to_u32_ex (commData + 2, pWav->container); sampleSizeInBits = drwav_bytes_to_u16_ex (commData + 6, pWav->container); @@ -3459,12 +3500,15 @@ DRWAV_PRIVATE drwav_bool32 drwav_init__internal(drwav* pWav, drwav_chunk_proc on compressionFormat = DR_WAVE_FORMAT_MULAW; } else if (drwav_fourcc_equal(type, "ima4")) { compressionFormat = DR_WAVE_FORMAT_DVI_ADPCM; - sampleSizeInBits = 4; + sampleSizeInBits = 4; /* I haven't been able to figure out how to get correct decoding for IMA ADPCM. Until this is figured out we'll need to abort when we encounter such an encoding. Advice welcome! */ + (void)compressionFormat; + (void)sampleSizeInBits; + return DRWAV_FALSE; } else { return DRWAV_FALSE; /* Unknown or unsupported compression format. Need to abort. */ @@ -3501,7 +3545,7 @@ DRWAV_PRIVATE drwav_bool32 drwav_init__internal(drwav* pWav, drwav_chunk_proc on /* In AIFF, samples are padded to 8 byte boundaries. We need to round up our bits per sample here. */ fmt.bitsPerSample += (fmt.bitsPerSample & 7); - + /* If the form type is AIFC there will be some additional data in the chunk. We need to seek past it. */ if (isAIFCFormType) { @@ -3527,20 +3571,46 @@ DRWAV_PRIVATE drwav_bool32 drwav_init__internal(drwav* pWav, drwav_chunk_proc on return DRWAV_FALSE; } - /* We need to seek forward by the offset. */ + /* The position of the audio data starts at an offset. */ offset = drwav_bytes_to_u32_ex(offsetAndBlockSizeData + 0, pWav->container); - if (drwav__seek_forward(pWav->onSeek, offset, pWav->pUserData) == DRWAV_FALSE) { - return DRWAV_FALSE; - } - cursor += offset; + pWav->dataChunkDataPos = cursor + offset; - pWav->dataChunkDataPos = cursor; + /* The data chunk size needs to be reduced by the offset or else seeking will break. */ dataChunkSize = chunkSize; + if (dataChunkSize > offset) { + dataChunkSize -= offset; + } else { + dataChunkSize = 0; + } - /* If we're running in sequential mode, or we're not reading metadata, we have enough now that we can get out of the loop. */ - if (sequential || !isProcessingMetadata) { - break; /* No need to keep reading beyond the data chunk. */ + if (sequential) { + if (foundChunk_fmt) { /* <-- Name is misleading, but will be set to true if the COMM chunk has been parsed. */ + /* + Getting here means we're opening in sequential mode and we've found the SSND (data) and COMM (fmt) chunks. We need + to get out of the loop here or else we'll end up going past the data chunk and will have no way of getting back to + it since we're not allowed to seek backwards. + + One subtle detail here is that there is an offset with the SSND chunk. We need to make sure we seek past this offset + so we're left sitting on the first byte of actual audio data. + */ + if (drwav__seek_forward(pWav->onSeek, offset, pWav->pUserData) == DRWAV_FALSE) { + return DRWAV_FALSE; + } + cursor += offset; + + break; + } else { + /* + Getting here means the COMM chunk was not found. In sequential mode, if we haven't yet found the COMM chunk + we'll need to abort because we can't be doing a backwards seek back to the SSND chunk in order to read the + data. For this reason, this configuration of AIFF files are not supported with sequential mode. + */ + return DRWAV_FALSE; + } } else { + chunkSize += header.paddingSize; /* <-- Make sure we seek past the padding. */ + chunkSize -= sizeof(offsetAndBlockSizeData); /* <-- This was read earlier. */ + if (drwav__seek_forward(pWav->onSeek, chunkSize, pWav->pUserData) == DRWAV_FALSE) { break; } @@ -3551,13 +3621,9 @@ DRWAV_PRIVATE drwav_bool32 drwav_init__internal(drwav* pWav, drwav_chunk_proc on } - /* Getting here means it's not a chunk that we care about internally, but might need to be handled as metadata by the caller. */ if (isProcessingMetadata) { - drwav_uint64 metadataBytesRead; - - metadataBytesRead = drwav__metadata_process_chunk(&metadataParser, &header, drwav_metadata_type_all_including_unknown); - DRWAV_ASSERT(metadataBytesRead <= header.sizeInBytes); + drwav__metadata_process_chunk(&metadataParser, &header, drwav_metadata_type_all_including_unknown); /* Go back to the start of the chunk so we can normalize the position of the cursor. */ if (drwav__seek_from_start(pWav->onSeek, cursor, pWav->pUserData) == DRWAV_FALSE) { @@ -3644,8 +3710,26 @@ DRWAV_PRIVATE drwav_bool32 drwav_init__internal(drwav* pWav, drwav_chunk_proc on pWav->metadataCount = metadataParser.metadataCount; } - - /* At this point we should be sitting on the first byte of the raw audio data. */ + /* + It's possible for the size reported in the data chunk to be greater than that of the file. We + need to do a validation check here to make sure we don't exceed the file size. To skip this + check, set the onTell callback to NULL. + */ + if (pWav->onTell != NULL && pWav->onSeek != NULL) { + if (pWav->onSeek(pWav->pUserData, 0, DRWAV_SEEK_END) == DRWAV_TRUE) { + drwav_int64 fileSize; + if (pWav->onTell(pWav->pUserData, &fileSize)) { + if (dataChunkSize + pWav->dataChunkDataPos > (drwav_uint64)fileSize) { + dataChunkSize = (drwav_uint64)fileSize - pWav->dataChunkDataPos; + } + } + } else { + /* + Failed to seek to the end of the file. It might not be supported by the backend so in + this case we cannot perform the validation check. + */ + } + } /* I've seen a WAV file in the wild where a RIFF-ecapsulated file has the size of it's "RIFF" and @@ -3667,6 +3751,7 @@ DRWAV_PRIVATE drwav_bool32 drwav_init__internal(drwav* pWav, drwav_chunk_proc on } } + /* At this point we want to be sitting on the first byte of the raw audio data. */ if (drwav__seek_from_start(pWav->onSeek, pWav->dataChunkDataPos, pWav->pUserData) == DRWAV_FALSE) { drwav_free(pWav->pMetadata, &pWav->allocationCallbacks); return DRWAV_FALSE; @@ -3677,8 +3762,26 @@ DRWAV_PRIVATE drwav_bool32 drwav_init__internal(drwav* pWav, drwav_chunk_proc on pWav->sampleRate = fmt.sampleRate; pWav->channels = fmt.channels; pWav->bitsPerSample = fmt.bitsPerSample; - pWav->bytesRemaining = dataChunkSize; pWav->translatedFormatTag = translatedFormatTag; + + /* + I've had a report where files would start glitching after seeking. The reason for this is the data + chunk is not a clean multiple of the PCM frame size in bytes. Where this becomes a problem is when + seeking, because the number of bytes remaining in the data chunk is used to calculate the current + byte position. If this byte position is not aligned to the number of bytes in a PCM frame, it will + result in the seek not being cleanly positioned at the start of the PCM frame thereby resulting in + all decoded frames after that being corrupted. + + To address this, we need to round the data chunk size down to the nearest multiple of the frame size. + */ + if (!drwav__is_compressed_format_tag(translatedFormatTag)) { + drwav_uint32 bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame > 0) { + dataChunkSize -= (dataChunkSize % bytesPerFrame); + } + } + + pWav->bytesRemaining = dataChunkSize; pWav->dataChunkDataSize = dataChunkSize; if (sampleCountFromFactChunk != 0) { @@ -3761,23 +3864,23 @@ DRWAV_PRIVATE drwav_bool32 drwav_init__internal(drwav* pWav, drwav_chunk_proc on return DRWAV_TRUE; } -DRWAV_API drwav_bool32 drwav_init(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, void* pUserData, const drwav_allocation_callbacks* pAllocationCallbacks) +DRWAV_API drwav_bool32 drwav_init(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, const drwav_allocation_callbacks* pAllocationCallbacks) { - return drwav_init_ex(pWav, onRead, onSeek, NULL, pUserData, NULL, 0, pAllocationCallbacks); + return drwav_init_ex(pWav, onRead, onSeek, onTell, NULL, pUserData, NULL, 0, pAllocationCallbacks); } -DRWAV_API drwav_bool32 drwav_init_ex(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_chunk_proc onChunk, void* pReadSeekUserData, void* pChunkUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks) +DRWAV_API drwav_bool32 drwav_init_ex(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, drwav_chunk_proc onChunk, void* pReadSeekTellUserData, void* pChunkUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks) { - if (!drwav_preinit(pWav, onRead, onSeek, pReadSeekUserData, pAllocationCallbacks)) { + if (!drwav_preinit(pWav, onRead, onSeek, onTell, pReadSeekTellUserData, pAllocationCallbacks)) { return DRWAV_FALSE; } return drwav_init__internal(pWav, onChunk, pChunkUserData, flags); } -DRWAV_API drwav_bool32 drwav_init_with_metadata(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, void* pUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks) +DRWAV_API drwav_bool32 drwav_init_with_metadata(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks) { - if (!drwav_preinit(pWav, onRead, onSeek, pUserData, pAllocationCallbacks)) { + if (!drwav_preinit(pWav, onRead, onSeek, onTell, pUserData, pAllocationCallbacks)) { return DRWAV_FALSE; } @@ -3992,8 +4095,8 @@ DRWAV_PRIVATE size_t drwav__write_or_count_metadata(drwav* pWav, drwav_metadata* for (iLoop = 0; iLoop < pMetadata->data.smpl.sampleLoopCount; ++iLoop) { bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.pLoops[iLoop].cuePointId); bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.pLoops[iLoop].type); - bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.pLoops[iLoop].firstSampleByteOffset); - bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.pLoops[iLoop].lastSampleByteOffset); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.pLoops[iLoop].firstSampleOffset); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.pLoops[iLoop].lastSampleOffset); bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.pLoops[iLoop].sampleFraction); bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.pLoops[iLoop].playCount); } @@ -4033,7 +4136,7 @@ DRWAV_PRIVATE size_t drwav__write_or_count_metadata(drwav* pWav, drwav_metadata* bytesWritten += drwav__write_or_count(pWav, pMetadata->data.cue.pCuePoints[iCuePoint].dataChunkId, 4); bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.cue.pCuePoints[iCuePoint].chunkStart); bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.cue.pCuePoints[iCuePoint].blockStart); - bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.cue.pCuePoints[iCuePoint].sampleByteOffset); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.cue.pCuePoints[iCuePoint].sampleOffset); } } break; @@ -4139,15 +4242,20 @@ DRWAV_PRIVATE size_t drwav__write_or_count_metadata(drwav* pWav, drwav_metadata* const char* pID = NULL; switch (pMetadata->type) { - case drwav_metadata_type_list_info_software: pID = "ISFT"; break; - case drwav_metadata_type_list_info_copyright: pID = "ICOP"; break; - case drwav_metadata_type_list_info_title: pID = "INAM"; break; - case drwav_metadata_type_list_info_artist: pID = "IART"; break; - case drwav_metadata_type_list_info_comment: pID = "ICMT"; break; - case drwav_metadata_type_list_info_date: pID = "ICRD"; break; - case drwav_metadata_type_list_info_genre: pID = "IGNR"; break; - case drwav_metadata_type_list_info_album: pID = "IPRD"; break; - case drwav_metadata_type_list_info_tracknumber: pID = "ITRK"; break; + case drwav_metadata_type_list_info_software: pID = "ISFT"; break; + case drwav_metadata_type_list_info_copyright: pID = "ICOP"; break; + case drwav_metadata_type_list_info_title: pID = "INAM"; break; + case drwav_metadata_type_list_info_artist: pID = "IART"; break; + case drwav_metadata_type_list_info_comment: pID = "ICMT"; break; + case drwav_metadata_type_list_info_date: pID = "ICRD"; break; + case drwav_metadata_type_list_info_genre: pID = "IGNR"; break; + case drwav_metadata_type_list_info_album: pID = "IPRD"; break; + case drwav_metadata_type_list_info_tracknumber: pID = "ITRK"; break; + case drwav_metadata_type_list_info_location: pID = "IARL"; break; + case drwav_metadata_type_list_info_organization: pID = "ICMS"; break; + case drwav_metadata_type_list_info_keywords: pID = "IKEY"; break; + case drwav_metadata_type_list_info_medium: pID = "IMED"; break; + case drwav_metadata_type_list_info_description: pID = "ISBJ"; break; default: break; } @@ -4192,7 +4300,7 @@ DRWAV_PRIVATE size_t drwav__write_or_count_metadata(drwav* pWav, drwav_metadata* if (pMetadata->data.labelOrNote.stringLength > 0) { chunkSize += pMetadata->data.labelOrNote.stringLength + 1; - } + } } break; case drwav_metadata_type_list_labelled_cue_region: @@ -4431,7 +4539,7 @@ DRWAV_PRIVATE drwav_bool32 drwav_init_write__internal(drwav* pWav, const drwav_d /* "RIFF" chunk. */ if (pFormat->container == drwav_container_riff) { - drwav_uint32 chunkSizeRIFF = 28 + (drwav_uint32)initialDataChunkSize; /* +28 = "WAVE" + [sizeof "fmt " chunk] */ + drwav_uint32 chunkSizeRIFF = 36 + (drwav_uint32)initialDataChunkSize; /* +36 = "WAVE" + [sizeof "fmt " chunk] + [data chunk header] */ runningPos += drwav__write(pWav, "RIFF", 4); runningPos += drwav__write_u32ne_to_le(pWav, chunkSizeRIFF); runningPos += drwav__write(pWav, "WAVE", 4); @@ -4701,7 +4809,7 @@ DRWAV_PRIVATE drwav_result drwav_result_from_errno(int e) #ifdef ENOSYS case ENOSYS: return DRWAV_NOT_IMPLEMENTED; #endif - #ifdef ENOTEMPTY + #if defined(ENOTEMPTY) && ENOTEMPTY != EEXIST /* In AIX, ENOTEMPTY and EEXIST use the same value. */ case ENOTEMPTY: return DRWAV_DIRECTORY_NOT_EMPTY; #endif #ifdef ELOOP @@ -5158,7 +5266,38 @@ DRWAV_PRIVATE size_t drwav__on_write_stdio(void* pUserData, const void* pData, s DRWAV_PRIVATE drwav_bool32 drwav__on_seek_stdio(void* pUserData, int offset, drwav_seek_origin origin) { - return fseek((FILE*)pUserData, offset, (origin == drwav_seek_origin_current) ? SEEK_CUR : SEEK_SET) == 0; + int whence = SEEK_SET; + if (origin == DRWAV_SEEK_CUR) { + whence = SEEK_CUR; + } else if (origin == DRWAV_SEEK_END) { + whence = SEEK_END; + } + + return fseek((FILE*)pUserData, offset, whence) == 0; +} + +DRWAV_PRIVATE drwav_bool32 drwav__on_tell_stdio(void* pUserData, drwav_int64* pCursor) +{ + FILE* pFileStdio = (FILE*)pUserData; + drwav_int64 result; + + /* These were all validated at a higher level. */ + DRWAV_ASSERT(pFileStdio != NULL); + DRWAV_ASSERT(pCursor != NULL); + +#if defined(_WIN32) && !defined(NXDK) + #if defined(_MSC_VER) && _MSC_VER > 1200 + result = _ftelli64(pFileStdio); + #else + result = ftell(pFileStdio); + #endif +#else + result = ftell(pFileStdio); +#endif + + *pCursor = result; + + return DRWAV_TRUE; } DRWAV_API drwav_bool32 drwav_init_file(drwav* pWav, const char* filename, const drwav_allocation_callbacks* pAllocationCallbacks) @@ -5171,12 +5310,12 @@ DRWAV_PRIVATE drwav_bool32 drwav_init_file__internal_FILE(drwav* pWav, FILE* pFi { drwav_bool32 result; - result = drwav_preinit(pWav, drwav__on_read_stdio, drwav__on_seek_stdio, (void*)pFile, pAllocationCallbacks); + result = drwav_preinit(pWav, drwav__on_read_stdio, drwav__on_seek_stdio, drwav__on_tell_stdio, (void*)pFile, pAllocationCallbacks); if (result != DRWAV_TRUE) { fclose(pFile); return result; } - + result = drwav_init__internal(pWav, onChunk, pChunkUserData, flags); if (result != DRWAV_TRUE) { fclose(pFile); @@ -5349,29 +5488,34 @@ DRWAV_PRIVATE size_t drwav__on_read_memory(void* pUserData, void* pBufferOut, si DRWAV_PRIVATE drwav_bool32 drwav__on_seek_memory(void* pUserData, int offset, drwav_seek_origin origin) { drwav* pWav = (drwav*)pUserData; + drwav_int64 newCursor; + DRWAV_ASSERT(pWav != NULL); - if (origin == drwav_seek_origin_current) { - if (offset > 0) { - if (pWav->memoryStream.currentReadPos + offset > pWav->memoryStream.dataSize) { - return DRWAV_FALSE; /* Trying to seek too far forward. */ - } - } else { - if (pWav->memoryStream.currentReadPos < (size_t)-offset) { - return DRWAV_FALSE; /* Trying to seek too far backwards. */ - } - } + newCursor = pWav->memoryStream.currentReadPos; - /* This will never underflow thanks to the clamps above. */ - pWav->memoryStream.currentReadPos += offset; + if (origin == DRWAV_SEEK_SET) { + newCursor = 0; + } else if (origin == DRWAV_SEEK_CUR) { + newCursor = (drwav_int64)pWav->memoryStream.currentReadPos; + } else if (origin == DRWAV_SEEK_END) { + newCursor = (drwav_int64)pWav->memoryStream.dataSize; } else { - if ((drwav_uint32)offset <= pWav->memoryStream.dataSize) { - pWav->memoryStream.currentReadPos = offset; - } else { - return DRWAV_FALSE; /* Trying to seek too far forward. */ - } + DRWAV_ASSERT(!"Invalid seek origin"); + return DRWAV_FALSE; } + newCursor += offset; + + if (newCursor < 0) { + return DRWAV_FALSE; /* Trying to seek prior to the start of the buffer. */ + } + if ((size_t)newCursor > pWav->memoryStream.dataSize) { + return DRWAV_FALSE; /* Trying to seek beyond the end of the buffer. */ + } + + pWav->memoryStream.currentReadPos = (size_t)newCursor; + return DRWAV_TRUE; } @@ -5418,29 +5562,45 @@ DRWAV_PRIVATE size_t drwav__on_write_memory(void* pUserData, const void* pDataIn DRWAV_PRIVATE drwav_bool32 drwav__on_seek_memory_write(void* pUserData, int offset, drwav_seek_origin origin) { drwav* pWav = (drwav*)pUserData; + drwav_int64 newCursor; + DRWAV_ASSERT(pWav != NULL); - if (origin == drwav_seek_origin_current) { - if (offset > 0) { - if (pWav->memoryStreamWrite.currentWritePos + offset > pWav->memoryStreamWrite.dataSize) { - offset = (int)(pWav->memoryStreamWrite.dataSize - pWav->memoryStreamWrite.currentWritePos); /* Trying to seek too far forward. */ - } - } else { - if (pWav->memoryStreamWrite.currentWritePos < (size_t)-offset) { - offset = -(int)pWav->memoryStreamWrite.currentWritePos; /* Trying to seek too far backwards. */ - } - } + newCursor = pWav->memoryStreamWrite.currentWritePos; - /* This will never underflow thanks to the clamps above. */ - pWav->memoryStreamWrite.currentWritePos += offset; + if (origin == DRWAV_SEEK_SET) { + newCursor = 0; + } else if (origin == DRWAV_SEEK_CUR) { + newCursor = (drwav_int64)pWav->memoryStreamWrite.currentWritePos; + } else if (origin == DRWAV_SEEK_END) { + newCursor = (drwav_int64)pWav->memoryStreamWrite.dataSize; } else { - if ((drwav_uint32)offset <= pWav->memoryStreamWrite.dataSize) { - pWav->memoryStreamWrite.currentWritePos = offset; - } else { - pWav->memoryStreamWrite.currentWritePos = pWav->memoryStreamWrite.dataSize; /* Trying to seek too far forward. */ - } + DRWAV_ASSERT(!"Invalid seek origin"); + return DRWAV_FALSE; + } + + newCursor += offset; + + if (newCursor < 0) { + return DRWAV_FALSE; /* Trying to seek prior to the start of the buffer. */ + } + if ((size_t)newCursor > pWav->memoryStreamWrite.dataSize) { + return DRWAV_FALSE; /* Trying to seek beyond the end of the buffer. */ } + pWav->memoryStreamWrite.currentWritePos = (size_t)newCursor; + + return DRWAV_TRUE; +} + +DRWAV_PRIVATE drwav_bool32 drwav__on_tell_memory(void* pUserData, drwav_int64* pCursor) +{ + drwav* pWav = (drwav*)pUserData; + + DRWAV_ASSERT(pWav != NULL); + DRWAV_ASSERT(pCursor != NULL); + + *pCursor = (drwav_int64)pWav->memoryStream.currentReadPos; return DRWAV_TRUE; } @@ -5455,7 +5615,7 @@ DRWAV_API drwav_bool32 drwav_init_memory_ex(drwav* pWav, const void* data, size_ return DRWAV_FALSE; } - if (!drwav_preinit(pWav, drwav__on_read_memory, drwav__on_seek_memory, pWav, pAllocationCallbacks)) { + if (!drwav_preinit(pWav, drwav__on_read_memory, drwav__on_seek_memory, drwav__on_tell_memory, pWav, pAllocationCallbacks)) { return DRWAV_FALSE; } @@ -5472,7 +5632,7 @@ DRWAV_API drwav_bool32 drwav_init_memory_with_metadata(drwav* pWav, const void* return DRWAV_FALSE; } - if (!drwav_preinit(pWav, drwav__on_read_memory, drwav__on_seek_memory, pWav, pAllocationCallbacks)) { + if (!drwav_preinit(pWav, drwav__on_read_memory, drwav__on_seek_memory, drwav__on_tell_memory, pWav, pAllocationCallbacks)) { return DRWAV_FALSE; } @@ -5562,25 +5722,25 @@ DRWAV_API drwav_result drwav_uninit(drwav* pWav) if (pWav->onSeek && !pWav->isSequentialWrite) { if (pWav->container == drwav_container_riff) { /* The "RIFF" chunk size. */ - if (pWav->onSeek(pWav->pUserData, 4, drwav_seek_origin_start)) { + if (pWav->onSeek(pWav->pUserData, 4, DRWAV_SEEK_SET)) { drwav_uint32 riffChunkSize = drwav__riff_chunk_size_riff(pWav->dataChunkDataSize, pWav->pMetadata, pWav->metadataCount); drwav__write_u32ne_to_le(pWav, riffChunkSize); } /* The "data" chunk size. */ - if (pWav->onSeek(pWav->pUserData, (int)pWav->dataChunkDataPos - 4, drwav_seek_origin_start)) { + if (pWav->onSeek(pWav->pUserData, (int)pWav->dataChunkDataPos - 4, DRWAV_SEEK_SET)) { drwav_uint32 dataChunkSize = drwav__data_chunk_size_riff(pWav->dataChunkDataSize); drwav__write_u32ne_to_le(pWav, dataChunkSize); } } else if (pWav->container == drwav_container_w64) { /* The "RIFF" chunk size. */ - if (pWav->onSeek(pWav->pUserData, 16, drwav_seek_origin_start)) { + if (pWav->onSeek(pWav->pUserData, 16, DRWAV_SEEK_SET)) { drwav_uint64 riffChunkSize = drwav__riff_chunk_size_w64(pWav->dataChunkDataSize); drwav__write_u64ne_to_le(pWav, riffChunkSize); } /* The "data" chunk size. */ - if (pWav->onSeek(pWav->pUserData, (int)pWav->dataChunkDataPos - 8, drwav_seek_origin_start)) { + if (pWav->onSeek(pWav->pUserData, (int)pWav->dataChunkDataPos - 8, DRWAV_SEEK_SET)) { drwav_uint64 dataChunkSize = drwav__data_chunk_size_w64(pWav->dataChunkDataSize); drwav__write_u64ne_to_le(pWav, dataChunkSize); } @@ -5589,13 +5749,13 @@ DRWAV_API drwav_result drwav_uninit(drwav* pWav) int ds64BodyPos = 12 + 8; /* The "RIFF" chunk size. */ - if (pWav->onSeek(pWav->pUserData, ds64BodyPos + 0, drwav_seek_origin_start)) { + if (pWav->onSeek(pWav->pUserData, ds64BodyPos + 0, DRWAV_SEEK_SET)) { drwav_uint64 riffChunkSize = drwav__riff_chunk_size_rf64(pWav->dataChunkDataSize, pWav->pMetadata, pWav->metadataCount); drwav__write_u64ne_to_le(pWav, riffChunkSize); } /* The "data" chunk size. */ - if (pWav->onSeek(pWav->pUserData, ds64BodyPos + 8, drwav_seek_origin_start)) { + if (pWav->onSeek(pWav->pUserData, ds64BodyPos + 8, DRWAV_SEEK_SET)) { drwav_uint64 dataChunkSize = drwav__data_chunk_size_rf64(pWav->dataChunkDataSize); drwav__write_u64ne_to_le(pWav, dataChunkSize); } @@ -5660,7 +5820,7 @@ DRWAV_API size_t drwav_read_raw(drwav* pWav, size_t bytesToRead, void* pBufferOu bytesToSeek = 0x7FFFFFFF; } - if (pWav->onSeek(pWav->pUserData, (int)bytesToSeek, drwav_seek_origin_current) == DRWAV_FALSE) { + if (pWav->onSeek(pWav->pUserData, (int)bytesToSeek, DRWAV_SEEK_CUR) == DRWAV_FALSE) { break; } @@ -5786,10 +5946,12 @@ DRWAV_API drwav_uint64 drwav_read_pcm_frames(drwav* pWav, drwav_uint64 framesToR post_process: { if (pWav->container == drwav_container_aiff && pWav->bitsPerSample == 8 && pWav->aiff.isUnsigned == DRWAV_FALSE) { - drwav_uint64 iSample; + if (pBufferOut != NULL) { + drwav_uint64 iSample; - for (iSample = 0; iSample < framesRead * pWav->channels; iSample += 1) { - ((drwav_uint8*)pBufferOut)[iSample] += 128; + for (iSample = 0; iSample < framesRead * pWav->channels; iSample += 1) { + ((drwav_uint8*)pBufferOut)[iSample] += 128; + } } } } @@ -5805,7 +5967,7 @@ DRWAV_PRIVATE drwav_bool32 drwav_seek_to_first_pcm_frame(drwav* pWav) return DRWAV_FALSE; /* No seeking in write mode. */ } - if (!pWav->onSeek(pWav->pUserData, (int)pWav->dataChunkDataPos, drwav_seek_origin_start)) { + if (!pWav->onSeek(pWav->pUserData, (int)pWav->dataChunkDataPos, DRWAV_SEEK_SET)) { return DRWAV_FALSE; } @@ -5923,7 +6085,7 @@ DRWAV_API drwav_bool32 drwav_seek_to_pcm_frame(drwav* pWav, drwav_uint64 targetF while (offset > 0) { int offset32 = ((offset > INT_MAX) ? INT_MAX : (int)offset); - if (!pWav->onSeek(pWav->pUserData, offset32, drwav_seek_origin_current)) { + if (!pWav->onSeek(pWav->pUserData, offset32, DRWAV_SEEK_CUR)) { return DRWAV_FALSE; } @@ -6096,6 +6258,13 @@ DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__msadpcm(drwav* pWav, drwav { drwav_uint64 totalFramesRead = 0; + static const drwav_int32 adaptationTable[] = { + 230, 230, 230, 230, 307, 409, 512, 614, + 768, 614, 512, 409, 307, 230, 230, 230 + }; + static const drwav_int32 coeff1Table[] = { 256, 512, 0, 192, 240, 460, 392 }; + static const drwav_int32 coeff2Table[] = { 0, -256, 0, 64, 0, -208, -232 }; + DRWAV_ASSERT(pWav != NULL); DRWAV_ASSERT(framesToRead > 0); @@ -6121,6 +6290,11 @@ DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__msadpcm(drwav* pWav, drwav pWav->msadpcm.cachedFrames[2] = pWav->msadpcm.prevFrames[0][0]; pWav->msadpcm.cachedFrames[3] = pWav->msadpcm.prevFrames[0][1]; pWav->msadpcm.cachedFrameCount = 2; + + /* The predictor is used as an index into coeff1Table so we'll need to validate to ensure it never overflows. */ + if (pWav->msadpcm.predictor[0] >= drwav_countof(coeff1Table) || pWav->msadpcm.predictor[0] >= drwav_countof(coeff2Table)) { + return totalFramesRead; /* Invalid file. */ + } } else { /* Stereo. */ drwav_uint8 header[14]; @@ -6143,6 +6317,12 @@ DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__msadpcm(drwav* pWav, drwav pWav->msadpcm.cachedFrames[2] = pWav->msadpcm.prevFrames[0][1]; pWav->msadpcm.cachedFrames[3] = pWav->msadpcm.prevFrames[1][1]; pWav->msadpcm.cachedFrameCount = 2; + + /* The predictor is used as an index into coeff1Table so we'll need to validate to ensure it never overflows. */ + if (pWav->msadpcm.predictor[0] >= drwav_countof(coeff1Table) || pWav->msadpcm.predictor[0] >= drwav_countof(coeff2Table) || + pWav->msadpcm.predictor[1] >= drwav_countof(coeff1Table) || pWav->msadpcm.predictor[1] >= drwav_countof(coeff2Table)) { + return totalFramesRead; /* Invalid file. */ + } } } @@ -6176,13 +6356,6 @@ DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__msadpcm(drwav* pWav, drwav if (pWav->msadpcm.bytesRemainingInBlock == 0) { continue; } else { - static drwav_int32 adaptationTable[] = { - 230, 230, 230, 230, 307, 409, 512, 614, - 768, 614, 512, 409, 307, 230, 230, 230 - }; - static drwav_int32 coeff1Table[] = { 256, 512, 0, 192, 240, 460, 392 }; - static drwav_int32 coeff2Table[] = { 0, -256, 0, 64, 0, -208, -232 }; - drwav_uint8 nibbles; drwav_int32 nibble0; drwav_int32 nibble1; @@ -6201,6 +6374,11 @@ DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__msadpcm(drwav* pWav, drwav drwav_int32 newSample0; drwav_int32 newSample1; + /* The predictor is read from the file and then indexed into a table. Check that it's in bounds. */ + if (pWav->msadpcm.predictor[0] >= drwav_countof(coeff1Table) || pWav->msadpcm.predictor[0] >= drwav_countof(coeff2Table)) { + return totalFramesRead; + } + newSample0 = ((pWav->msadpcm.prevFrames[0][1] * coeff1Table[pWav->msadpcm.predictor[0]]) + (pWav->msadpcm.prevFrames[0][0] * coeff2Table[pWav->msadpcm.predictor[0]])) >> 8; newSample0 += nibble0 * pWav->msadpcm.delta[0]; newSample0 = drwav_clamp(newSample0, -32768, 32767); @@ -6226,7 +6404,6 @@ DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__msadpcm(drwav* pWav, drwav pWav->msadpcm.prevFrames[0][0] = pWav->msadpcm.prevFrames[0][1]; pWav->msadpcm.prevFrames[0][1] = newSample1; - pWav->msadpcm.cachedFrames[2] = newSample0; pWav->msadpcm.cachedFrames[3] = newSample1; pWav->msadpcm.cachedFrameCount = 2; @@ -6236,6 +6413,10 @@ DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__msadpcm(drwav* pWav, drwav drwav_int32 newSample1; /* Left. */ + if (pWav->msadpcm.predictor[0] >= drwav_countof(coeff1Table) || pWav->msadpcm.predictor[0] >= drwav_countof(coeff2Table)) { + return totalFramesRead; /* Out of bounds. Invalid file. */ + } + newSample0 = ((pWav->msadpcm.prevFrames[0][1] * coeff1Table[pWav->msadpcm.predictor[0]]) + (pWav->msadpcm.prevFrames[0][0] * coeff2Table[pWav->msadpcm.predictor[0]])) >> 8; newSample0 += nibble0 * pWav->msadpcm.delta[0]; newSample0 = drwav_clamp(newSample0, -32768, 32767); @@ -6250,6 +6431,10 @@ DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__msadpcm(drwav* pWav, drwav /* Right. */ + if (pWav->msadpcm.predictor[1] >= drwav_countof(coeff1Table) || pWav->msadpcm.predictor[1] >= drwav_countof(coeff2Table)) { + return totalFramesRead; /* Out of bounds. Invalid file. */ + } + newSample1 = ((pWav->msadpcm.prevFrames[1][1] * coeff1Table[pWav->msadpcm.predictor[1]]) + (pWav->msadpcm.prevFrames[1][0] * coeff2Table[pWav->msadpcm.predictor[1]])) >> 8; newSample1 += nibble1 * pWav->msadpcm.delta[1]; newSample1 = drwav_clamp(newSample1, -32768, 32767); @@ -6279,12 +6464,12 @@ DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__ima(drwav* pWav, drwav_uin drwav_uint64 totalFramesRead = 0; drwav_uint32 iChannel; - static drwav_int32 indexTable[16] = { + static const drwav_int32 indexTable[16] = { -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 }; - static drwav_int32 stepTable[89] = { + static const drwav_int32 stepTable[89] = { 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, @@ -6315,7 +6500,7 @@ DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__ima(drwav* pWav, drwav_uin pWav->ima.bytesRemainingInBlock = pWav->fmt.blockAlign - sizeof(header); if (header[2] >= drwav_countof(stepTable)) { - pWav->onSeek(pWav->pUserData, pWav->ima.bytesRemainingInBlock, drwav_seek_origin_current); + pWav->onSeek(pWav->pUserData, pWav->ima.bytesRemainingInBlock, DRWAV_SEEK_CUR); pWav->ima.bytesRemainingInBlock = 0; return totalFramesRead; /* Invalid data. */ } @@ -6333,7 +6518,7 @@ DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__ima(drwav* pWav, drwav_uin pWav->ima.bytesRemainingInBlock = pWav->fmt.blockAlign - sizeof(header); if (header[2] >= drwav_countof(stepTable) || header[6] >= drwav_countof(stepTable)) { - pWav->onSeek(pWav->pUserData, pWav->ima.bytesRemainingInBlock, drwav_seek_origin_current); + pWav->onSeek(pWav->pUserData, pWav->ima.bytesRemainingInBlock, DRWAV_SEEK_CUR); pWav->ima.bytesRemainingInBlock = 0; return totalFramesRead; /* Invalid data. */ } @@ -6434,7 +6619,7 @@ DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__ima(drwav* pWav, drwav_uin #ifndef DR_WAV_NO_CONVERSION_API -static unsigned short g_drwavAlawTable[256] = { +static const unsigned short g_drwavAlawTable[256] = { 0xEA80, 0xEB80, 0xE880, 0xE980, 0xEE80, 0xEF80, 0xEC80, 0xED80, 0xE280, 0xE380, 0xE080, 0xE180, 0xE680, 0xE780, 0xE480, 0xE580, 0xF540, 0xF5C0, 0xF440, 0xF4C0, 0xF740, 0xF7C0, 0xF640, 0xF6C0, 0xF140, 0xF1C0, 0xF040, 0xF0C0, 0xF340, 0xF3C0, 0xF240, 0xF2C0, 0xAA00, 0xAE00, 0xA200, 0xA600, 0xBA00, 0xBE00, 0xB200, 0xB600, 0x8A00, 0x8E00, 0x8200, 0x8600, 0x9A00, 0x9E00, 0x9200, 0x9600, @@ -6453,7 +6638,7 @@ static unsigned short g_drwavAlawTable[256] = { 0x02B0, 0x0290, 0x02F0, 0x02D0, 0x0230, 0x0210, 0x0270, 0x0250, 0x03B0, 0x0390, 0x03F0, 0x03D0, 0x0330, 0x0310, 0x0370, 0x0350 }; -static unsigned short g_drwavMulawTable[256] = { +static const unsigned short g_drwavMulawTable[256] = { 0x8284, 0x8684, 0x8A84, 0x8E84, 0x9284, 0x9684, 0x9A84, 0x9E84, 0xA284, 0xA684, 0xAA84, 0xAE84, 0xB284, 0xB684, 0xBA84, 0xBE84, 0xC184, 0xC384, 0xC584, 0xC784, 0xC984, 0xCB84, 0xCD84, 0xCF84, 0xD184, 0xD384, 0xD584, 0xD784, 0xD984, 0xDB84, 0xDD84, 0xDF84, 0xE104, 0xE204, 0xE304, 0xE404, 0xE504, 0xE604, 0xE704, 0xE804, 0xE904, 0xEA04, 0xEB04, 0xEC04, 0xED04, 0xEE04, 0xEF04, 0xF004, @@ -7828,7 +8013,7 @@ DRWAV_API void drwav_f32_to_s32(drwav_int32* pOut, const float* pIn, size_t samp } for (i = 0; i < sampleCount; ++i) { - *pOut++ = (drwav_int32)(2147483648.0 * pIn[i]); + *pOut++ = (drwav_int32)(2147483648.0f * pIn[i]); } } @@ -7881,6 +8066,12 @@ DRWAV_PRIVATE drwav_int16* drwav__read_pcm_frames_and_close_s16(drwav* pWav, uns DRWAV_ASSERT(pWav != NULL); + /* Check for overflow before multiplication. */ + if (pWav->channels == 0 || pWav->totalPCMFrameCount > DRWAV_SIZE_MAX / pWav->channels / sizeof(drwav_int16)) { + drwav_uninit(pWav); + return NULL; /* Overflow or invalid channels. */ + } + sampleDataSize = pWav->totalPCMFrameCount * pWav->channels * sizeof(drwav_int16); if (sampleDataSize > DRWAV_SIZE_MAX) { drwav_uninit(pWav); @@ -7923,6 +8114,12 @@ DRWAV_PRIVATE float* drwav__read_pcm_frames_and_close_f32(drwav* pWav, unsigned DRWAV_ASSERT(pWav != NULL); + /* Check for overflow before multiplication. */ + if (pWav->channels == 0 || pWav->totalPCMFrameCount > DRWAV_SIZE_MAX / pWav->channels / sizeof(float)) { + drwav_uninit(pWav); + return NULL; /* Overflow or invalid channels. */ + } + sampleDataSize = pWav->totalPCMFrameCount * pWav->channels * sizeof(float); if (sampleDataSize > DRWAV_SIZE_MAX) { drwav_uninit(pWav); @@ -7965,6 +8162,12 @@ DRWAV_PRIVATE drwav_int32* drwav__read_pcm_frames_and_close_s32(drwav* pWav, uns DRWAV_ASSERT(pWav != NULL); + /* Check for overflow before multiplication. */ + if (pWav->channels == 0 || pWav->totalPCMFrameCount > DRWAV_SIZE_MAX / pWav->channels / sizeof(drwav_int32)) { + drwav_uninit(pWav); + return NULL; /* Overflow or invalid channels. */ + } + sampleDataSize = pWav->totalPCMFrameCount * pWav->channels * sizeof(drwav_int32); if (sampleDataSize > DRWAV_SIZE_MAX) { drwav_uninit(pWav); @@ -8001,7 +8204,7 @@ DRWAV_PRIVATE drwav_int32* drwav__read_pcm_frames_and_close_s32(drwav* pWav, uns -DRWAV_API drwav_int16* drwav_open_and_read_pcm_frames_s16(drwav_read_proc onRead, drwav_seek_proc onSeek, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) +DRWAV_API drwav_int16* drwav_open_and_read_pcm_frames_s16(drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) { drwav wav; @@ -8015,14 +8218,14 @@ DRWAV_API drwav_int16* drwav_open_and_read_pcm_frames_s16(drwav_read_proc onRead *totalFrameCountOut = 0; } - if (!drwav_init(&wav, onRead, onSeek, pUserData, pAllocationCallbacks)) { + if (!drwav_init(&wav, onRead, onSeek, onTell, pUserData, pAllocationCallbacks)) { return NULL; } return drwav__read_pcm_frames_and_close_s16(&wav, channelsOut, sampleRateOut, totalFrameCountOut); } -DRWAV_API float* drwav_open_and_read_pcm_frames_f32(drwav_read_proc onRead, drwav_seek_proc onSeek, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) +DRWAV_API float* drwav_open_and_read_pcm_frames_f32(drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) { drwav wav; @@ -8036,14 +8239,14 @@ DRWAV_API float* drwav_open_and_read_pcm_frames_f32(drwav_read_proc onRead, drwa *totalFrameCountOut = 0; } - if (!drwav_init(&wav, onRead, onSeek, pUserData, pAllocationCallbacks)) { + if (!drwav_init(&wav, onRead, onSeek, onTell, pUserData, pAllocationCallbacks)) { return NULL; } return drwav__read_pcm_frames_and_close_f32(&wav, channelsOut, sampleRateOut, totalFrameCountOut); } -DRWAV_API drwav_int32* drwav_open_and_read_pcm_frames_s32(drwav_read_proc onRead, drwav_seek_proc onSeek, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) +DRWAV_API drwav_int32* drwav_open_and_read_pcm_frames_s32(drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) { drwav wav; @@ -8057,7 +8260,7 @@ DRWAV_API drwav_int32* drwav_open_and_read_pcm_frames_s32(drwav_read_proc onRead *totalFrameCountOut = 0; } - if (!drwav_init(&wav, onRead, onSeek, pUserData, pAllocationCallbacks)) { + if (!drwav_init(&wav, onRead, onSeek, onTell, pUserData, pAllocationCallbacks)) { return NULL; } @@ -8345,13 +8548,59 @@ DRWAV_API drwav_bool32 drwav_fourcc_equal(const drwav_uint8* a, const char* b) /* REVISION HISTORY ================ -v0.13.11 - 07-07-2023 +v0.14.3 - 2025-12-14 + - Fix a possible out-of-bounds read when reading from MS-ADPCM encoded files. + - Fix a possible integer overflow error. + +v0.14.2 - 2025-12-02 + - Fix a compilation warning. + +v0.14.1 - 2025-09-10 + - Fix an error with the NXDK build. + +v0.14.0 - 2025-07-23 + - API CHANGE: Seek origin enums have been renamed to the following: + - drwav_seek_origin_start -> DRWAV_SEEK_SET + - drwav_seek_origin_current -> DRWAV_SEEK_CUR + - DRWAV_SEEK_END (new) + - API CHANGE: A new seek origin has been added to allow seeking from the end of the file. If you implement your own `onSeek` callback, you must now handle `DRWAV_SEEK_END`. If you only use `*_init_file()` or `*_init_memory()`, you need not change anything. + - API CHANGE: An `onTell` callback has been added to the following functions: + - drwav_init() + - drwav_init_ex() + - drwav_init_with_metadata() + - drwav_open_and_read_pcm_frames_s16() + - drwav_open_and_read_pcm_frames_f32() + - drwav_open_and_read_pcm_frames_s32() + - API CHANGE: The `firstSampleByteOffset`, `lastSampleByteOffset` and `sampleByteOffset` members of `drwav_cue_point` have been renamed to `firstSampleOffset`, `lastSampleOffset` and `sampleOffset`, respectively. + - Fix a static analysis warning. + - Fix compilation for AIX OS. + +v0.13.17 - 2024-12-17 + - Fix a possible crash when reading from MS-ADPCM encoded files. + - Improve detection of ARM64EC + +v0.13.16 - 2024-02-27 + - Fix a Wdouble-promotion warning. + +v0.13.15 - 2024-01-23 + - Relax some unnecessary validation that prevented some files from loading. + +v0.13.14 - 2023-12-02 + - Fix a warning about an unused variable. + +v0.13.13 - 2023-11-02 + - Fix a warning when compiling with Clang. + +v0.13.12 - 2023-08-07 + - Fix a possible crash in drwav_read_pcm_frames(). + +v0.13.11 - 2023-07-07 - AIFF compatibility improvements. v0.13.10 - 2023-05-29 - Fix a bug where drwav_init_with_metadata() does not decode any frames after initializtion. -v0.13.9 - 2022-05-22 +v0.13.9 - 2023-05-22 - Add support for AIFF decoding (writing and metadata not supported). - Add support for RIFX decoding (writing and metadata not supported). - Fix a bug where metadata is not processed if it's located before the "fmt " chunk. diff --git a/tools/common/json.hpp b/tools/common/json.hpp new file mode 100644 index 000000000..cf1d68c27 --- /dev/null +++ b/tools/common/json.hpp @@ -0,0 +1,25366 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-License-Identifier: MIT + +/****************************************************************************\ + * Note on documentation: The source files contain links to the online * + * documentation of the public API at https://json.nlohmann.me. This URL * + * contains the most recent documentation and should also be applicable to * + * previous versions; documentation for deprecated functions is not * + * removed, but marked deprecated. See "Generate documentation" section in * + * file docs/README.md. * +\****************************************************************************/ + +#ifndef INCLUDE_NLOHMANN_JSON_HPP_ +#define INCLUDE_NLOHMANN_JSON_HPP_ + +#include // all_of, find, for_each +#include // nullptr_t, ptrdiff_t, size_t +#include // hash, less +#include // initializer_list +#ifndef JSON_NO_IO + #include // istream, ostream +#endif // JSON_NO_IO +#include // random_access_iterator_tag +#include // unique_ptr +#include // string, stoi, to_string +#include // declval, forward, move, pair, swap +#include // vector + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// This file contains all macro definitions affecting or depending on the ABI + +#ifndef JSON_SKIP_LIBRARY_VERSION_CHECK + #if defined(NLOHMANN_JSON_VERSION_MAJOR) && defined(NLOHMANN_JSON_VERSION_MINOR) && defined(NLOHMANN_JSON_VERSION_PATCH) + #if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 11 || NLOHMANN_JSON_VERSION_PATCH != 3 + #warning "Already included a different version of the library!" + #endif + #endif +#endif + +#define NLOHMANN_JSON_VERSION_MAJOR 3 // NOLINT(modernize-macro-to-enum) +#define NLOHMANN_JSON_VERSION_MINOR 11 // NOLINT(modernize-macro-to-enum) +#define NLOHMANN_JSON_VERSION_PATCH 3 // NOLINT(modernize-macro-to-enum) + +#ifndef JSON_DIAGNOSTICS + #define JSON_DIAGNOSTICS 0 +#endif + +#ifndef JSON_DIAGNOSTIC_POSITIONS + #define JSON_DIAGNOSTIC_POSITIONS 0 +#endif + +#ifndef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON + #define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 0 +#endif + +#if JSON_DIAGNOSTICS + #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag +#else + #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS +#endif + +#if JSON_DIAGNOSTIC_POSITIONS + #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS _dp +#else + #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS +#endif + +#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON + #define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON _ldvcmp +#else + #define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON +#endif + +#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION + #define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0 +#endif + +// Construct the namespace ABI tags component +#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c) json_abi ## a ## b ## c +#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c) \ + NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c) + +#define NLOHMANN_JSON_ABI_TAGS \ + NLOHMANN_JSON_ABI_TAGS_CONCAT( \ + NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS, \ + NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON, \ + NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS) + +// Construct the namespace version component +#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \ + _v ## major ## _ ## minor ## _ ## patch +#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(major, minor, patch) \ + NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) + +#if NLOHMANN_JSON_NAMESPACE_NO_VERSION +#define NLOHMANN_JSON_NAMESPACE_VERSION +#else +#define NLOHMANN_JSON_NAMESPACE_VERSION \ + NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(NLOHMANN_JSON_VERSION_MAJOR, \ + NLOHMANN_JSON_VERSION_MINOR, \ + NLOHMANN_JSON_VERSION_PATCH) +#endif + +// Combine namespace components +#define NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) a ## b +#define NLOHMANN_JSON_NAMESPACE_CONCAT(a, b) \ + NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) + +#ifndef NLOHMANN_JSON_NAMESPACE +#define NLOHMANN_JSON_NAMESPACE \ + nlohmann::NLOHMANN_JSON_NAMESPACE_CONCAT( \ + NLOHMANN_JSON_ABI_TAGS, \ + NLOHMANN_JSON_NAMESPACE_VERSION) +#endif + +#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN +#define NLOHMANN_JSON_NAMESPACE_BEGIN \ + namespace nlohmann \ + { \ + inline namespace NLOHMANN_JSON_NAMESPACE_CONCAT( \ + NLOHMANN_JSON_ABI_TAGS, \ + NLOHMANN_JSON_NAMESPACE_VERSION) \ + { +#endif + +#ifndef NLOHMANN_JSON_NAMESPACE_END +#define NLOHMANN_JSON_NAMESPACE_END \ + } /* namespace (inline namespace) NOLINT(readability/namespace) */ \ + } // namespace nlohmann +#endif + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // transform +#include // array +#include // forward_list +#include // inserter, front_inserter, end +#include // map +#ifdef JSON_HAS_CPP_17 + #include // optional +#endif +#include // string +#include // tuple, make_tuple +#include // is_arithmetic, is_same, is_enum, underlying_type, is_convertible +#include // unordered_map +#include // pair, declval +#include // valarray + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // nullptr_t +#include // exception +#if JSON_DIAGNOSTICS + #include // accumulate +#endif +#include // runtime_error +#include // to_string +#include // vector + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // array +#include // size_t +#include // uint8_t +#include // string + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // declval, pair +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +template struct make_void +{ + using type = void; +}; +template using void_t = typename make_void::type; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +// https://en.cppreference.com/w/cpp/experimental/is_detected +struct nonesuch +{ + nonesuch() = delete; + ~nonesuch() = delete; + nonesuch(nonesuch const&) = delete; + nonesuch(nonesuch const&&) = delete; + void operator=(nonesuch const&) = delete; + void operator=(nonesuch&&) = delete; +}; + +template class Op, + class... Args> +struct detector +{ + using value_t = std::false_type; + using type = Default; +}; + +template class Op, class... Args> +struct detector>, Op, Args...> +{ + using value_t = std::true_type; + using type = Op; +}; + +template class Op, class... Args> +using is_detected = typename detector::value_t; + +template class Op, class... Args> +struct is_detected_lazy : is_detected { }; + +template class Op, class... Args> +using detected_t = typename detector::type; + +template class Op, class... Args> +using detected_or = detector; + +template class Op, class... Args> +using detected_or_t = typename detected_or::type; + +template class Op, class... Args> +using is_detected_exact = std::is_same>; + +template class Op, class... Args> +using is_detected_convertible = + std::is_convertible, To>; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + + +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-FileCopyrightText: 2016 - 2021 Evan Nemerson +// SPDX-License-Identifier: MIT + +/* Hedley - https://nemequ.github.io/hedley + * Created by Evan Nemerson + */ + +#if !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < 15) +#if defined(JSON_HEDLEY_VERSION) + #undef JSON_HEDLEY_VERSION +#endif +#define JSON_HEDLEY_VERSION 15 + +#if defined(JSON_HEDLEY_STRINGIFY_EX) + #undef JSON_HEDLEY_STRINGIFY_EX +#endif +#define JSON_HEDLEY_STRINGIFY_EX(x) #x + +#if defined(JSON_HEDLEY_STRINGIFY) + #undef JSON_HEDLEY_STRINGIFY +#endif +#define JSON_HEDLEY_STRINGIFY(x) JSON_HEDLEY_STRINGIFY_EX(x) + +#if defined(JSON_HEDLEY_CONCAT_EX) + #undef JSON_HEDLEY_CONCAT_EX +#endif +#define JSON_HEDLEY_CONCAT_EX(a,b) a##b + +#if defined(JSON_HEDLEY_CONCAT) + #undef JSON_HEDLEY_CONCAT +#endif +#define JSON_HEDLEY_CONCAT(a,b) JSON_HEDLEY_CONCAT_EX(a,b) + +#if defined(JSON_HEDLEY_CONCAT3_EX) + #undef JSON_HEDLEY_CONCAT3_EX +#endif +#define JSON_HEDLEY_CONCAT3_EX(a,b,c) a##b##c + +#if defined(JSON_HEDLEY_CONCAT3) + #undef JSON_HEDLEY_CONCAT3 +#endif +#define JSON_HEDLEY_CONCAT3(a,b,c) JSON_HEDLEY_CONCAT3_EX(a,b,c) + +#if defined(JSON_HEDLEY_VERSION_ENCODE) + #undef JSON_HEDLEY_VERSION_ENCODE +#endif +#define JSON_HEDLEY_VERSION_ENCODE(major,minor,revision) (((major) * 1000000) + ((minor) * 1000) + (revision)) + +#if defined(JSON_HEDLEY_VERSION_DECODE_MAJOR) + #undef JSON_HEDLEY_VERSION_DECODE_MAJOR +#endif +#define JSON_HEDLEY_VERSION_DECODE_MAJOR(version) ((version) / 1000000) + +#if defined(JSON_HEDLEY_VERSION_DECODE_MINOR) + #undef JSON_HEDLEY_VERSION_DECODE_MINOR +#endif +#define JSON_HEDLEY_VERSION_DECODE_MINOR(version) (((version) % 1000000) / 1000) + +#if defined(JSON_HEDLEY_VERSION_DECODE_REVISION) + #undef JSON_HEDLEY_VERSION_DECODE_REVISION +#endif +#define JSON_HEDLEY_VERSION_DECODE_REVISION(version) ((version) % 1000) + +#if defined(JSON_HEDLEY_GNUC_VERSION) + #undef JSON_HEDLEY_GNUC_VERSION +#endif +#if defined(__GNUC__) && defined(__GNUC_PATCHLEVEL__) + #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#elif defined(__GNUC__) + #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, 0) +#endif + +#if defined(JSON_HEDLEY_GNUC_VERSION_CHECK) + #undef JSON_HEDLEY_GNUC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_GNUC_VERSION) + #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GNUC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_MSVC_VERSION) + #undef JSON_HEDLEY_MSVC_VERSION +#endif +#if defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 140000000) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 10000000, (_MSC_FULL_VER % 10000000) / 100000, (_MSC_FULL_VER % 100000) / 100) +#elif defined(_MSC_FULL_VER) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 1000000, (_MSC_FULL_VER % 1000000) / 10000, (_MSC_FULL_VER % 10000) / 10) +#elif defined(_MSC_VER) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_VER / 100, _MSC_VER % 100, 0) +#endif + +#if defined(JSON_HEDLEY_MSVC_VERSION_CHECK) + #undef JSON_HEDLEY_MSVC_VERSION_CHECK +#endif +#if !defined(JSON_HEDLEY_MSVC_VERSION) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (0) +#elif defined(_MSC_VER) && (_MSC_VER >= 1400) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 10000000) + (minor * 100000) + (patch))) +#elif defined(_MSC_VER) && (_MSC_VER >= 1200) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 1000000) + (minor * 10000) + (patch))) +#else + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_VER >= ((major * 100) + (minor))) +#endif + +#if defined(JSON_HEDLEY_INTEL_VERSION) + #undef JSON_HEDLEY_INTEL_VERSION +#endif +#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE) && !defined(__ICL) + #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, __INTEL_COMPILER_UPDATE) +#elif defined(__INTEL_COMPILER) && !defined(__ICL) + #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, 0) +#endif + +#if defined(JSON_HEDLEY_INTEL_VERSION_CHECK) + #undef JSON_HEDLEY_INTEL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_INTEL_VERSION) + #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_INTEL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_INTEL_CL_VERSION) + #undef JSON_HEDLEY_INTEL_CL_VERSION +#endif +#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE) && defined(__ICL) + #define JSON_HEDLEY_INTEL_CL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER, __INTEL_COMPILER_UPDATE, 0) +#endif + +#if defined(JSON_HEDLEY_INTEL_CL_VERSION_CHECK) + #undef JSON_HEDLEY_INTEL_CL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_INTEL_CL_VERSION) + #define JSON_HEDLEY_INTEL_CL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_INTEL_CL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_INTEL_CL_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_PGI_VERSION) + #undef JSON_HEDLEY_PGI_VERSION +#endif +#if defined(__PGI) && defined(__PGIC__) && defined(__PGIC_MINOR__) && defined(__PGIC_PATCHLEVEL__) + #define JSON_HEDLEY_PGI_VERSION JSON_HEDLEY_VERSION_ENCODE(__PGIC__, __PGIC_MINOR__, __PGIC_PATCHLEVEL__) +#endif + +#if defined(JSON_HEDLEY_PGI_VERSION_CHECK) + #undef JSON_HEDLEY_PGI_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_PGI_VERSION) + #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PGI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_SUNPRO_VERSION) + #undef JSON_HEDLEY_SUNPRO_VERSION +#endif +#if defined(__SUNPRO_C) && (__SUNPRO_C > 0x1000) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_C >> 16) & 0xf) * 10) + ((__SUNPRO_C >> 12) & 0xf), (((__SUNPRO_C >> 8) & 0xf) * 10) + ((__SUNPRO_C >> 4) & 0xf), (__SUNPRO_C & 0xf) * 10) +#elif defined(__SUNPRO_C) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_C >> 8) & 0xf, (__SUNPRO_C >> 4) & 0xf, (__SUNPRO_C) & 0xf) +#elif defined(__SUNPRO_CC) && (__SUNPRO_CC > 0x1000) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_CC >> 16) & 0xf) * 10) + ((__SUNPRO_CC >> 12) & 0xf), (((__SUNPRO_CC >> 8) & 0xf) * 10) + ((__SUNPRO_CC >> 4) & 0xf), (__SUNPRO_CC & 0xf) * 10) +#elif defined(__SUNPRO_CC) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_CC >> 8) & 0xf, (__SUNPRO_CC >> 4) & 0xf, (__SUNPRO_CC) & 0xf) +#endif + +#if defined(JSON_HEDLEY_SUNPRO_VERSION_CHECK) + #undef JSON_HEDLEY_SUNPRO_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_SUNPRO_VERSION) + #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_SUNPRO_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION) + #undef JSON_HEDLEY_EMSCRIPTEN_VERSION +#endif +#if defined(__EMSCRIPTEN__) + #define JSON_HEDLEY_EMSCRIPTEN_VERSION JSON_HEDLEY_VERSION_ENCODE(__EMSCRIPTEN_major__, __EMSCRIPTEN_minor__, __EMSCRIPTEN_tiny__) +#endif + +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK) + #undef JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION) + #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_EMSCRIPTEN_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_ARM_VERSION) + #undef JSON_HEDLEY_ARM_VERSION +#endif +#if defined(__CC_ARM) && defined(__ARMCOMPILER_VERSION) + #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCOMPILER_VERSION / 1000000, (__ARMCOMPILER_VERSION % 1000000) / 10000, (__ARMCOMPILER_VERSION % 10000) / 100) +#elif defined(__CC_ARM) && defined(__ARMCC_VERSION) + #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCC_VERSION / 1000000, (__ARMCC_VERSION % 1000000) / 10000, (__ARMCC_VERSION % 10000) / 100) +#endif + +#if defined(JSON_HEDLEY_ARM_VERSION_CHECK) + #undef JSON_HEDLEY_ARM_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_ARM_VERSION) + #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_ARM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_IBM_VERSION) + #undef JSON_HEDLEY_IBM_VERSION +#endif +#if defined(__ibmxl__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ibmxl_version__, __ibmxl_release__, __ibmxl_modification__) +#elif defined(__xlC__) && defined(__xlC_ver__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, (__xlC_ver__ >> 8) & 0xff) +#elif defined(__xlC__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, 0) +#endif + +#if defined(JSON_HEDLEY_IBM_VERSION_CHECK) + #undef JSON_HEDLEY_IBM_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_IBM_VERSION) + #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IBM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_VERSION) + #undef JSON_HEDLEY_TI_VERSION +#endif +#if \ + defined(__TI_COMPILER_VERSION__) && \ + ( \ + defined(__TMS470__) || defined(__TI_ARM__) || \ + defined(__MSP430__) || \ + defined(__TMS320C2000__) \ + ) +#if (__TI_COMPILER_VERSION__ >= 16000000) + #define JSON_HEDLEY_TI_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif +#endif + +#if defined(JSON_HEDLEY_TI_VERSION_CHECK) + #undef JSON_HEDLEY_TI_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_VERSION) + #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL2000_VERSION) + #undef JSON_HEDLEY_TI_CL2000_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C2000__) + #define JSON_HEDLEY_TI_CL2000_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL2000_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL2000_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL2000_VERSION) + #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL2000_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL430_VERSION) + #undef JSON_HEDLEY_TI_CL430_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__MSP430__) + #define JSON_HEDLEY_TI_CL430_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL430_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL430_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL430_VERSION) + #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL430_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION) + #undef JSON_HEDLEY_TI_ARMCL_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && (defined(__TMS470__) || defined(__TI_ARM__)) + #define JSON_HEDLEY_TI_ARMCL_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION_CHECK) + #undef JSON_HEDLEY_TI_ARMCL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION) + #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_ARMCL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL6X_VERSION) + #undef JSON_HEDLEY_TI_CL6X_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C6X__) + #define JSON_HEDLEY_TI_CL6X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL6X_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL6X_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL6X_VERSION) + #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL6X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL7X_VERSION) + #undef JSON_HEDLEY_TI_CL7X_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__C7000__) + #define JSON_HEDLEY_TI_CL7X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL7X_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL7X_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL7X_VERSION) + #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL7X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION) + #undef JSON_HEDLEY_TI_CLPRU_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__PRU__) + #define JSON_HEDLEY_TI_CLPRU_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CLPRU_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION) + #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CLPRU_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_CRAY_VERSION) + #undef JSON_HEDLEY_CRAY_VERSION +#endif +#if defined(_CRAYC) + #if defined(_RELEASE_PATCHLEVEL) + #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, _RELEASE_PATCHLEVEL) + #else + #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, 0) + #endif +#endif + +#if defined(JSON_HEDLEY_CRAY_VERSION_CHECK) + #undef JSON_HEDLEY_CRAY_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_CRAY_VERSION) + #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_CRAY_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_IAR_VERSION) + #undef JSON_HEDLEY_IAR_VERSION +#endif +#if defined(__IAR_SYSTEMS_ICC__) + #if __VER__ > 1000 + #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE((__VER__ / 1000000), ((__VER__ / 1000) % 1000), (__VER__ % 1000)) + #else + #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE(__VER__ / 100, __VER__ % 100, 0) + #endif +#endif + +#if defined(JSON_HEDLEY_IAR_VERSION_CHECK) + #undef JSON_HEDLEY_IAR_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_IAR_VERSION) + #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IAR_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TINYC_VERSION) + #undef JSON_HEDLEY_TINYC_VERSION +#endif +#if defined(__TINYC__) + #define JSON_HEDLEY_TINYC_VERSION JSON_HEDLEY_VERSION_ENCODE(__TINYC__ / 1000, (__TINYC__ / 100) % 10, __TINYC__ % 100) +#endif + +#if defined(JSON_HEDLEY_TINYC_VERSION_CHECK) + #undef JSON_HEDLEY_TINYC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TINYC_VERSION) + #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TINYC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_DMC_VERSION) + #undef JSON_HEDLEY_DMC_VERSION +#endif +#if defined(__DMC__) + #define JSON_HEDLEY_DMC_VERSION JSON_HEDLEY_VERSION_ENCODE(__DMC__ >> 8, (__DMC__ >> 4) & 0xf, __DMC__ & 0xf) +#endif + +#if defined(JSON_HEDLEY_DMC_VERSION_CHECK) + #undef JSON_HEDLEY_DMC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_DMC_VERSION) + #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_DMC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_COMPCERT_VERSION) + #undef JSON_HEDLEY_COMPCERT_VERSION +#endif +#if defined(__COMPCERT_VERSION__) + #define JSON_HEDLEY_COMPCERT_VERSION JSON_HEDLEY_VERSION_ENCODE(__COMPCERT_VERSION__ / 10000, (__COMPCERT_VERSION__ / 100) % 100, __COMPCERT_VERSION__ % 100) +#endif + +#if defined(JSON_HEDLEY_COMPCERT_VERSION_CHECK) + #undef JSON_HEDLEY_COMPCERT_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_COMPCERT_VERSION) + #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_COMPCERT_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_PELLES_VERSION) + #undef JSON_HEDLEY_PELLES_VERSION +#endif +#if defined(__POCC__) + #define JSON_HEDLEY_PELLES_VERSION JSON_HEDLEY_VERSION_ENCODE(__POCC__ / 100, __POCC__ % 100, 0) +#endif + +#if defined(JSON_HEDLEY_PELLES_VERSION_CHECK) + #undef JSON_HEDLEY_PELLES_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_PELLES_VERSION) + #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PELLES_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_MCST_LCC_VERSION) + #undef JSON_HEDLEY_MCST_LCC_VERSION +#endif +#if defined(__LCC__) && defined(__LCC_MINOR__) + #define JSON_HEDLEY_MCST_LCC_VERSION JSON_HEDLEY_VERSION_ENCODE(__LCC__ / 100, __LCC__ % 100, __LCC_MINOR__) +#endif + +#if defined(JSON_HEDLEY_MCST_LCC_VERSION_CHECK) + #undef JSON_HEDLEY_MCST_LCC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_MCST_LCC_VERSION) + #define JSON_HEDLEY_MCST_LCC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_MCST_LCC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_MCST_LCC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_GCC_VERSION) + #undef JSON_HEDLEY_GCC_VERSION +#endif +#if \ + defined(JSON_HEDLEY_GNUC_VERSION) && \ + !defined(__clang__) && \ + !defined(JSON_HEDLEY_INTEL_VERSION) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_ARM_VERSION) && \ + !defined(JSON_HEDLEY_CRAY_VERSION) && \ + !defined(JSON_HEDLEY_TI_VERSION) && \ + !defined(JSON_HEDLEY_TI_ARMCL_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL430_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL2000_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL6X_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL7X_VERSION) && \ + !defined(JSON_HEDLEY_TI_CLPRU_VERSION) && \ + !defined(__COMPCERT__) && \ + !defined(JSON_HEDLEY_MCST_LCC_VERSION) + #define JSON_HEDLEY_GCC_VERSION JSON_HEDLEY_GNUC_VERSION +#endif + +#if defined(JSON_HEDLEY_GCC_VERSION_CHECK) + #undef JSON_HEDLEY_GCC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_GCC_VERSION) + #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GCC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_ATTRIBUTE +#endif +#if \ + defined(__has_attribute) && \ + ( \ + (!defined(JSON_HEDLEY_IAR_VERSION) || JSON_HEDLEY_IAR_VERSION_CHECK(8,5,9)) \ + ) +# define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) __has_attribute(attribute) +#else +# define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_ATTRIBUTE +#endif +#if defined(__has_attribute) + #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_ATTRIBUTE +#endif +#if defined(__has_attribute) + #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE +#endif +#if \ + defined(__has_cpp_attribute) && \ + defined(__cplusplus) && \ + (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) (0) +#endif + +#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS) + #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS +#endif +#if !defined(__cplusplus) || !defined(__has_cpp_attribute) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0) +#elif \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_IAR_VERSION) && \ + (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) && \ + (!defined(JSON_HEDLEY_MSVC_VERSION) || JSON_HEDLEY_MSVC_VERSION_CHECK(19,20,0)) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(ns::attribute) +#else + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE +#endif +#if defined(__has_cpp_attribute) && defined(__cplusplus) + #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE +#endif +#if defined(__has_cpp_attribute) && defined(__cplusplus) + #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_BUILTIN) + #undef JSON_HEDLEY_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_HAS_BUILTIN(builtin) __has_builtin(builtin) +#else + #define JSON_HEDLEY_HAS_BUILTIN(builtin) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_BUILTIN) + #undef JSON_HEDLEY_GNUC_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_GNUC_HAS_BUILTIN(builtin,major,minor,patch) __has_builtin(builtin) +#else + #define JSON_HEDLEY_GNUC_HAS_BUILTIN(builtin,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_BUILTIN) + #undef JSON_HEDLEY_GCC_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_GCC_HAS_BUILTIN(builtin,major,minor,patch) __has_builtin(builtin) +#else + #define JSON_HEDLEY_GCC_HAS_BUILTIN(builtin,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_FEATURE) + #undef JSON_HEDLEY_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_HAS_FEATURE(feature) __has_feature(feature) +#else + #define JSON_HEDLEY_HAS_FEATURE(feature) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_FEATURE) + #undef JSON_HEDLEY_GNUC_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_GNUC_HAS_FEATURE(feature,major,minor,patch) __has_feature(feature) +#else + #define JSON_HEDLEY_GNUC_HAS_FEATURE(feature,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_FEATURE) + #undef JSON_HEDLEY_GCC_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_GCC_HAS_FEATURE(feature,major,minor,patch) __has_feature(feature) +#else + #define JSON_HEDLEY_GCC_HAS_FEATURE(feature,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_EXTENSION) + #undef JSON_HEDLEY_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_HAS_EXTENSION(extension) __has_extension(extension) +#else + #define JSON_HEDLEY_HAS_EXTENSION(extension) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_EXTENSION) + #undef JSON_HEDLEY_GNUC_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_GNUC_HAS_EXTENSION(extension,major,minor,patch) __has_extension(extension) +#else + #define JSON_HEDLEY_GNUC_HAS_EXTENSION(extension,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_EXTENSION) + #undef JSON_HEDLEY_GCC_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_GCC_HAS_EXTENSION(extension,major,minor,patch) __has_extension(extension) +#else + #define JSON_HEDLEY_GCC_HAS_EXTENSION(extension,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_WARNING) + #undef JSON_HEDLEY_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_HAS_WARNING(warning) __has_warning(warning) +#else + #define JSON_HEDLEY_HAS_WARNING(warning) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_WARNING) + #undef JSON_HEDLEY_GNUC_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_GNUC_HAS_WARNING(warning,major,minor,patch) __has_warning(warning) +#else + #define JSON_HEDLEY_GNUC_HAS_WARNING(warning,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_WARNING) + #undef JSON_HEDLEY_GCC_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_GCC_HAS_WARNING(warning,major,minor,patch) __has_warning(warning) +#else + #define JSON_HEDLEY_GCC_HAS_WARNING(warning,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \ + defined(__clang__) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,4,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,7,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(2,0,1) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(5,0,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,17) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(8,0,0) || \ + (JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) && defined(__C99_PRAGMA_OPERATOR)) + #define JSON_HEDLEY_PRAGMA(value) _Pragma(#value) +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_PRAGMA(value) __pragma(value) +#else + #define JSON_HEDLEY_PRAGMA(value) +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_PUSH) + #undef JSON_HEDLEY_DIAGNOSTIC_PUSH +#endif +#if defined(JSON_HEDLEY_DIAGNOSTIC_POP) + #undef JSON_HEDLEY_DIAGNOSTIC_POP +#endif +#if defined(__clang__) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("clang diagnostic push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("clang diagnostic pop") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("warning(push)") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("warning(pop)") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("GCC diagnostic pop") +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH __pragma(warning(push)) + #define JSON_HEDLEY_DIAGNOSTIC_POP __pragma(warning(pop)) +#elif JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("pop") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,4,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("diag_push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("diag_pop") +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,90,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("warning(push)") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("warning(pop)") +#else + #define JSON_HEDLEY_DIAGNOSTIC_PUSH + #define JSON_HEDLEY_DIAGNOSTIC_POP +#endif + +/* JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_ is for + HEDLEY INTERNAL USE ONLY. API subject to change without notice. */ +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_ +#endif +#if defined(__cplusplus) +# if JSON_HEDLEY_HAS_WARNING("-Wc++98-compat") +# if JSON_HEDLEY_HAS_WARNING("-Wc++17-extensions") +# if JSON_HEDLEY_HAS_WARNING("-Wc++1z-extensions") +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + _Pragma("clang diagnostic ignored \"-Wc++17-extensions\"") \ + _Pragma("clang diagnostic ignored \"-Wc++1z-extensions\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + _Pragma("clang diagnostic ignored \"-Wc++17-extensions\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# endif +# else +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# endif +# endif +#endif +#if !defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(x) x +#endif + +#if defined(JSON_HEDLEY_CONST_CAST) + #undef JSON_HEDLEY_CONST_CAST +#endif +#if defined(__cplusplus) +# define JSON_HEDLEY_CONST_CAST(T, expr) (const_cast(expr)) +#elif \ + JSON_HEDLEY_HAS_WARNING("-Wcast-qual") || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_CONST_CAST(T, expr) (__extension__ ({ \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL \ + ((T) (expr)); \ + JSON_HEDLEY_DIAGNOSTIC_POP \ + })) +#else +# define JSON_HEDLEY_CONST_CAST(T, expr) ((T) (expr)) +#endif + +#if defined(JSON_HEDLEY_REINTERPRET_CAST) + #undef JSON_HEDLEY_REINTERPRET_CAST +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_REINTERPRET_CAST(T, expr) (reinterpret_cast(expr)) +#else + #define JSON_HEDLEY_REINTERPRET_CAST(T, expr) ((T) (expr)) +#endif + +#if defined(JSON_HEDLEY_STATIC_CAST) + #undef JSON_HEDLEY_STATIC_CAST +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_STATIC_CAST(T, expr) (static_cast(expr)) +#else + #define JSON_HEDLEY_STATIC_CAST(T, expr) ((T) (expr)) +#endif + +#if defined(JSON_HEDLEY_CPP_CAST) + #undef JSON_HEDLEY_CPP_CAST +#endif +#if defined(__cplusplus) +# if JSON_HEDLEY_HAS_WARNING("-Wold-style-cast") +# define JSON_HEDLEY_CPP_CAST(T, expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wold-style-cast\"") \ + ((T) (expr)) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# elif JSON_HEDLEY_IAR_VERSION_CHECK(8,3,0) +# define JSON_HEDLEY_CPP_CAST(T, expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("diag_suppress=Pe137") \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_CPP_CAST(T, expr) ((T) (expr)) +# endif +#else +# define JSON_HEDLEY_CPP_CAST(T, expr) (expr) +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wdeprecated-declarations") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("warning(disable:1478 1786)") +#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED __pragma(warning(disable:1478 1786)) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(20,7,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1216,1444,1445") +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1444") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED __pragma(warning(disable:4996)) +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1444") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1291,1718") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) && !defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("error_messages(off,E_DEPRECATED_ATT,E_DEPRECATED_ATT_MESS)") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) && defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("error_messages(off,symdeprecated,symdeprecated2)") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress=Pe1444,Pe1215") +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,90,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("warn(disable:2241)") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("clang diagnostic ignored \"-Wunknown-pragmas\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("warning(disable:161)") +#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS __pragma(warning(disable:161)) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 1675") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("GCC diagnostic ignored \"-Wunknown-pragmas\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS __pragma(warning(disable:4068)) +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(16,9,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 163") +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 163") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress=Pe161") +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 161") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-attributes") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("clang diagnostic ignored \"-Wunknown-attributes\"") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(17,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("warning(disable:1292)") +#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES __pragma(warning(disable:1292)) +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(19,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES __pragma(warning(disable:5030)) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(20,7,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097,1098") +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,14,0) && defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("error_messages(off,attrskipunsup)") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,3,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1173") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress=Pe1097") +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wcast-qual") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("clang diagnostic ignored \"-Wcast-qual\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("warning(disable:2203 2331)") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("GCC diagnostic ignored \"-Wcast-qual\"") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunused-function") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("clang diagnostic ignored \"-Wunused-function\"") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("GCC diagnostic ignored \"-Wunused-function\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(1,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION __pragma(warning(disable:4505)) +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("diag_suppress 3142") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION +#endif + +#if defined(JSON_HEDLEY_DEPRECATED) + #undef JSON_HEDLEY_DEPRECATED +#endif +#if defined(JSON_HEDLEY_DEPRECATED_FOR) + #undef JSON_HEDLEY_DEPRECATED_FOR +#endif +#if \ + JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DEPRECATED(since) __declspec(deprecated("Since " # since)) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __declspec(deprecated("Since " #since "; use " #replacement)) +#elif \ + (JSON_HEDLEY_HAS_EXTENSION(attribute_deprecated_with_message) && !defined(JSON_HEDLEY_IAR_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,5,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,3,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,3,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DEPRECATED(since) __attribute__((__deprecated__("Since " #since))) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __attribute__((__deprecated__("Since " #since "; use " #replacement))) +#elif defined(__cplusplus) && (__cplusplus >= 201402L) + #define JSON_HEDLEY_DEPRECATED(since) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[deprecated("Since " #since)]]) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[deprecated("Since " #since "; use " #replacement)]]) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(deprecated) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) + #define JSON_HEDLEY_DEPRECATED(since) __attribute__((__deprecated__)) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __attribute__((__deprecated__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_PELLES_VERSION_CHECK(6,50,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DEPRECATED(since) __declspec(deprecated) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __declspec(deprecated) +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DEPRECATED(since) _Pragma("deprecated") + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) _Pragma("deprecated") +#else + #define JSON_HEDLEY_DEPRECATED(since) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) +#endif + +#if defined(JSON_HEDLEY_UNAVAILABLE) + #undef JSON_HEDLEY_UNAVAILABLE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(warning) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_UNAVAILABLE(available_since) __attribute__((__warning__("Not available until " #available_since))) +#else + #define JSON_HEDLEY_UNAVAILABLE(available_since) +#endif + +#if defined(JSON_HEDLEY_WARN_UNUSED_RESULT) + #undef JSON_HEDLEY_WARN_UNUSED_RESULT +#endif +#if defined(JSON_HEDLEY_WARN_UNUSED_RESULT_MSG) + #undef JSON_HEDLEY_WARN_UNUSED_RESULT_MSG +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(warn_unused_result) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_WARN_UNUSED_RESULT __attribute__((__warn_unused_result__)) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) __attribute__((__warn_unused_result__)) +#elif (JSON_HEDLEY_HAS_CPP_ATTRIBUTE(nodiscard) >= 201907L) + #define JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard(msg)]]) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE(nodiscard) + #define JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) +#elif defined(_Check_return_) /* SAL */ + #define JSON_HEDLEY_WARN_UNUSED_RESULT _Check_return_ + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) _Check_return_ +#else + #define JSON_HEDLEY_WARN_UNUSED_RESULT + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) +#endif + +#if defined(JSON_HEDLEY_SENTINEL) + #undef JSON_HEDLEY_SENTINEL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(sentinel) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,4,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_SENTINEL(position) __attribute__((__sentinel__(position))) +#else + #define JSON_HEDLEY_SENTINEL(position) +#endif + +#if defined(JSON_HEDLEY_NO_RETURN) + #undef JSON_HEDLEY_NO_RETURN +#endif +#if JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_NO_RETURN __noreturn +#elif \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_NO_RETURN __attribute__((__noreturn__)) +#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L + #define JSON_HEDLEY_NO_RETURN _Noreturn +#elif defined(__cplusplus) && (__cplusplus >= 201103L) + #define JSON_HEDLEY_NO_RETURN JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[noreturn]]) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(noreturn) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,2,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) + #define JSON_HEDLEY_NO_RETURN __attribute__((__noreturn__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_NO_RETURN _Pragma("does_not_return") +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_NO_RETURN __declspec(noreturn) +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,0,0) && defined(__cplusplus) + #define JSON_HEDLEY_NO_RETURN _Pragma("FUNC_NEVER_RETURNS;") +#elif JSON_HEDLEY_COMPCERT_VERSION_CHECK(3,2,0) + #define JSON_HEDLEY_NO_RETURN __attribute((noreturn)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(9,0,0) + #define JSON_HEDLEY_NO_RETURN __declspec(noreturn) +#else + #define JSON_HEDLEY_NO_RETURN +#endif + +#if defined(JSON_HEDLEY_NO_ESCAPE) + #undef JSON_HEDLEY_NO_ESCAPE +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(noescape) + #define JSON_HEDLEY_NO_ESCAPE __attribute__((__noescape__)) +#else + #define JSON_HEDLEY_NO_ESCAPE +#endif + +#if defined(JSON_HEDLEY_UNREACHABLE) + #undef JSON_HEDLEY_UNREACHABLE +#endif +#if defined(JSON_HEDLEY_UNREACHABLE_RETURN) + #undef JSON_HEDLEY_UNREACHABLE_RETURN +#endif +#if defined(JSON_HEDLEY_ASSUME) + #undef JSON_HEDLEY_ASSUME +#endif +#if \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_ASSUME(expr) __assume(expr) +#elif JSON_HEDLEY_HAS_BUILTIN(__builtin_assume) + #define JSON_HEDLEY_ASSUME(expr) __builtin_assume(expr) +#elif \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) + #if defined(__cplusplus) + #define JSON_HEDLEY_ASSUME(expr) std::_nassert(expr) + #else + #define JSON_HEDLEY_ASSUME(expr) _nassert(expr) + #endif +#endif +#if \ + (JSON_HEDLEY_HAS_BUILTIN(__builtin_unreachable) && (!defined(JSON_HEDLEY_ARM_VERSION))) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,5,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,10,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,5) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(10,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_UNREACHABLE() __builtin_unreachable() +#elif defined(JSON_HEDLEY_ASSUME) + #define JSON_HEDLEY_UNREACHABLE() JSON_HEDLEY_ASSUME(0) +#endif +#if !defined(JSON_HEDLEY_ASSUME) + #if defined(JSON_HEDLEY_UNREACHABLE) + #define JSON_HEDLEY_ASSUME(expr) JSON_HEDLEY_STATIC_CAST(void, ((expr) ? 1 : (JSON_HEDLEY_UNREACHABLE(), 1))) + #else + #define JSON_HEDLEY_ASSUME(expr) JSON_HEDLEY_STATIC_CAST(void, expr) + #endif +#endif +#if defined(JSON_HEDLEY_UNREACHABLE) + #if \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) return (JSON_HEDLEY_STATIC_CAST(void, JSON_HEDLEY_ASSUME(0)), (value)) + #else + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) JSON_HEDLEY_UNREACHABLE() + #endif +#else + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) return (value) +#endif +#if !defined(JSON_HEDLEY_UNREACHABLE) + #define JSON_HEDLEY_UNREACHABLE() JSON_HEDLEY_ASSUME(0) +#endif + +JSON_HEDLEY_DIAGNOSTIC_PUSH +#if JSON_HEDLEY_HAS_WARNING("-Wpedantic") + #pragma clang diagnostic ignored "-Wpedantic" +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wc++98-compat-pedantic") && defined(__cplusplus) + #pragma clang diagnostic ignored "-Wc++98-compat-pedantic" +#endif +#if JSON_HEDLEY_GCC_HAS_WARNING("-Wvariadic-macros",4,0,0) + #if defined(__clang__) + #pragma clang diagnostic ignored "-Wvariadic-macros" + #elif defined(JSON_HEDLEY_GCC_VERSION) + #pragma GCC diagnostic ignored "-Wvariadic-macros" + #endif +#endif +#if defined(JSON_HEDLEY_NON_NULL) + #undef JSON_HEDLEY_NON_NULL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(nonnull) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) + #define JSON_HEDLEY_NON_NULL(...) __attribute__((__nonnull__(__VA_ARGS__))) +#else + #define JSON_HEDLEY_NON_NULL(...) +#endif +JSON_HEDLEY_DIAGNOSTIC_POP + +#if defined(JSON_HEDLEY_PRINTF_FORMAT) + #undef JSON_HEDLEY_PRINTF_FORMAT +#endif +#if defined(__MINGW32__) && JSON_HEDLEY_GCC_HAS_ATTRIBUTE(format,4,4,0) && !defined(__USE_MINGW_ANSI_STDIO) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(ms_printf, string_idx, first_to_check))) +#elif defined(__MINGW32__) && JSON_HEDLEY_GCC_HAS_ATTRIBUTE(format,4,4,0) && defined(__USE_MINGW_ANSI_STDIO) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(gnu_printf, string_idx, first_to_check))) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(format) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(__printf__, string_idx, first_to_check))) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(6,0,0) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __declspec(vaformat(printf,string_idx,first_to_check)) +#else + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) +#endif + +#if defined(JSON_HEDLEY_CONSTEXPR) + #undef JSON_HEDLEY_CONSTEXPR +#endif +#if defined(__cplusplus) + #if __cplusplus >= 201103L + #define JSON_HEDLEY_CONSTEXPR JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(constexpr) + #endif +#endif +#if !defined(JSON_HEDLEY_CONSTEXPR) + #define JSON_HEDLEY_CONSTEXPR +#endif + +#if defined(JSON_HEDLEY_PREDICT) + #undef JSON_HEDLEY_PREDICT +#endif +#if defined(JSON_HEDLEY_LIKELY) + #undef JSON_HEDLEY_LIKELY +#endif +#if defined(JSON_HEDLEY_UNLIKELY) + #undef JSON_HEDLEY_UNLIKELY +#endif +#if defined(JSON_HEDLEY_UNPREDICTABLE) + #undef JSON_HEDLEY_UNPREDICTABLE +#endif +#if JSON_HEDLEY_HAS_BUILTIN(__builtin_unpredictable) + #define JSON_HEDLEY_UNPREDICTABLE(expr) __builtin_unpredictable((expr)) +#endif +#if \ + (JSON_HEDLEY_HAS_BUILTIN(__builtin_expect_with_probability) && !defined(JSON_HEDLEY_PGI_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(9,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PREDICT(expr, value, probability) __builtin_expect_with_probability( (expr), (value), (probability)) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) __builtin_expect_with_probability(!!(expr), 1 , (probability)) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) __builtin_expect_with_probability(!!(expr), 0 , (probability)) +# define JSON_HEDLEY_LIKELY(expr) __builtin_expect (!!(expr), 1 ) +# define JSON_HEDLEY_UNLIKELY(expr) __builtin_expect (!!(expr), 0 ) +#elif \ + (JSON_HEDLEY_HAS_BUILTIN(__builtin_expect) && !defined(JSON_HEDLEY_INTEL_CL_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,7,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,27) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PREDICT(expr, expected, probability) \ + (((probability) >= 0.9) ? __builtin_expect((expr), (expected)) : (JSON_HEDLEY_STATIC_CAST(void, expected), (expr))) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) \ + (__extension__ ({ \ + double hedley_probability_ = (probability); \ + ((hedley_probability_ >= 0.9) ? __builtin_expect(!!(expr), 1) : ((hedley_probability_ <= 0.1) ? __builtin_expect(!!(expr), 0) : !!(expr))); \ + })) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) \ + (__extension__ ({ \ + double hedley_probability_ = (probability); \ + ((hedley_probability_ >= 0.9) ? __builtin_expect(!!(expr), 0) : ((hedley_probability_ <= 0.1) ? __builtin_expect(!!(expr), 1) : !!(expr))); \ + })) +# define JSON_HEDLEY_LIKELY(expr) __builtin_expect(!!(expr), 1) +# define JSON_HEDLEY_UNLIKELY(expr) __builtin_expect(!!(expr), 0) +#else +# define JSON_HEDLEY_PREDICT(expr, expected, probability) (JSON_HEDLEY_STATIC_CAST(void, expected), (expr)) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) (!!(expr)) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) (!!(expr)) +# define JSON_HEDLEY_LIKELY(expr) (!!(expr)) +# define JSON_HEDLEY_UNLIKELY(expr) (!!(expr)) +#endif +#if !defined(JSON_HEDLEY_UNPREDICTABLE) + #define JSON_HEDLEY_UNPREDICTABLE(expr) JSON_HEDLEY_PREDICT(expr, 1, 0.5) +#endif + +#if defined(JSON_HEDLEY_MALLOC) + #undef JSON_HEDLEY_MALLOC +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(malloc) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(12,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_MALLOC __attribute__((__malloc__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_MALLOC _Pragma("returns_new_memory") +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_MALLOC __declspec(restrict) +#else + #define JSON_HEDLEY_MALLOC +#endif + +#if defined(JSON_HEDLEY_PURE) + #undef JSON_HEDLEY_PURE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(pure) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(2,96,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PURE __attribute__((__pure__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) +# define JSON_HEDLEY_PURE _Pragma("does_not_write_global_data") +#elif defined(__cplusplus) && \ + ( \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(2,0,1) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) \ + ) +# define JSON_HEDLEY_PURE _Pragma("FUNC_IS_PURE;") +#else +# define JSON_HEDLEY_PURE +#endif + +#if defined(JSON_HEDLEY_CONST) + #undef JSON_HEDLEY_CONST +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(const) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(2,5,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_CONST __attribute__((__const__)) +#elif \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_CONST _Pragma("no_side_effect") +#else + #define JSON_HEDLEY_CONST JSON_HEDLEY_PURE +#endif + +#if defined(JSON_HEDLEY_RESTRICT) + #undef JSON_HEDLEY_RESTRICT +#endif +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && !defined(__cplusplus) + #define JSON_HEDLEY_RESTRICT restrict +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,4) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,14,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) || \ + defined(__clang__) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_RESTRICT __restrict +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,3,0) && !defined(__cplusplus) + #define JSON_HEDLEY_RESTRICT _Restrict +#else + #define JSON_HEDLEY_RESTRICT +#endif + +#if defined(JSON_HEDLEY_INLINE) + #undef JSON_HEDLEY_INLINE +#endif +#if \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \ + (defined(__cplusplus) && (__cplusplus >= 199711L)) + #define JSON_HEDLEY_INLINE inline +#elif \ + defined(JSON_HEDLEY_GCC_VERSION) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(6,2,0) + #define JSON_HEDLEY_INLINE __inline__ +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(12,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,1,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_INLINE __inline +#else + #define JSON_HEDLEY_INLINE +#endif + +#if defined(JSON_HEDLEY_ALWAYS_INLINE) + #undef JSON_HEDLEY_ALWAYS_INLINE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(always_inline) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) +# define JSON_HEDLEY_ALWAYS_INLINE __attribute__((__always_inline__)) JSON_HEDLEY_INLINE +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(12,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) +# define JSON_HEDLEY_ALWAYS_INLINE __forceinline +#elif defined(__cplusplus) && \ + ( \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) \ + ) +# define JSON_HEDLEY_ALWAYS_INLINE _Pragma("FUNC_ALWAYS_INLINE;") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) +# define JSON_HEDLEY_ALWAYS_INLINE _Pragma("inline=forced") +#else +# define JSON_HEDLEY_ALWAYS_INLINE JSON_HEDLEY_INLINE +#endif + +#if defined(JSON_HEDLEY_NEVER_INLINE) + #undef JSON_HEDLEY_NEVER_INLINE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(noinline) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) + #define JSON_HEDLEY_NEVER_INLINE __attribute__((__noinline__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_NEVER_INLINE __declspec(noinline) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(10,2,0) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("noinline") +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,0,0) && defined(__cplusplus) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("FUNC_CANNOT_INLINE;") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("inline=never") +#elif JSON_HEDLEY_COMPCERT_VERSION_CHECK(3,2,0) + #define JSON_HEDLEY_NEVER_INLINE __attribute((noinline)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(9,0,0) + #define JSON_HEDLEY_NEVER_INLINE __declspec(noinline) +#else + #define JSON_HEDLEY_NEVER_INLINE +#endif + +#if defined(JSON_HEDLEY_PRIVATE) + #undef JSON_HEDLEY_PRIVATE +#endif +#if defined(JSON_HEDLEY_PUBLIC) + #undef JSON_HEDLEY_PUBLIC +#endif +#if defined(JSON_HEDLEY_IMPORT) + #undef JSON_HEDLEY_IMPORT +#endif +#if defined(_WIN32) || defined(__CYGWIN__) +# define JSON_HEDLEY_PRIVATE +# define JSON_HEDLEY_PUBLIC __declspec(dllexport) +# define JSON_HEDLEY_IMPORT __declspec(dllimport) +#else +# if \ + JSON_HEDLEY_HAS_ATTRIBUTE(visibility) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + ( \ + defined(__TI_EABI__) && \ + ( \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) \ + ) \ + ) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PRIVATE __attribute__((__visibility__("hidden"))) +# define JSON_HEDLEY_PUBLIC __attribute__((__visibility__("default"))) +# else +# define JSON_HEDLEY_PRIVATE +# define JSON_HEDLEY_PUBLIC +# endif +# define JSON_HEDLEY_IMPORT extern +#endif + +#if defined(JSON_HEDLEY_NO_THROW) + #undef JSON_HEDLEY_NO_THROW +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(nothrow) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_NO_THROW __attribute__((__nothrow__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) + #define JSON_HEDLEY_NO_THROW __declspec(nothrow) +#else + #define JSON_HEDLEY_NO_THROW +#endif + +#if defined(JSON_HEDLEY_FALL_THROUGH) + #undef JSON_HEDLEY_FALL_THROUGH +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(fallthrough) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(7,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_FALL_THROUGH __attribute__((__fallthrough__)) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(clang,fallthrough) + #define JSON_HEDLEY_FALL_THROUGH JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[clang::fallthrough]]) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE(fallthrough) + #define JSON_HEDLEY_FALL_THROUGH JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[fallthrough]]) +#elif defined(__fallthrough) /* SAL */ + #define JSON_HEDLEY_FALL_THROUGH __fallthrough +#else + #define JSON_HEDLEY_FALL_THROUGH +#endif + +#if defined(JSON_HEDLEY_RETURNS_NON_NULL) + #undef JSON_HEDLEY_RETURNS_NON_NULL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(returns_nonnull) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_RETURNS_NON_NULL __attribute__((__returns_nonnull__)) +#elif defined(_Ret_notnull_) /* SAL */ + #define JSON_HEDLEY_RETURNS_NON_NULL _Ret_notnull_ +#else + #define JSON_HEDLEY_RETURNS_NON_NULL +#endif + +#if defined(JSON_HEDLEY_ARRAY_PARAM) + #undef JSON_HEDLEY_ARRAY_PARAM +#endif +#if \ + defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && \ + !defined(__STDC_NO_VLA__) && \ + !defined(__cplusplus) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_TINYC_VERSION) + #define JSON_HEDLEY_ARRAY_PARAM(name) (name) +#else + #define JSON_HEDLEY_ARRAY_PARAM(name) +#endif + +#if defined(JSON_HEDLEY_IS_CONSTANT) + #undef JSON_HEDLEY_IS_CONSTANT +#endif +#if defined(JSON_HEDLEY_REQUIRE_CONSTEXPR) + #undef JSON_HEDLEY_REQUIRE_CONSTEXPR +#endif +/* JSON_HEDLEY_IS_CONSTEXPR_ is for + HEDLEY INTERNAL USE ONLY. API subject to change without notice. */ +#if defined(JSON_HEDLEY_IS_CONSTEXPR_) + #undef JSON_HEDLEY_IS_CONSTEXPR_ +#endif +#if \ + JSON_HEDLEY_HAS_BUILTIN(__builtin_constant_p) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,19) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) && !defined(__cplusplus)) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_IS_CONSTANT(expr) __builtin_constant_p(expr) +#endif +#if !defined(__cplusplus) +# if \ + JSON_HEDLEY_HAS_BUILTIN(__builtin_types_compatible_p) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,4,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,24) +#if defined(__INTPTR_TYPE__) + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) __builtin_types_compatible_p(__typeof__((1 ? (void*) ((__INTPTR_TYPE__) ((expr) * 0)) : (int*) 0)), int*) +#else + #include + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) __builtin_types_compatible_p(__typeof__((1 ? (void*) ((intptr_t) ((expr) * 0)) : (int*) 0)), int*) +#endif +# elif \ + ( \ + defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && \ + !defined(JSON_HEDLEY_SUNPRO_VERSION) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_IAR_VERSION)) || \ + (JSON_HEDLEY_HAS_EXTENSION(c_generic_selections) && !defined(JSON_HEDLEY_IAR_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(17,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(12,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,3,0) +#if defined(__INTPTR_TYPE__) + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) _Generic((1 ? (void*) ((__INTPTR_TYPE__) ((expr) * 0)) : (int*) 0), int*: 1, void*: 0) +#else + #include + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) _Generic((1 ? (void*) ((intptr_t) * 0) : (int*) 0), int*: 1, void*: 0) +#endif +# elif \ + defined(JSON_HEDLEY_GCC_VERSION) || \ + defined(JSON_HEDLEY_INTEL_VERSION) || \ + defined(JSON_HEDLEY_TINYC_VERSION) || \ + defined(JSON_HEDLEY_TI_ARMCL_VERSION) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(18,12,0) || \ + defined(JSON_HEDLEY_TI_CL2000_VERSION) || \ + defined(JSON_HEDLEY_TI_CL6X_VERSION) || \ + defined(JSON_HEDLEY_TI_CL7X_VERSION) || \ + defined(JSON_HEDLEY_TI_CLPRU_VERSION) || \ + defined(__clang__) +# define JSON_HEDLEY_IS_CONSTEXPR_(expr) ( \ + sizeof(void) != \ + sizeof(*( \ + 1 ? \ + ((void*) ((expr) * 0L) ) : \ +((struct { char v[sizeof(void) * 2]; } *) 1) \ + ) \ + ) \ + ) +# endif +#endif +#if defined(JSON_HEDLEY_IS_CONSTEXPR_) + #if !defined(JSON_HEDLEY_IS_CONSTANT) + #define JSON_HEDLEY_IS_CONSTANT(expr) JSON_HEDLEY_IS_CONSTEXPR_(expr) + #endif + #define JSON_HEDLEY_REQUIRE_CONSTEXPR(expr) (JSON_HEDLEY_IS_CONSTEXPR_(expr) ? (expr) : (-1)) +#else + #if !defined(JSON_HEDLEY_IS_CONSTANT) + #define JSON_HEDLEY_IS_CONSTANT(expr) (0) + #endif + #define JSON_HEDLEY_REQUIRE_CONSTEXPR(expr) (expr) +#endif + +#if defined(JSON_HEDLEY_BEGIN_C_DECLS) + #undef JSON_HEDLEY_BEGIN_C_DECLS +#endif +#if defined(JSON_HEDLEY_END_C_DECLS) + #undef JSON_HEDLEY_END_C_DECLS +#endif +#if defined(JSON_HEDLEY_C_DECL) + #undef JSON_HEDLEY_C_DECL +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_BEGIN_C_DECLS extern "C" { + #define JSON_HEDLEY_END_C_DECLS } + #define JSON_HEDLEY_C_DECL extern "C" +#else + #define JSON_HEDLEY_BEGIN_C_DECLS + #define JSON_HEDLEY_END_C_DECLS + #define JSON_HEDLEY_C_DECL +#endif + +#if defined(JSON_HEDLEY_STATIC_ASSERT) + #undef JSON_HEDLEY_STATIC_ASSERT +#endif +#if \ + !defined(__cplusplus) && ( \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)) || \ + (JSON_HEDLEY_HAS_FEATURE(c_static_assert) && !defined(JSON_HEDLEY_INTEL_CL_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(6,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + defined(_Static_assert) \ + ) +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) _Static_assert(expr, message) +#elif \ + (defined(__cplusplus) && (__cplusplus >= 201103L)) || \ + JSON_HEDLEY_MSVC_VERSION_CHECK(16,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(static_assert(expr, message)) +#else +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) +#endif + +#if defined(JSON_HEDLEY_NULL) + #undef JSON_HEDLEY_NULL +#endif +#if defined(__cplusplus) + #if __cplusplus >= 201103L + #define JSON_HEDLEY_NULL JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(nullptr) + #elif defined(NULL) + #define JSON_HEDLEY_NULL NULL + #else + #define JSON_HEDLEY_NULL JSON_HEDLEY_STATIC_CAST(void*, 0) + #endif +#elif defined(NULL) + #define JSON_HEDLEY_NULL NULL +#else + #define JSON_HEDLEY_NULL ((void*) 0) +#endif + +#if defined(JSON_HEDLEY_MESSAGE) + #undef JSON_HEDLEY_MESSAGE +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") +# define JSON_HEDLEY_MESSAGE(msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS \ + JSON_HEDLEY_PRAGMA(message msg) \ + JSON_HEDLEY_DIAGNOSTIC_POP +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message msg) +#elif JSON_HEDLEY_CRAY_VERSION_CHECK(5,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(_CRI message msg) +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#else +# define JSON_HEDLEY_MESSAGE(msg) +#endif + +#if defined(JSON_HEDLEY_WARNING) + #undef JSON_HEDLEY_WARNING +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") +# define JSON_HEDLEY_WARNING(msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS \ + JSON_HEDLEY_PRAGMA(clang warning msg) \ + JSON_HEDLEY_DIAGNOSTIC_POP +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,8,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_PRAGMA(GCC warning msg) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#else +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_MESSAGE(msg) +#endif + +#if defined(JSON_HEDLEY_REQUIRE) + #undef JSON_HEDLEY_REQUIRE +#endif +#if defined(JSON_HEDLEY_REQUIRE_MSG) + #undef JSON_HEDLEY_REQUIRE_MSG +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(diagnose_if) +# if JSON_HEDLEY_HAS_WARNING("-Wgcc-compat") +# define JSON_HEDLEY_REQUIRE(expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wgcc-compat\"") \ + __attribute__((diagnose_if(!(expr), #expr, "error"))) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wgcc-compat\"") \ + __attribute__((diagnose_if(!(expr), msg, "error"))) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_REQUIRE(expr) __attribute__((diagnose_if(!(expr), #expr, "error"))) +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) __attribute__((diagnose_if(!(expr), msg, "error"))) +# endif +#else +# define JSON_HEDLEY_REQUIRE(expr) +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) +#endif + +#if defined(JSON_HEDLEY_FLAGS) + #undef JSON_HEDLEY_FLAGS +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(flag_enum) && (!defined(__cplusplus) || JSON_HEDLEY_HAS_WARNING("-Wbitfield-enum-conversion")) + #define JSON_HEDLEY_FLAGS __attribute__((__flag_enum__)) +#else + #define JSON_HEDLEY_FLAGS +#endif + +#if defined(JSON_HEDLEY_FLAGS_CAST) + #undef JSON_HEDLEY_FLAGS_CAST +#endif +#if JSON_HEDLEY_INTEL_VERSION_CHECK(19,0,0) +# define JSON_HEDLEY_FLAGS_CAST(T, expr) (__extension__ ({ \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("warning(disable:188)") \ + ((T) (expr)); \ + JSON_HEDLEY_DIAGNOSTIC_POP \ + })) +#else +# define JSON_HEDLEY_FLAGS_CAST(T, expr) JSON_HEDLEY_STATIC_CAST(T, expr) +#endif + +#if defined(JSON_HEDLEY_EMPTY_BASES) + #undef JSON_HEDLEY_EMPTY_BASES +#endif +#if \ + (JSON_HEDLEY_MSVC_VERSION_CHECK(19,0,23918) && !JSON_HEDLEY_MSVC_VERSION_CHECK(20,0,0)) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_EMPTY_BASES __declspec(empty_bases) +#else + #define JSON_HEDLEY_EMPTY_BASES +#endif + +/* Remaining macros are deprecated. */ + +#if defined(JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK) + #undef JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK +#endif +#if defined(__clang__) + #define JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK(major,minor,patch) (0) +#else + #define JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK(major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_CLANG_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) + +#if defined(JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) + +#if defined(JSON_HEDLEY_CLANG_HAS_BUILTIN) + #undef JSON_HEDLEY_CLANG_HAS_BUILTIN +#endif +#define JSON_HEDLEY_CLANG_HAS_BUILTIN(builtin) JSON_HEDLEY_HAS_BUILTIN(builtin) + +#if defined(JSON_HEDLEY_CLANG_HAS_FEATURE) + #undef JSON_HEDLEY_CLANG_HAS_FEATURE +#endif +#define JSON_HEDLEY_CLANG_HAS_FEATURE(feature) JSON_HEDLEY_HAS_FEATURE(feature) + +#if defined(JSON_HEDLEY_CLANG_HAS_EXTENSION) + #undef JSON_HEDLEY_CLANG_HAS_EXTENSION +#endif +#define JSON_HEDLEY_CLANG_HAS_EXTENSION(extension) JSON_HEDLEY_HAS_EXTENSION(extension) + +#if defined(JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_DECLSPEC_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) + +#if defined(JSON_HEDLEY_CLANG_HAS_WARNING) + #undef JSON_HEDLEY_CLANG_HAS_WARNING +#endif +#define JSON_HEDLEY_CLANG_HAS_WARNING(warning) JSON_HEDLEY_HAS_WARNING(warning) + +#endif /* !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < X) */ + + +// This file contains all internal macro definitions (except those affecting ABI) +// You MUST include macro_unscope.hpp at the end of json.hpp to undef all of them + +// #include + + +// exclude unsupported compilers +#if !defined(JSON_SKIP_UNSUPPORTED_COMPILER_CHECK) + #if defined(__clang__) + #if (__clang_major__ * 10000 + __clang_minor__ * 100 + __clang_patchlevel__) < 30400 + #error "unsupported Clang version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #elif defined(__GNUC__) && !(defined(__ICC) || defined(__INTEL_COMPILER)) + #if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) < 40800 + #error "unsupported GCC version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #endif +#endif + +// C++ language standard detection +// if the user manually specified the used c++ version this is skipped +#if !defined(JSON_HAS_CPP_20) && !defined(JSON_HAS_CPP_17) && !defined(JSON_HAS_CPP_14) && !defined(JSON_HAS_CPP_11) + #if (defined(__cplusplus) && __cplusplus >= 202002L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L) + #define JSON_HAS_CPP_20 + #define JSON_HAS_CPP_17 + #define JSON_HAS_CPP_14 + #elif (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 + #define JSON_HAS_CPP_17 + #define JSON_HAS_CPP_14 + #elif (defined(__cplusplus) && __cplusplus >= 201402L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1) + #define JSON_HAS_CPP_14 + #endif + // the cpp 11 flag is always specified because it is the minimal required version + #define JSON_HAS_CPP_11 +#endif + +#ifdef __has_include + #if __has_include() + #include + #endif +#endif + +#if !defined(JSON_HAS_FILESYSTEM) && !defined(JSON_HAS_EXPERIMENTAL_FILESYSTEM) + #ifdef JSON_HAS_CPP_17 + #if defined(__cpp_lib_filesystem) + #define JSON_HAS_FILESYSTEM 1 + #elif defined(__cpp_lib_experimental_filesystem) + #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 1 + #elif !defined(__has_include) + #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 1 + #elif __has_include() + #define JSON_HAS_FILESYSTEM 1 + #elif __has_include() + #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 1 + #endif + + // std::filesystem does not work on MinGW GCC 8: https://sourceforge.net/p/mingw-w64/bugs/737/ + #if defined(__MINGW32__) && defined(__GNUC__) && __GNUC__ == 8 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before GCC 8: https://en.cppreference.com/w/cpp/compiler_support + #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ < 8 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before Clang 7: https://en.cppreference.com/w/cpp/compiler_support + #if defined(__clang_major__) && __clang_major__ < 7 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before MSVC 19.14: https://en.cppreference.com/w/cpp/compiler_support + #if defined(_MSC_VER) && _MSC_VER < 1914 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before iOS 13 + #if defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < 130000 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before macOS Catalina + #if defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED < 101500 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + #endif +#endif + +#ifndef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 0 +#endif + +#ifndef JSON_HAS_FILESYSTEM + #define JSON_HAS_FILESYSTEM 0 +#endif + +#ifndef JSON_HAS_THREE_WAY_COMPARISON + #if defined(__cpp_impl_three_way_comparison) && __cpp_impl_three_way_comparison >= 201907L \ + && defined(__cpp_lib_three_way_comparison) && __cpp_lib_three_way_comparison >= 201907L + #define JSON_HAS_THREE_WAY_COMPARISON 1 + #else + #define JSON_HAS_THREE_WAY_COMPARISON 0 + #endif +#endif + +#ifndef JSON_HAS_RANGES + // ranges header shipping in GCC 11.1.0 (released 2021-04-27) has syntax error + #if defined(__GLIBCXX__) && __GLIBCXX__ == 20210427 + #define JSON_HAS_RANGES 0 + #elif defined(__cpp_lib_ranges) + #define JSON_HAS_RANGES 1 + #else + #define JSON_HAS_RANGES 0 + #endif +#endif + +#ifndef JSON_HAS_STATIC_RTTI + #if !defined(_HAS_STATIC_RTTI) || _HAS_STATIC_RTTI != 0 + #define JSON_HAS_STATIC_RTTI 1 + #else + #define JSON_HAS_STATIC_RTTI 0 + #endif +#endif + +#ifdef JSON_HAS_CPP_17 + #define JSON_INLINE_VARIABLE inline +#else + #define JSON_INLINE_VARIABLE +#endif + +#if JSON_HEDLEY_HAS_ATTRIBUTE(no_unique_address) + #define JSON_NO_UNIQUE_ADDRESS [[no_unique_address]] +#else + #define JSON_NO_UNIQUE_ADDRESS +#endif + +// disable documentation warnings on clang +#if defined(__clang__) + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdocumentation" + #pragma clang diagnostic ignored "-Wdocumentation-unknown-command" +#endif + +// allow disabling exceptions +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) + #define JSON_THROW(exception) throw exception + #define JSON_TRY try + #define JSON_CATCH(exception) catch(exception) + #define JSON_INTERNAL_CATCH(exception) catch(exception) +#else + #include + #define JSON_THROW(exception) std::abort() + #define JSON_TRY if(true) + #define JSON_CATCH(exception) if(false) + #define JSON_INTERNAL_CATCH(exception) if(false) +#endif + +// override exception macros +#if defined(JSON_THROW_USER) + #undef JSON_THROW + #define JSON_THROW JSON_THROW_USER +#endif +#if defined(JSON_TRY_USER) + #undef JSON_TRY + #define JSON_TRY JSON_TRY_USER +#endif +#if defined(JSON_CATCH_USER) + #undef JSON_CATCH + #define JSON_CATCH JSON_CATCH_USER + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_CATCH_USER +#endif +#if defined(JSON_INTERNAL_CATCH_USER) + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_INTERNAL_CATCH_USER +#endif + +// allow overriding assert +#if !defined(JSON_ASSERT) + #include // assert + #define JSON_ASSERT(x) assert(x) +#endif + +// allow to access some private functions (needed by the test suite) +#if defined(JSON_TESTS_PRIVATE) + #define JSON_PRIVATE_UNLESS_TESTED public +#else + #define JSON_PRIVATE_UNLESS_TESTED private +#endif + +/*! +@brief macro to briefly define a mapping between an enum and JSON +@def NLOHMANN_JSON_SERIALIZE_ENUM +@since version 3.4.0 +*/ +#define NLOHMANN_JSON_SERIALIZE_ENUM(ENUM_TYPE, ...) \ + template \ + inline void to_json(BasicJsonType& j, const ENUM_TYPE& e) \ + { \ + /* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + /* NOLINTNEXTLINE(modernize-avoid-c-arrays) we don't want to depend on */ \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [e](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.first == e; \ + }); \ + j = ((it != std::end(m)) ? it : std::begin(m))->second; \ + } \ + template \ + inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ + { \ + /* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + /* NOLINTNEXTLINE(modernize-avoid-c-arrays) we don't want to depend on */ \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [&j](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.second == j; \ + }); \ + e = ((it != std::end(m)) ? it : std::begin(m))->first; \ + } + +// Ugly macros to avoid uglier copy-paste when specializing basic_json. They +// may be removed in the future once the class is split. + +#define NLOHMANN_BASIC_JSON_TPL_DECLARATION \ + template class ObjectType, \ + template class ArrayType, \ + class StringType, class BooleanType, class NumberIntegerType, \ + class NumberUnsignedType, class NumberFloatType, \ + template class AllocatorType, \ + template class JSONSerializer, \ + class BinaryType, \ + class CustomBaseClass> + +#define NLOHMANN_BASIC_JSON_TPL \ + basic_json + +// Macros to simplify conversion from/to types + +#define NLOHMANN_JSON_EXPAND( x ) x +#define NLOHMANN_JSON_GET_MACRO(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, NAME,...) NAME +#define NLOHMANN_JSON_PASTE(...) NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_GET_MACRO(__VA_ARGS__, \ + NLOHMANN_JSON_PASTE64, \ + NLOHMANN_JSON_PASTE63, \ + NLOHMANN_JSON_PASTE62, \ + NLOHMANN_JSON_PASTE61, \ + NLOHMANN_JSON_PASTE60, \ + NLOHMANN_JSON_PASTE59, \ + NLOHMANN_JSON_PASTE58, \ + NLOHMANN_JSON_PASTE57, \ + NLOHMANN_JSON_PASTE56, \ + NLOHMANN_JSON_PASTE55, \ + NLOHMANN_JSON_PASTE54, \ + NLOHMANN_JSON_PASTE53, \ + NLOHMANN_JSON_PASTE52, \ + NLOHMANN_JSON_PASTE51, \ + NLOHMANN_JSON_PASTE50, \ + NLOHMANN_JSON_PASTE49, \ + NLOHMANN_JSON_PASTE48, \ + NLOHMANN_JSON_PASTE47, \ + NLOHMANN_JSON_PASTE46, \ + NLOHMANN_JSON_PASTE45, \ + NLOHMANN_JSON_PASTE44, \ + NLOHMANN_JSON_PASTE43, \ + NLOHMANN_JSON_PASTE42, \ + NLOHMANN_JSON_PASTE41, \ + NLOHMANN_JSON_PASTE40, \ + NLOHMANN_JSON_PASTE39, \ + NLOHMANN_JSON_PASTE38, \ + NLOHMANN_JSON_PASTE37, \ + NLOHMANN_JSON_PASTE36, \ + NLOHMANN_JSON_PASTE35, \ + NLOHMANN_JSON_PASTE34, \ + NLOHMANN_JSON_PASTE33, \ + NLOHMANN_JSON_PASTE32, \ + NLOHMANN_JSON_PASTE31, \ + NLOHMANN_JSON_PASTE30, \ + NLOHMANN_JSON_PASTE29, \ + NLOHMANN_JSON_PASTE28, \ + NLOHMANN_JSON_PASTE27, \ + NLOHMANN_JSON_PASTE26, \ + NLOHMANN_JSON_PASTE25, \ + NLOHMANN_JSON_PASTE24, \ + NLOHMANN_JSON_PASTE23, \ + NLOHMANN_JSON_PASTE22, \ + NLOHMANN_JSON_PASTE21, \ + NLOHMANN_JSON_PASTE20, \ + NLOHMANN_JSON_PASTE19, \ + NLOHMANN_JSON_PASTE18, \ + NLOHMANN_JSON_PASTE17, \ + NLOHMANN_JSON_PASTE16, \ + NLOHMANN_JSON_PASTE15, \ + NLOHMANN_JSON_PASTE14, \ + NLOHMANN_JSON_PASTE13, \ + NLOHMANN_JSON_PASTE12, \ + NLOHMANN_JSON_PASTE11, \ + NLOHMANN_JSON_PASTE10, \ + NLOHMANN_JSON_PASTE9, \ + NLOHMANN_JSON_PASTE8, \ + NLOHMANN_JSON_PASTE7, \ + NLOHMANN_JSON_PASTE6, \ + NLOHMANN_JSON_PASTE5, \ + NLOHMANN_JSON_PASTE4, \ + NLOHMANN_JSON_PASTE3, \ + NLOHMANN_JSON_PASTE2, \ + NLOHMANN_JSON_PASTE1)(__VA_ARGS__)) +#define NLOHMANN_JSON_PASTE2(func, v1) func(v1) +#define NLOHMANN_JSON_PASTE3(func, v1, v2) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE2(func, v2) +#define NLOHMANN_JSON_PASTE4(func, v1, v2, v3) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE3(func, v2, v3) +#define NLOHMANN_JSON_PASTE5(func, v1, v2, v3, v4) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE4(func, v2, v3, v4) +#define NLOHMANN_JSON_PASTE6(func, v1, v2, v3, v4, v5) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE5(func, v2, v3, v4, v5) +#define NLOHMANN_JSON_PASTE7(func, v1, v2, v3, v4, v5, v6) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE6(func, v2, v3, v4, v5, v6) +#define NLOHMANN_JSON_PASTE8(func, v1, v2, v3, v4, v5, v6, v7) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE7(func, v2, v3, v4, v5, v6, v7) +#define NLOHMANN_JSON_PASTE9(func, v1, v2, v3, v4, v5, v6, v7, v8) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE8(func, v2, v3, v4, v5, v6, v7, v8) +#define NLOHMANN_JSON_PASTE10(func, v1, v2, v3, v4, v5, v6, v7, v8, v9) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE9(func, v2, v3, v4, v5, v6, v7, v8, v9) +#define NLOHMANN_JSON_PASTE11(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE10(func, v2, v3, v4, v5, v6, v7, v8, v9, v10) +#define NLOHMANN_JSON_PASTE12(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE11(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) +#define NLOHMANN_JSON_PASTE13(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE12(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) +#define NLOHMANN_JSON_PASTE14(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE13(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) +#define NLOHMANN_JSON_PASTE15(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE14(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) +#define NLOHMANN_JSON_PASTE16(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE15(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) +#define NLOHMANN_JSON_PASTE17(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE16(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) +#define NLOHMANN_JSON_PASTE18(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE17(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) +#define NLOHMANN_JSON_PASTE19(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE18(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) +#define NLOHMANN_JSON_PASTE20(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE19(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) +#define NLOHMANN_JSON_PASTE21(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE20(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) +#define NLOHMANN_JSON_PASTE22(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE21(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) +#define NLOHMANN_JSON_PASTE23(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE22(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) +#define NLOHMANN_JSON_PASTE24(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE23(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) +#define NLOHMANN_JSON_PASTE25(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE24(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) +#define NLOHMANN_JSON_PASTE26(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE25(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) +#define NLOHMANN_JSON_PASTE27(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE26(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) +#define NLOHMANN_JSON_PASTE28(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE27(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) +#define NLOHMANN_JSON_PASTE29(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE28(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) +#define NLOHMANN_JSON_PASTE30(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE29(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) +#define NLOHMANN_JSON_PASTE31(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE30(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) +#define NLOHMANN_JSON_PASTE32(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE31(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) +#define NLOHMANN_JSON_PASTE33(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE32(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) +#define NLOHMANN_JSON_PASTE34(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE33(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) +#define NLOHMANN_JSON_PASTE35(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE34(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) +#define NLOHMANN_JSON_PASTE36(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE35(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) +#define NLOHMANN_JSON_PASTE37(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE36(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) +#define NLOHMANN_JSON_PASTE38(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE37(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) +#define NLOHMANN_JSON_PASTE39(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE38(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) +#define NLOHMANN_JSON_PASTE40(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE39(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) +#define NLOHMANN_JSON_PASTE41(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE40(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) +#define NLOHMANN_JSON_PASTE42(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE41(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) +#define NLOHMANN_JSON_PASTE43(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE42(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) +#define NLOHMANN_JSON_PASTE44(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE43(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) +#define NLOHMANN_JSON_PASTE45(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE44(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) +#define NLOHMANN_JSON_PASTE46(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE45(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) +#define NLOHMANN_JSON_PASTE47(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE46(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) +#define NLOHMANN_JSON_PASTE48(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE47(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) +#define NLOHMANN_JSON_PASTE49(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE48(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) +#define NLOHMANN_JSON_PASTE50(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE49(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) +#define NLOHMANN_JSON_PASTE51(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE50(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) +#define NLOHMANN_JSON_PASTE52(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE51(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) +#define NLOHMANN_JSON_PASTE53(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE52(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) +#define NLOHMANN_JSON_PASTE54(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE53(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) +#define NLOHMANN_JSON_PASTE55(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE54(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) +#define NLOHMANN_JSON_PASTE56(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE55(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) +#define NLOHMANN_JSON_PASTE57(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE56(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) +#define NLOHMANN_JSON_PASTE58(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE57(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) +#define NLOHMANN_JSON_PASTE59(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE58(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) +#define NLOHMANN_JSON_PASTE60(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE59(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) +#define NLOHMANN_JSON_PASTE61(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE60(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) +#define NLOHMANN_JSON_PASTE62(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE61(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) +#define NLOHMANN_JSON_PASTE63(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE62(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) +#define NLOHMANN_JSON_PASTE64(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE63(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) + +#define NLOHMANN_JSON_TO(v1) nlohmann_json_j[#v1] = nlohmann_json_t.v1; +#define NLOHMANN_JSON_FROM(v1) nlohmann_json_j.at(#v1).get_to(nlohmann_json_t.v1); +#define NLOHMANN_JSON_FROM_WITH_DEFAULT(v1) nlohmann_json_t.v1 = !nlohmann_json_j.is_null() ? nlohmann_json_j.value(#v1, nlohmann_json_default_obj.v1) : nlohmann_json_default_obj.v1; + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_INTRUSIVE +@since version 3.9.0 +*/ +#define NLOHMANN_DEFINE_TYPE_INTRUSIVE(Type, ...) \ + friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + friend void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + +#define NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Type, ...) \ + friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + friend void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + +#define NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(Type, ...) \ + friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE +@since version 3.9.0 +*/ +#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Type, ...) \ + inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + inline void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + +#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE(Type, ...) \ + inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + +#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Type, ...) \ + inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + inline void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE +@since version 3.11.x +*/ +#define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE(Type, BaseType, ...) \ + friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + friend void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + +#define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT(Type, BaseType, ...) \ + friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + friend void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + +#define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_ONLY_SERIALIZE(Type, BaseType, ...) \ + friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + +/*! +@brief macro +@def NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE +@since version 3.11.x +*/ +#define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE(Type, BaseType, ...) \ + inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + inline void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + +#define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Type, BaseType, ...) \ + inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + inline void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + +#define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE(Type, BaseType, ...) \ + inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + +// inspired from https://stackoverflow.com/a/26745591 +// allows to call any std function as if (e.g. with begin): +// using std::begin; begin(x); +// +// it allows using the detected idiom to retrieve the return type +// of such an expression +#define NLOHMANN_CAN_CALL_STD_FUNC_IMPL(std_name) \ + namespace detail { \ + using std::std_name; \ + \ + template \ + using result_of_##std_name = decltype(std_name(std::declval()...)); \ + } \ + \ + namespace detail2 { \ + struct std_name##_tag \ + { \ + }; \ + \ + template \ + std_name##_tag std_name(T&&...); \ + \ + template \ + using result_of_##std_name = decltype(std_name(std::declval()...)); \ + \ + template \ + struct would_call_std_##std_name \ + { \ + static constexpr auto const value = ::nlohmann::detail:: \ + is_detected_exact::value; \ + }; \ + } /* namespace detail2 */ \ + \ + template \ + struct would_call_std_##std_name : detail2::would_call_std_##std_name \ + { \ + } + +#ifndef JSON_USE_IMPLICIT_CONVERSIONS + #define JSON_USE_IMPLICIT_CONVERSIONS 1 +#endif + +#if JSON_USE_IMPLICIT_CONVERSIONS + #define JSON_EXPLICIT +#else + #define JSON_EXPLICIT explicit +#endif + +#ifndef JSON_DISABLE_ENUM_SERIALIZATION + #define JSON_DISABLE_ENUM_SERIALIZATION 0 +#endif + +#ifndef JSON_USE_GLOBAL_UDLS + #define JSON_USE_GLOBAL_UDLS 1 +#endif + +#if JSON_HAS_THREE_WAY_COMPARISON + #include // partial_ordering +#endif + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/////////////////////////// +// JSON type enumeration // +/////////////////////////// + +/*! +@brief the JSON type enumeration + +This enumeration collects the different JSON types. It is internally used to +distinguish the stored values, and the functions @ref basic_json::is_null(), +@ref basic_json::is_object(), @ref basic_json::is_array(), +@ref basic_json::is_string(), @ref basic_json::is_boolean(), +@ref basic_json::is_number() (with @ref basic_json::is_number_integer(), +@ref basic_json::is_number_unsigned(), and @ref basic_json::is_number_float()), +@ref basic_json::is_discarded(), @ref basic_json::is_primitive(), and +@ref basic_json::is_structured() rely on it. + +@note There are three enumeration entries (number_integer, number_unsigned, and +number_float), because the library distinguishes these three types for numbers: +@ref basic_json::number_unsigned_t is used for unsigned integers, +@ref basic_json::number_integer_t is used for signed integers, and +@ref basic_json::number_float_t is used for floating-point numbers or to +approximate integers which do not fit in the limits of their respective type. + +@sa see @ref basic_json::basic_json(const value_t value_type) -- create a JSON +value with the default value for a given type + +@since version 1.0.0 +*/ +enum class value_t : std::uint8_t +{ + null, ///< null value + object, ///< object (unordered set of name/value pairs) + array, ///< array (ordered collection of values) + string, ///< string value + boolean, ///< boolean value + number_integer, ///< number value (signed integer) + number_unsigned, ///< number value (unsigned integer) + number_float, ///< number value (floating-point) + binary, ///< binary array (ordered collection of bytes) + discarded ///< discarded by the parser callback function +}; + +/*! +@brief comparison operator for JSON types + +Returns an ordering that is similar to Python: +- order: null < boolean < number < object < array < string < binary +- furthermore, each type is not smaller than itself +- discarded values are not comparable +- binary is represented as a b"" string in python and directly comparable to a + string; however, making a binary array directly comparable with a string would + be surprising behavior in a JSON file. + +@since version 1.0.0 +*/ +#if JSON_HAS_THREE_WAY_COMPARISON + inline std::partial_ordering operator<=>(const value_t lhs, const value_t rhs) noexcept // *NOPAD* +#else + inline bool operator<(const value_t lhs, const value_t rhs) noexcept +#endif +{ + static constexpr std::array order = {{ + 0 /* null */, 3 /* object */, 4 /* array */, 5 /* string */, + 1 /* boolean */, 2 /* integer */, 2 /* unsigned */, 2 /* float */, + 6 /* binary */ + } + }; + + const auto l_index = static_cast(lhs); + const auto r_index = static_cast(rhs); +#if JSON_HAS_THREE_WAY_COMPARISON + if (l_index < order.size() && r_index < order.size()) + { + return order[l_index] <=> order[r_index]; // *NOPAD* + } + return std::partial_ordering::unordered; +#else + return l_index < order.size() && r_index < order.size() && order[l_index] < order[r_index]; +#endif +} + +// GCC selects the built-in operator< over an operator rewritten from +// a user-defined spaceship operator +// Clang, MSVC, and ICC select the rewritten candidate +// (see GCC bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105200) +#if JSON_HAS_THREE_WAY_COMPARISON && defined(__GNUC__) +inline bool operator<(const value_t lhs, const value_t rhs) noexcept +{ + return std::is_lt(lhs <=> rhs); // *NOPAD* +} +#endif + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/*! +@brief replace all occurrences of a substring by another string + +@param[in,out] s the string to manipulate; changed so that all + occurrences of @a f are replaced with @a t +@param[in] f the substring to replace with @a t +@param[in] t the string to replace @a f + +@pre The search string @a f must not be empty. **This precondition is +enforced with an assertion.** + +@since version 2.0.0 +*/ +template +inline void replace_substring(StringType& s, const StringType& f, + const StringType& t) +{ + JSON_ASSERT(!f.empty()); + for (auto pos = s.find(f); // find first occurrence of f + pos != StringType::npos; // make sure f was found + s.replace(pos, f.size(), t), // replace with t, and + pos = s.find(f, pos + t.size())) // find next occurrence of f + {} +} + +/*! + * @brief string escaping as described in RFC 6901 (Sect. 4) + * @param[in] s string to escape + * @return escaped string + * + * Note the order of escaping "~" to "~0" and "/" to "~1" is important. + */ +template +inline StringType escape(StringType s) +{ + replace_substring(s, StringType{"~"}, StringType{"~0"}); + replace_substring(s, StringType{"/"}, StringType{"~1"}); + return s; +} + +/*! + * @brief string unescaping as described in RFC 6901 (Sect. 4) + * @param[in] s string to unescape + * @return unescaped string + * + * Note the order of escaping "~1" to "/" and "~0" to "~" is important. + */ +template +static void unescape(StringType& s) +{ + replace_substring(s, StringType{"~1"}, StringType{"/"}); + replace_substring(s, StringType{"~0"}, StringType{"~"}); +} + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // size_t + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/// struct to capture the start position of the current token +struct position_t +{ + /// the total number of characters read + std::size_t chars_read_total = 0; + /// the number of characters read in the current line + std::size_t chars_read_current_line = 0; + /// the number of lines read + std::size_t lines_read = 0; + + /// conversion to size_t to preserve SAX interface + constexpr operator size_t() const + { + return chars_read_total; + } +}; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-FileCopyrightText: 2018 The Abseil Authors +// SPDX-License-Identifier: MIT + + + +#include // array +#include // size_t +#include // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type +#include // index_sequence, make_index_sequence, index_sequence_for + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +template +using uncvref_t = typename std::remove_cv::type>::type; + +#ifdef JSON_HAS_CPP_14 + +// the following utilities are natively available in C++14 +using std::enable_if_t; +using std::index_sequence; +using std::make_index_sequence; +using std::index_sequence_for; + +#else + +// alias templates to reduce boilerplate +template +using enable_if_t = typename std::enable_if::type; + +// The following code is taken from https://github.com/abseil/abseil-cpp/blob/10cb35e459f5ecca5b2ff107635da0bfa41011b4/absl/utility/utility.h +// which is part of Google Abseil (https://github.com/abseil/abseil-cpp), licensed under the Apache License 2.0. + +//// START OF CODE FROM GOOGLE ABSEIL + +// integer_sequence +// +// Class template representing a compile-time integer sequence. An instantiation +// of `integer_sequence` has a sequence of integers encoded in its +// type through its template arguments (which is a common need when +// working with C++11 variadic templates). `absl::integer_sequence` is designed +// to be a drop-in replacement for C++14's `std::integer_sequence`. +// +// Example: +// +// template< class T, T... Ints > +// void user_function(integer_sequence); +// +// int main() +// { +// // user_function's `T` will be deduced to `int` and `Ints...` +// // will be deduced to `0, 1, 2, 3, 4`. +// user_function(make_integer_sequence()); +// } +template +struct integer_sequence +{ + using value_type = T; + static constexpr std::size_t size() noexcept + { + return sizeof...(Ints); + } +}; + +// index_sequence +// +// A helper template for an `integer_sequence` of `size_t`, +// `absl::index_sequence` is designed to be a drop-in replacement for C++14's +// `std::index_sequence`. +template +using index_sequence = integer_sequence; + +namespace utility_internal +{ + +template +struct Extend; + +// Note that SeqSize == sizeof...(Ints). It's passed explicitly for efficiency. +template +struct Extend, SeqSize, 0> +{ + using type = integer_sequence < T, Ints..., (Ints + SeqSize)... >; +}; + +template +struct Extend, SeqSize, 1> +{ + using type = integer_sequence < T, Ints..., (Ints + SeqSize)..., 2 * SeqSize >; +}; + +// Recursion helper for 'make_integer_sequence'. +// 'Gen::type' is an alias for 'integer_sequence'. +template +struct Gen +{ + using type = + typename Extend < typename Gen < T, N / 2 >::type, N / 2, N % 2 >::type; +}; + +template +struct Gen +{ + using type = integer_sequence; +}; + +} // namespace utility_internal + +// Compile-time sequences of integers + +// make_integer_sequence +// +// This template alias is equivalent to +// `integer_sequence`, and is designed to be a drop-in +// replacement for C++14's `std::make_integer_sequence`. +template +using make_integer_sequence = typename utility_internal::Gen::type; + +// make_index_sequence +// +// This template alias is equivalent to `index_sequence<0, 1, ..., N-1>`, +// and is designed to be a drop-in replacement for C++14's +// `std::make_index_sequence`. +template +using make_index_sequence = make_integer_sequence; + +// index_sequence_for +// +// Converts a typename pack into an index sequence of the same length, and +// is designed to be a drop-in replacement for C++14's +// `std::index_sequence_for()` +template +using index_sequence_for = make_index_sequence; + +//// END OF CODE FROM GOOGLE ABSEIL + +#endif + +// dispatch utility (taken from ranges-v3) +template struct priority_tag : priority_tag < N - 1 > {}; +template<> struct priority_tag<0> {}; + +// taken from ranges-v3 +template +struct static_const +{ + static JSON_INLINE_VARIABLE constexpr T value{}; +}; + +#ifndef JSON_HAS_CPP_17 + template + constexpr T static_const::value; +#endif + +template +constexpr std::array make_array(Args&& ... args) +{ + return std::array {{static_cast(std::forward(args))...}}; +} + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // numeric_limits +#include // char_traits +#include // tuple +#include // false_type, is_constructible, is_integral, is_same, true_type +#include // declval + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // random_access_iterator_tag + +// #include + +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +template +struct iterator_types {}; + +template +struct iterator_types < + It, + void_t> +{ + using difference_type = typename It::difference_type; + using value_type = typename It::value_type; + using pointer = typename It::pointer; + using reference = typename It::reference; + using iterator_category = typename It::iterator_category; +}; + +// This is required as some compilers implement std::iterator_traits in a way that +// doesn't work with SFINAE. See https://github.com/nlohmann/json/issues/1341. +template +struct iterator_traits +{ +}; + +template +struct iterator_traits < T, enable_if_t < !std::is_pointer::value >> + : iterator_types +{ +}; + +template +struct iterator_traits::value>> +{ + using iterator_category = std::random_access_iterator_tag; + using value_type = T; + using difference_type = ptrdiff_t; + using pointer = T*; + using reference = T&; +}; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN + +NLOHMANN_CAN_CALL_STD_FUNC_IMPL(begin); + +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN + +NLOHMANN_CAN_CALL_STD_FUNC_IMPL(end); + +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann +// SPDX-License-Identifier: MIT + +#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_ + #define INCLUDE_NLOHMANN_JSON_FWD_HPP_ + + #include // int64_t, uint64_t + #include // map + #include // allocator + #include // string + #include // vector + + // #include + + + /*! + @brief namespace for Niels Lohmann + @see https://github.com/nlohmann + @since version 1.0.0 + */ + NLOHMANN_JSON_NAMESPACE_BEGIN + + /*! + @brief default JSONSerializer template argument + + This serializer ignores the template arguments and uses ADL + ([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl)) + for serialization. + */ + template + struct adl_serializer; + + /// a class to store JSON values + /// @sa https://json.nlohmann.me/api/basic_json/ + template class ObjectType = + std::map, + template class ArrayType = std::vector, + class StringType = std::string, class BooleanType = bool, + class NumberIntegerType = std::int64_t, + class NumberUnsignedType = std::uint64_t, + class NumberFloatType = double, + template class AllocatorType = std::allocator, + template class JSONSerializer = + adl_serializer, + class BinaryType = std::vector, // cppcheck-suppress syntaxError + class CustomBaseClass = void> + class basic_json; + + /// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document + /// @sa https://json.nlohmann.me/api/json_pointer/ + template + class json_pointer; + + /*! + @brief default specialization + @sa https://json.nlohmann.me/api/json/ + */ + using json = basic_json<>; + + /// @brief a minimal map-like container that preserves insertion order + /// @sa https://json.nlohmann.me/api/ordered_map/ + template + struct ordered_map; + + /// @brief specialization that maintains the insertion order of object keys + /// @sa https://json.nlohmann.me/api/ordered_json/ + using ordered_json = basic_json; + + NLOHMANN_JSON_NAMESPACE_END + +#endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_ + + +NLOHMANN_JSON_NAMESPACE_BEGIN +/*! +@brief detail namespace with internal helper functions + +This namespace collects functions that should not be exposed, +implementations of some @ref basic_json methods, and meta-programming helpers. + +@since version 2.1.0 +*/ +namespace detail +{ + +///////////// +// helpers // +///////////// + +// Note to maintainers: +// +// Every trait in this file expects a non CV-qualified type. +// The only exceptions are in the 'aliases for detected' section +// (i.e. those of the form: decltype(T::member_function(std::declval()))) +// +// In this case, T has to be properly CV-qualified to constraint the function arguments +// (e.g. to_json(BasicJsonType&, const T&)) + +template struct is_basic_json : std::false_type {}; + +NLOHMANN_BASIC_JSON_TPL_DECLARATION +struct is_basic_json : std::true_type {}; + +// used by exceptions create() member functions +// true_type for pointer to possibly cv-qualified basic_json or std::nullptr_t +// false_type otherwise +template +struct is_basic_json_context : + std::integral_constant < bool, + is_basic_json::type>::type>::value + || std::is_same::value > +{}; + +////////////////////// +// json_ref helpers // +////////////////////// + +template +class json_ref; + +template +struct is_json_ref : std::false_type {}; + +template +struct is_json_ref> : std::true_type {}; + +////////////////////////// +// aliases for detected // +////////////////////////// + +template +using mapped_type_t = typename T::mapped_type; + +template +using key_type_t = typename T::key_type; + +template +using value_type_t = typename T::value_type; + +template +using difference_type_t = typename T::difference_type; + +template +using pointer_t = typename T::pointer; + +template +using reference_t = typename T::reference; + +template +using iterator_category_t = typename T::iterator_category; + +template +using to_json_function = decltype(T::to_json(std::declval()...)); + +template +using from_json_function = decltype(T::from_json(std::declval()...)); + +template +using get_template_function = decltype(std::declval().template get()); + +// trait checking if JSONSerializer::from_json(json const&, udt&) exists +template +struct has_from_json : std::false_type {}; + +// trait checking if j.get is valid +// use this trait instead of std::is_constructible or std::is_convertible, +// both rely on, or make use of implicit conversions, and thus fail when T +// has several constructors/operator= (see https://github.com/nlohmann/json/issues/958) +template +struct is_getable +{ + static constexpr bool value = is_detected::value; +}; + +template +struct has_from_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> +{ + using serializer = typename BasicJsonType::template json_serializer; + + static constexpr bool value = + is_detected_exact::value; +}; + +// This trait checks if JSONSerializer::from_json(json const&) exists +// this overload is used for non-default-constructible user-defined-types +template +struct has_non_default_from_json : std::false_type {}; + +template +struct has_non_default_from_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> +{ + using serializer = typename BasicJsonType::template json_serializer; + + static constexpr bool value = + is_detected_exact::value; +}; + +// This trait checks if BasicJsonType::json_serializer::to_json exists +// Do not evaluate the trait when T is a basic_json type, to avoid template instantiation infinite recursion. +template +struct has_to_json : std::false_type {}; + +template +struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> +{ + using serializer = typename BasicJsonType::template json_serializer; + + static constexpr bool value = + is_detected_exact::value; +}; + +template +using detect_key_compare = typename T::key_compare; + +template +struct has_key_compare : std::integral_constant::value> {}; + +// obtains the actual object key comparator +template +struct actual_object_comparator +{ + using object_t = typename BasicJsonType::object_t; + using object_comparator_t = typename BasicJsonType::default_object_comparator_t; + using type = typename std::conditional < has_key_compare::value, + typename object_t::key_compare, object_comparator_t>::type; +}; + +template +using actual_object_comparator_t = typename actual_object_comparator::type; + +///////////////// +// char_traits // +///////////////// + +// Primary template of char_traits calls std char_traits +template +struct char_traits : std::char_traits +{}; + +// Explicitly define char traits for unsigned char since it is not standard +template<> +struct char_traits : std::char_traits +{ + using char_type = unsigned char; + using int_type = uint64_t; + + // Redefine to_int_type function + static int_type to_int_type(char_type c) noexcept + { + return static_cast(c); + } + + static char_type to_char_type(int_type i) noexcept + { + return static_cast(i); + } + + static constexpr int_type eof() noexcept + { + return static_cast(std::char_traits::eof()); + } +}; + +// Explicitly define char traits for signed char since it is not standard +template<> +struct char_traits : std::char_traits +{ + using char_type = signed char; + using int_type = uint64_t; + + // Redefine to_int_type function + static int_type to_int_type(char_type c) noexcept + { + return static_cast(c); + } + + static char_type to_char_type(int_type i) noexcept + { + return static_cast(i); + } + + static constexpr int_type eof() noexcept + { + return static_cast(std::char_traits::eof()); + } +}; + +/////////////////// +// is_ functions // +/////////////////// + +// https://en.cppreference.com/w/cpp/types/conjunction +template struct conjunction : std::true_type { }; +template struct conjunction : B { }; +template +struct conjunction +: std::conditional(B::value), conjunction, B>::type {}; + +// https://en.cppreference.com/w/cpp/types/negation +template struct negation : std::integral_constant < bool, !B::value > { }; + +// Reimplementation of is_constructible and is_default_constructible, due to them being broken for +// std::pair and std::tuple until LWG 2367 fix (see https://cplusplus.github.io/LWG/lwg-defects.html#2367). +// This causes compile errors in e.g. clang 3.5 or gcc 4.9. +template +struct is_default_constructible : std::is_default_constructible {}; + +template +struct is_default_constructible> + : conjunction, is_default_constructible> {}; + +template +struct is_default_constructible> + : conjunction, is_default_constructible> {}; + +template +struct is_default_constructible> + : conjunction...> {}; + +template +struct is_default_constructible> + : conjunction...> {}; + +template +struct is_constructible : std::is_constructible {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_iterator_traits : std::false_type {}; + +template +struct is_iterator_traits> +{ + private: + using traits = iterator_traits; + + public: + static constexpr auto value = + is_detected::value && + is_detected::value && + is_detected::value && + is_detected::value && + is_detected::value; +}; + +template +struct is_range +{ + private: + using t_ref = typename std::add_lvalue_reference::type; + + using iterator = detected_t; + using sentinel = detected_t; + + // to be 100% correct, it should use https://en.cppreference.com/w/cpp/iterator/input_or_output_iterator + // and https://en.cppreference.com/w/cpp/iterator/sentinel_for + // but reimplementing these would be too much work, as a lot of other concepts are used underneath + static constexpr auto is_iterator_begin = + is_iterator_traits>::value; + + public: + static constexpr bool value = !std::is_same::value && !std::is_same::value && is_iterator_begin; +}; + +template +using iterator_t = enable_if_t::value, result_of_begin())>>; + +template +using range_value_t = value_type_t>>; + +// The following implementation of is_complete_type is taken from +// https://blogs.msdn.microsoft.com/vcblog/2015/12/02/partial-support-for-expression-sfinae-in-vs-2015-update-1/ +// and is written by Xiang Fan who agreed to using it in this library. + +template +struct is_complete_type : std::false_type {}; + +template +struct is_complete_type : std::true_type {}; + +template +struct is_compatible_object_type_impl : std::false_type {}; + +template +struct is_compatible_object_type_impl < + BasicJsonType, CompatibleObjectType, + enable_if_t < is_detected::value&& + is_detected::value >> +{ + using object_t = typename BasicJsonType::object_t; + + // macOS's is_constructible does not play well with nonesuch... + static constexpr bool value = + is_constructible::value && + is_constructible::value; +}; + +template +struct is_compatible_object_type + : is_compatible_object_type_impl {}; + +template +struct is_constructible_object_type_impl : std::false_type {}; + +template +struct is_constructible_object_type_impl < + BasicJsonType, ConstructibleObjectType, + enable_if_t < is_detected::value&& + is_detected::value >> +{ + using object_t = typename BasicJsonType::object_t; + + static constexpr bool value = + (is_default_constructible::value && + (std::is_move_assignable::value || + std::is_copy_assignable::value) && + (is_constructible::value && + std::is_same < + typename object_t::mapped_type, + typename ConstructibleObjectType::mapped_type >::value)) || + (has_from_json::value || + has_non_default_from_json < + BasicJsonType, + typename ConstructibleObjectType::mapped_type >::value); +}; + +template +struct is_constructible_object_type + : is_constructible_object_type_impl {}; + +template +struct is_compatible_string_type +{ + static constexpr auto value = + is_constructible::value; +}; + +template +struct is_constructible_string_type +{ + // launder type through decltype() to fix compilation failure on ICPC +#ifdef __INTEL_COMPILER + using laundered_type = decltype(std::declval()); +#else + using laundered_type = ConstructibleStringType; +#endif + + static constexpr auto value = + conjunction < + is_constructible, + is_detected_exact>::value; +}; + +template +struct is_compatible_array_type_impl : std::false_type {}; + +template +struct is_compatible_array_type_impl < + BasicJsonType, CompatibleArrayType, + enable_if_t < + is_detected::value&& + is_iterator_traits>>::value&& +// special case for types like std::filesystem::path whose iterator's value_type are themselves +// c.f. https://github.com/nlohmann/json/pull/3073 + !std::is_same>::value >> +{ + static constexpr bool value = + is_constructible>::value; +}; + +template +struct is_compatible_array_type + : is_compatible_array_type_impl {}; + +template +struct is_constructible_array_type_impl : std::false_type {}; + +template +struct is_constructible_array_type_impl < + BasicJsonType, ConstructibleArrayType, + enable_if_t::value >> + : std::true_type {}; + +template +struct is_constructible_array_type_impl < + BasicJsonType, ConstructibleArrayType, + enable_if_t < !std::is_same::value&& + !is_compatible_string_type::value&& + is_default_constructible::value&& +(std::is_move_assignable::value || + std::is_copy_assignable::value)&& +is_detected::value&& +is_iterator_traits>>::value&& +is_detected::value&& +// special case for types like std::filesystem::path whose iterator's value_type are themselves +// c.f. https://github.com/nlohmann/json/pull/3073 +!std::is_same>::value&& +is_complete_type < +detected_t>::value >> +{ + using value_type = range_value_t; + + static constexpr bool value = + std::is_same::value || + has_from_json::value || + has_non_default_from_json < + BasicJsonType, + value_type >::value; +}; + +template +struct is_constructible_array_type + : is_constructible_array_type_impl {}; + +template +struct is_compatible_integer_type_impl : std::false_type {}; + +template +struct is_compatible_integer_type_impl < + RealIntegerType, CompatibleNumberIntegerType, + enable_if_t < std::is_integral::value&& + std::is_integral::value&& + !std::is_same::value >> +{ + // is there an assert somewhere on overflows? + using RealLimits = std::numeric_limits; + using CompatibleLimits = std::numeric_limits; + + static constexpr auto value = + is_constructible::value && + CompatibleLimits::is_integer && + RealLimits::is_signed == CompatibleLimits::is_signed; +}; + +template +struct is_compatible_integer_type + : is_compatible_integer_type_impl {}; + +template +struct is_compatible_type_impl: std::false_type {}; + +template +struct is_compatible_type_impl < + BasicJsonType, CompatibleType, + enable_if_t::value >> +{ + static constexpr bool value = + has_to_json::value; +}; + +template +struct is_compatible_type + : is_compatible_type_impl {}; + +template +struct is_constructible_tuple : std::false_type {}; + +template +struct is_constructible_tuple> : conjunction...> {}; + +template +struct is_json_iterator_of : std::false_type {}; + +template +struct is_json_iterator_of : std::true_type {}; + +template +struct is_json_iterator_of : std::true_type +{}; + +// checks if a given type T is a template specialization of Primary +template